diff options
Diffstat (limited to 'browser/components/downloads/test')
48 files changed, 7168 insertions, 0 deletions
diff --git a/browser/components/downloads/test/browser/blank.JPG b/browser/components/downloads/test/browser/blank.JPG Binary files differnew file mode 100644 index 0000000000..1cda9a53dc --- /dev/null +++ b/browser/components/downloads/test/browser/blank.JPG diff --git a/browser/components/downloads/test/browser/browser.toml b/browser/components/downloads/test/browser/browser.toml new file mode 100644 index 0000000000..8236e452e6 --- /dev/null +++ b/browser/components/downloads/test/browser/browser.toml @@ -0,0 +1,100 @@ +[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'", # Bug 1775779 +] +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 = [ + "win10_2009 && bits == 64 && !debug", # Bug 1719949 +] + +["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"] + +["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..78433bfff4 --- /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.startLoadingURIString(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..ce2c64a799 --- /dev/null +++ b/browser/components/downloads/test/browser/browser_download_spam_protection.js @@ -0,0 +1,217 @@ +/* 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, { + BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs", + DownloadSpamProtection: "resource:///modules/DownloadSpamProtection.sys.mjs", + PermissionTestUtils: "resource://testing-common/PermissionTestUtils.sys.mjs", +}); + +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..eb823e09e8 --- /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.isHidden(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.isHidden(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..8be0d86ec5 --- /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.isHidden(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.isHidden(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..32764436eb --- /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.isHidden(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..d1035db08d --- /dev/null +++ b/browser/components/downloads/test/browser/browser_downloads_panel_block.js @@ -0,0 +1,130 @@ +/* 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 task_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 task_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 task_openPanel(); + Assert.ok(!item.parentNode); + + hidePromise = promisePanelHidden(); + DownloadsPanel.hidePanel(); + await hidePromise; + } + + await task_resetState(); +}); + +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..15d6c3c897 --- /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.isVisible(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..7c44939e59 --- /dev/null +++ b/browser/components/downloads/test/browser/browser_downloads_panel_opens.js @@ -0,0 +1,672 @@ +/* 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"); + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(r => setTimeout(r, 100)); + is( + DownloadsPanel.panel.state, + "closed", + "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..e5145d524d --- /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.isVisible(contextMenu), + "The context menu is visible" + ); + await TestUtils.waitForTick(); + + info("Checking visibility of the system viewer menu items"); + is( + BrowserTestUtils.isHidden(useSystemMenuItem), + expected.useSystemMenuItemDisabled, + `The 'Use system viewer' menu item was ${ + expected.useSystemMenuItemDisabled ? "hidden" : "visible" + }` + ); + is( + BrowserTestUtils.isHidden(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.isVisible(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.startLoadingURIString(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..ba22421aa3 --- /dev/null +++ b/browser/components/downloads/test/browser/head.js @@ -0,0 +1,450 @@ +/* -*- 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", + HttpServer: "resource://testing-common/httpd.sys.mjs", + PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", +}); + +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 = Promise.withResolvers(); + +/** + * 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 = Promise.withResolvers(); +} + +/** + * 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 = new FileUtils.File( + PathUtils.join(PathUtils.tempDir, `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; + + await BrowserTestUtils.waitForMutationCondition( + DownloadsView.richListBox, + { attributeFilter: ["disabled"] }, + () => !DownloadsView.richListBox.hasAttribute("disabled") + ); +} + +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 Binary files differnew file mode 100644 index 0000000000..04b7f003b4 --- /dev/null +++ b/browser/components/downloads/test/browser/not-really-a-jpeg.jpeg 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..b0774a45e2 --- /dev/null +++ b/browser/components/downloads/test/unit/head.js @@ -0,0 +1,63 @@ +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", + NetUtil: "resource://gre/modules/NetUtil.sys.mjs", + TestUtils: "resource://testing-common/TestUtils.sys.mjs", +}); + +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..75f2a65df5 --- /dev/null +++ b/browser/components/downloads/test/unit/test_DownloadLastDir_basics.js @@ -0,0 +1,177 @@ +/* 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"]); + dir1.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); + let uri1 = Services.io.newURI("https://test1.moz.org"); + downloadLastDir.setFile(uri1, dir1); + let dir2 = FileUtils.getDir("TmpD", ["dir2"]); + dir2.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); + let uri2 = Services.io.newURI("https://test2.moz.org"); + downloadLastDir.setFile(uri2, dir2); + let dir3 = FileUtils.getDir("TmpD", ["dir3"]); + dir3.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); + 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"]); + dir4.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); + 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" + ); + + info("blob: URIs should point to a folder based on their origin."); + let blobUri1 = Services.io.newURI( + "blob:https://chat.mozilla.org/35d6a992-6e18-4957-8216-070c53b9bc83" + ); + let blobOriginUri1 = Services.io.newURI("https://chat.mozilla.org/"); + downloadLastDir.setFile(blobUri1, dir1); + Assert.equal( + (await downloadLastDir.getFileAsync(blobUri1)).path, + (await downloadLastDir.getFileAsync(blobOriginUri1)).path, + "Check blob URI" + ); + // While we are no longer supposed to store pdf.js URLs like this, this + // test remains to cover resource origins. + info("Test blob: URIs to local resouce."); + let blobUri2 = Services.io.newURI( + "blob:resource://pdf.js/ed645567-3eea-4ff1-94fd-efb04812afe0" + ); + let blobOriginUri2 = Services.io.newURI("resource://pdf.js/"); + downloadLastDir.setFile(blobUri2, dir2); + Assert.equal( + (await downloadLastDir.getFileAsync(blobUri2)).path, + (await downloadLastDir.getFileAsync(blobOriginUri2)).path, + "Check blob URI" + ); + info("Test an empty blob:"); + let noOriginBlobUri = Services.io.newURI("blob:"); + downloadLastDir.setFile(blobUri2, dir3); + Assert.equal( + (await downloadLastDir.getFileAsync(noOriginBlobUri)).path, + dir3.path, + "Check blob URI" + ); + } +); 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..29d88de55a --- /dev/null +++ b/browser/components/downloads/test/unit/test_DownloadsViewableInternally.js @@ -0,0 +1,238 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const PREF_SVG_DISABLED = "svg.disabled"; +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 { 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"); + Services.prefs.setBoolPref(PREF_SVG_DISABLED, false); + Services.prefs.setBoolPref(PREF_AVIF_ENABLED, true); + + checkAll(XML_MIME, "xml", false); + checkAll(SVG_MIME, "svg", false); + checkAll(AVIF_MIME, "avif", false); + + DownloadsViewableInternally.register(); + + checkAll(XML_MIME, "xml", true); + checkAll(SVG_MIME, "svg", true); + checkAll(AVIF_MIME, "avif", true); + + // Disable xml, and avif, check that avif becomes disabled + Services.prefs.setCharPref(PREF_ENABLED_TYPES, "svg"); + + // (XML is externally managed) + checkAll(XML_MIME, "xml", 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(AVIF_MIME), "image/avif should be disabled by pref"); + + // Enable, check that everything is enabled again + Services.prefs.setCharPref(PREF_ENABLED_TYPES, "xml,svg,avif"); + + checkAll(XML_MIME, "xml", true); + checkAll(SVG_MIME, "svg", 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], + ]) { + 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. + Services.prefs.setCharPref(PREF_ENABLED_TYPES, "svg,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" + ); + + { + 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); + + // 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); + + 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); +}); diff --git a/browser/components/downloads/test/unit/xpcshell.toml b/browser/components/downloads/test/unit/xpcshell.toml new file mode 100644 index 0000000000..37a26b4b65 --- /dev/null +++ b/browser/components/downloads/test/unit/xpcshell.toml @@ -0,0 +1,12 @@ +[DEFAULT] +head = "head.js" +firefox-appdir = "browser" +skip-if = ["os == 'android'"] # bug 1730213 + +["test_DownloadLastDir_basics.js"] + +["test_DownloadsCommon_getMimeInfo.js"] + +["test_DownloadsCommon_isFileOfType.js"] + +["test_DownloadsViewableInternally.js"] |