From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- .../downloads/test/unit/test_DownloadList.js | 677 +++++++++++++++++++++ 1 file changed, 677 insertions(+) create mode 100644 toolkit/components/downloads/test/unit/test_DownloadList.js (limited to 'toolkit/components/downloads/test/unit/test_DownloadList.js') diff --git a/toolkit/components/downloads/test/unit/test_DownloadList.js b/toolkit/components/downloads/test/unit/test_DownloadList.js new file mode 100644 index 0000000000..17ac5ba13c --- /dev/null +++ b/toolkit/components/downloads/test/unit/test_DownloadList.js @@ -0,0 +1,677 @@ +/* -*- 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/ */ + +/** + * Tests the DownloadList object. + */ + +"use strict"; + +// Globals + +const { TelemetryTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TelemetryTestUtils.sys.mjs" +); + +Services.prefs.setBoolPref( + "toolkit.telemetry.testing.overrideProductsCheck", + true +); + +/** + * Returns a Date in the past usable to add expirable visits. + * + * @note Expiration ignores any visit added in the last 7 days, but it's + * better be safe against DST issues, by going back one day more. + */ +function getExpirableDate() { + let dateObj = new Date(); + // Normalize to midnight + dateObj.setHours(0); + dateObj.setMinutes(0); + dateObj.setSeconds(0); + dateObj.setMilliseconds(0); + return new Date(dateObj.getTime() - 8 * 86400000); +} + +/** + * Adds an expirable history visit for a download. + * + * @param aSourceUrl + * String containing the URI for the download source, or null to use + * httpUrl("source.txt"). + * + * @return {Promise} + * @rejects JavaScript exception. + */ +function promiseExpirableDownloadVisit(aSourceUrl) { + return PlacesUtils.history.insert({ + url: aSourceUrl || httpUrl("source.txt"), + visits: [ + { + transition: PlacesUtils.history.TRANSITIONS.DOWNLOAD, + date: getExpirableDate(), + }, + ], + }); +} + +// Tests + +/** + * Checks the testing mechanism used to build different download lists. + */ +add_task(async function test_construction() { + let downloadListOne = await promiseNewList(); + let downloadListTwo = await promiseNewList(); + let privateDownloadListOne = await promiseNewList(true); + let privateDownloadListTwo = await promiseNewList(true); + + Assert.notEqual(downloadListOne, downloadListTwo); + Assert.notEqual(privateDownloadListOne, privateDownloadListTwo); + Assert.notEqual(downloadListOne, privateDownloadListOne); +}); + +/** + * Checks the methods to add and retrieve items from the list. + */ +add_task(async function test_add_getAll() { + let list = await promiseNewList(); + + let downloadOne = await promiseNewDownload(); + await list.add(downloadOne); + + let itemsOne = await list.getAll(); + Assert.equal(itemsOne.length, 1); + Assert.equal(itemsOne[0], downloadOne); + + let downloadTwo = await promiseNewDownload(); + await list.add(downloadTwo); + + let itemsTwo = await list.getAll(); + Assert.equal(itemsTwo.length, 2); + Assert.equal(itemsTwo[0], downloadOne); + Assert.equal(itemsTwo[1], downloadTwo); + + // The first snapshot should not have been modified. + Assert.equal(itemsOne.length, 1); +}); + +/** + * Checks the method to remove items from the list. + */ +add_task(async function test_remove() { + let list = await promiseNewList(); + + await list.add(await promiseNewDownload()); + await list.add(await promiseNewDownload()); + + let items = await list.getAll(); + await list.remove(items[0]); + + // Removing an item that was never added should not raise an error. + await list.remove(await promiseNewDownload()); + + items = await list.getAll(); + Assert.equal(items.length, 1); +}); + +/** + * Tests that the "add", "remove", and "getAll" methods on the global + * DownloadCombinedList object combine the contents of the global DownloadList + * objects for public and private downloads. + */ +add_task(async function test_DownloadCombinedList_add_remove_getAll() { + let publicList = await promiseNewList(); + let privateList = await Downloads.getList(Downloads.PRIVATE); + let combinedList = await Downloads.getList(Downloads.ALL); + + let publicDownload = await promiseNewDownload(); + let privateDownload = await Downloads.createDownload({ + source: { url: httpUrl("source.txt"), isPrivate: true }, + target: getTempFile(TEST_TARGET_FILE_NAME).path, + }); + + await publicList.add(publicDownload); + await privateList.add(privateDownload); + + Assert.equal((await combinedList.getAll()).length, 2); + + await combinedList.remove(publicDownload); + await combinedList.remove(privateDownload); + + Assert.equal((await combinedList.getAll()).length, 0); + + await combinedList.add(publicDownload); + await combinedList.add(privateDownload); + + Assert.equal((await publicList.getAll()).length, 1); + Assert.equal((await privateList.getAll()).length, 1); + Assert.equal((await combinedList.getAll()).length, 2); + + await publicList.remove(publicDownload); + await privateList.remove(privateDownload); + + Assert.equal((await combinedList.getAll()).length, 0); +}); + +/** + * Checks that views receive the download add and remove notifications, and that + * adding and removing views works as expected, both for a normal and a combined + * list. + */ +add_task(async function test_notifications_add_remove() { + for (let isCombined of [false, true]) { + // Force creating a new list for both the public and combined cases. + let list = await promiseNewList(); + if (isCombined) { + list = await Downloads.getList(Downloads.ALL); + } + + let downloadOne = await promiseNewDownload(); + let downloadTwo = await Downloads.createDownload({ + source: { url: httpUrl("source.txt"), isPrivate: true }, + target: getTempFile(TEST_TARGET_FILE_NAME).path, + }); + await list.add(downloadOne); + await list.add(downloadTwo); + + // Check that we receive add notifications for existing elements. + let addNotifications = 0; + let viewOne = { + onDownloadAdded(aDownload) { + // The first download to be notified should be the first that was added. + if (addNotifications == 0) { + Assert.equal(aDownload, downloadOne); + } else if (addNotifications == 1) { + Assert.equal(aDownload, downloadTwo); + } + addNotifications++; + }, + }; + await list.addView(viewOne); + Assert.equal(addNotifications, 2); + + // Check that we receive add notifications for new elements. + await list.add(await promiseNewDownload()); + Assert.equal(addNotifications, 3); + + // Check that we receive remove notifications. + let removeNotifications = 0; + let viewTwo = { + onDownloadRemoved(aDownload) { + Assert.equal(aDownload, downloadOne); + removeNotifications++; + }, + }; + await list.addView(viewTwo); + await list.remove(downloadOne); + Assert.equal(removeNotifications, 1); + + // We should not receive remove notifications after the view is removed. + await list.removeView(viewTwo); + await list.remove(downloadTwo); + Assert.equal(removeNotifications, 1); + + // We should not receive add notifications after the view is removed. + await list.removeView(viewOne); + await list.add(await promiseNewDownload()); + Assert.equal(addNotifications, 3); + } +}); + +/** + * Checks that views receive the download change notifications, both for a + * normal and a combined list. + */ +add_task(async function test_notifications_change() { + for (let isCombined of [false, true]) { + // Force creating a new list for both the public and combined cases. + let list = await promiseNewList(); + if (isCombined) { + list = await Downloads.getList(Downloads.ALL); + } + + let downloadOne = await promiseNewDownload(); + let downloadTwo = await Downloads.createDownload({ + source: { url: httpUrl("source.txt"), isPrivate: true }, + target: getTempFile(TEST_TARGET_FILE_NAME).path, + }); + await list.add(downloadOne); + await list.add(downloadTwo); + + // Check that we receive change notifications. + let receivedOnDownloadChanged = false; + await list.addView({ + onDownloadChanged(aDownload) { + Assert.equal(aDownload, downloadOne); + receivedOnDownloadChanged = true; + }, + }); + await downloadOne.start(); + Assert.ok(receivedOnDownloadChanged); + + // We should not receive change notifications after a download is removed. + receivedOnDownloadChanged = false; + await list.remove(downloadTwo); + await downloadTwo.start(); + Assert.ok(!receivedOnDownloadChanged); + } +}); + +/** + * Checks that the reference to "this" is correct in the view callbacks. + */ +add_task(async function test_notifications_this() { + let list = await promiseNewList(); + + // Check that we receive change notifications. + let receivedOnDownloadAdded = false; + let receivedOnDownloadChanged = false; + let receivedOnDownloadRemoved = false; + let view = { + onDownloadAdded() { + Assert.equal(this, view); + receivedOnDownloadAdded = true; + }, + onDownloadChanged() { + // Only do this check once. + if (!receivedOnDownloadChanged) { + Assert.equal(this, view); + receivedOnDownloadChanged = true; + } + }, + onDownloadRemoved() { + Assert.equal(this, view); + receivedOnDownloadRemoved = true; + }, + }; + await list.addView(view); + + let download = await promiseNewDownload(); + await list.add(download); + await download.start(); + await list.remove(download); + + // Verify that we executed the checks. + Assert.ok(receivedOnDownloadAdded); + Assert.ok(receivedOnDownloadChanged); + Assert.ok(receivedOnDownloadRemoved); +}); + +/** + * Checks that download is removed on history expiration. + */ +add_task(async function test_history_expiration() { + mustInterruptResponses(); + + function cleanup() { + Services.prefs.clearUserPref("places.history.expiration.max_pages"); + } + registerCleanupFunction(cleanup); + + // Set max pages to 0 to make the download expire. + Services.prefs.setIntPref("places.history.expiration.max_pages", 0); + + let list = await promiseNewList(); + let downloadOne = await promiseNewDownload(); + let downloadTwo = await promiseNewDownload(httpUrl("interruptible.txt")); + + let deferred = PromiseUtils.defer(); + let removeNotifications = 0; + let downloadView = { + onDownloadRemoved(aDownload) { + if (++removeNotifications == 2) { + deferred.resolve(); + } + }, + }; + await list.addView(downloadView); + + // Work with one finished download and one canceled download. + await downloadOne.start(); + downloadTwo.start().catch(() => {}); + await downloadTwo.cancel(); + + // We must replace the visits added while executing the downloads with visits + // that are older than 7 days, otherwise they will not be expired. + await PlacesUtils.history.clear(); + await promiseExpirableDownloadVisit(); + await promiseExpirableDownloadVisit(httpUrl("interruptible.txt")); + + // After clearing history, we can add the downloads to be removed to the list. + await list.add(downloadOne); + await list.add(downloadTwo); + + // Force a history expiration. + Cc["@mozilla.org/places/expiration;1"] + .getService(Ci.nsIObserver) + .observe(null, "places-debug-start-expiration", -1); + + // Wait for both downloads to be removed. + await deferred.promise; + + cleanup(); +}); + +/** + * Checks all downloads are removed after clearing history. + */ +add_task(async function test_history_clear() { + let list = await promiseNewList(); + let downloadOne = await promiseNewDownload(); + let downloadTwo = await promiseNewDownload(); + await list.add(downloadOne); + await list.add(downloadTwo); + + let deferred = PromiseUtils.defer(); + let removeNotifications = 0; + let downloadView = { + onDownloadRemoved(aDownload) { + if (++removeNotifications == 2) { + deferred.resolve(); + } + }, + }; + await list.addView(downloadView); + + await downloadOne.start(); + await downloadTwo.start(); + + await PlacesUtils.history.clear(); + + // Wait for the removal notifications that may still be pending. + await deferred.promise; +}); + +/** + * Tests the removeFinished method to ensure that it only removes + * finished downloads. + */ +add_task(async function test_removeFinished() { + let list = await promiseNewList(); + let downloadOne = await promiseNewDownload(); + let downloadTwo = await promiseNewDownload(); + let downloadThree = await promiseNewDownload(); + let downloadFour = await promiseNewDownload(); + await list.add(downloadOne); + await list.add(downloadTwo); + await list.add(downloadThree); + await list.add(downloadFour); + + let deferred = PromiseUtils.defer(); + let removeNotifications = 0; + let downloadView = { + onDownloadRemoved(aDownload) { + Assert.ok( + aDownload == downloadOne || + aDownload == downloadTwo || + aDownload == downloadThree + ); + Assert.ok(removeNotifications < 3); + if (++removeNotifications == 3) { + deferred.resolve(); + } + }, + }; + await list.addView(downloadView); + + // Start three of the downloads, but don't start downloadTwo, then set + // downloadFour to have partial data. All downloads except downloadFour + // should be removed. + await downloadOne.start(); + await downloadThree.start(); + await downloadFour.start(); + downloadFour.hasPartialData = true; + + list.removeFinished(); + await deferred.promise; + + let downloads = await list.getAll(); + Assert.equal(downloads.length, 1); +}); + +/** + * Tests that removeFinished method keeps the file that is currently downloading, + * even if it needs to remove failed download of the same file. + */ +add_task(async function test_removeFinished_keepsDownloadingFile() { + let targetFile = getTempFile(TEST_TARGET_FILE_NAME); + + let oneDownload = await Downloads.createDownload({ + source: httpUrl("empty.txt"), + target: targetFile.path, + }); + + let otherDownload = await Downloads.createDownload({ + source: httpUrl("empty.txt"), + target: targetFile.path, + }); + + let list = await promiseNewList(); + await list.add(oneDownload); + await list.add(otherDownload); + + let deferred = PromiseUtils.defer(); + let downloadView = { + async onDownloadRemoved(aDownload) { + Assert.equal(aDownload, oneDownload); + await TestUtils.waitForCondition(() => oneDownload._finalizeExecuted); + deferred.resolve(); + }, + }; + await list.addView(downloadView); + + await oneDownload.start(); + await otherDownload.start(); + + oneDownload.hasPartialData = otherDownload.hasPartialData = true; + oneDownload.error = "Download failed"; + + list.removeFinished(); + await deferred.promise; + + let downloads = await list.getAll(); + Assert.equal( + downloads.length, + 1, + "Failed download should be removed, active download should be kept" + ); + + Assert.ok( + await IOUtils.exists(otherDownload.target.path), + "The file should not have been deleted." + ); +}); + +/** + * Tests the global DownloadSummary objects for the public, private, and + * combined download lists. + */ +add_task(async function test_DownloadSummary() { + mustInterruptResponses(); + + let publicList = await promiseNewList(); + let privateList = await Downloads.getList(Downloads.PRIVATE); + + let publicSummary = await Downloads.getSummary(Downloads.PUBLIC); + let privateSummary = await Downloads.getSummary(Downloads.PRIVATE); + let combinedSummary = await Downloads.getSummary(Downloads.ALL); + + // Add a public download that has succeeded. + let succeededPublicDownload = await promiseNewDownload(); + await succeededPublicDownload.start(); + await publicList.add(succeededPublicDownload); + + // Add a public download that has been canceled midway. + let canceledPublicDownload = await promiseNewDownload( + httpUrl("interruptible.txt") + ); + canceledPublicDownload.start().catch(() => {}); + await promiseDownloadMidway(canceledPublicDownload); + await canceledPublicDownload.cancel(); + await publicList.add(canceledPublicDownload); + + // Add a public download that is in progress. + let inProgressPublicDownload = await promiseNewDownload( + httpUrl("interruptible.txt") + ); + inProgressPublicDownload.start().catch(() => {}); + await promiseDownloadMidway(inProgressPublicDownload); + await publicList.add(inProgressPublicDownload); + + // Add a public download of unknown size that is in progress. + let inProgressSizelessPublicDownload = await promiseNewDownload( + httpUrl("interruptible_nosize.txt") + ); + inProgressSizelessPublicDownload.start().catch(() => {}); + await promiseDownloadStarted(inProgressSizelessPublicDownload); + await publicList.add(inProgressSizelessPublicDownload); + + // Add a private download that is in progress. + let inProgressPrivateDownload = await Downloads.createDownload({ + source: { url: httpUrl("interruptible.txt"), isPrivate: true }, + target: getTempFile(TEST_TARGET_FILE_NAME).path, + }); + inProgressPrivateDownload.start().catch(() => {}); + await promiseDownloadMidway(inProgressPrivateDownload); + await privateList.add(inProgressPrivateDownload); + + // Verify that the summary includes the total number of bytes and the + // currently transferred bytes only for the downloads that are not stopped. + // For simplicity, we assume that after a download is added to the list, its + // current state is immediately propagated to the summary object, which is + // true in the current implementation, though it is not guaranteed as all the + // download operations may happen asynchronously. + Assert.ok(!publicSummary.allHaveStopped); + Assert.ok(!publicSummary.allUnknownSize); + Assert.equal(publicSummary.progressTotalBytes, TEST_DATA_SHORT.length * 3); + Assert.equal(publicSummary.progressCurrentBytes, TEST_DATA_SHORT.length * 2); + + Assert.ok(!privateSummary.allHaveStopped); + Assert.ok(!privateSummary.allUnknownSize); + Assert.equal(privateSummary.progressTotalBytes, TEST_DATA_SHORT.length * 2); + Assert.equal(privateSummary.progressCurrentBytes, TEST_DATA_SHORT.length); + + Assert.ok(!combinedSummary.allHaveStopped); + Assert.ok(!combinedSummary.allUnknownSize); + Assert.equal(combinedSummary.progressTotalBytes, TEST_DATA_SHORT.length * 5); + Assert.equal( + combinedSummary.progressCurrentBytes, + TEST_DATA_SHORT.length * 3 + ); + + await inProgressPublicDownload.cancel(); + + // Stopping the download should have excluded it from the summary, but we + // should still have one public download (with unknown size) and also one + // private download remaining. + Assert.ok(!publicSummary.allHaveStopped); + Assert.ok(publicSummary.allUnknownSize); + Assert.equal(publicSummary.progressTotalBytes, TEST_DATA_SHORT.length); + Assert.equal(publicSummary.progressCurrentBytes, TEST_DATA_SHORT.length); + + Assert.ok(!privateSummary.allHaveStopped); + Assert.ok(!privateSummary.allUnknownSize); + Assert.equal(privateSummary.progressTotalBytes, TEST_DATA_SHORT.length * 2); + Assert.equal(privateSummary.progressCurrentBytes, TEST_DATA_SHORT.length); + + Assert.ok(!combinedSummary.allHaveStopped); + Assert.ok(!combinedSummary.allUnknownSize); + Assert.equal(combinedSummary.progressTotalBytes, TEST_DATA_SHORT.length * 3); + Assert.equal( + combinedSummary.progressCurrentBytes, + TEST_DATA_SHORT.length * 2 + ); + + await inProgressPrivateDownload.cancel(); + + // Stopping the private download should have excluded it from the summary, so + // now only the unknown size public download should remain. + Assert.ok(!publicSummary.allHaveStopped); + Assert.ok(publicSummary.allUnknownSize); + Assert.equal(publicSummary.progressTotalBytes, TEST_DATA_SHORT.length); + Assert.equal(publicSummary.progressCurrentBytes, TEST_DATA_SHORT.length); + + Assert.ok(privateSummary.allHaveStopped); + Assert.ok(privateSummary.allUnknownSize); + Assert.equal(privateSummary.progressTotalBytes, 0); + Assert.equal(privateSummary.progressCurrentBytes, 0); + + Assert.ok(!combinedSummary.allHaveStopped); + Assert.ok(combinedSummary.allUnknownSize); + Assert.equal(combinedSummary.progressTotalBytes, TEST_DATA_SHORT.length); + Assert.equal(combinedSummary.progressCurrentBytes, TEST_DATA_SHORT.length); + + await inProgressSizelessPublicDownload.cancel(); + + // All the downloads should be stopped now. + Assert.ok(publicSummary.allHaveStopped); + Assert.ok(publicSummary.allUnknownSize); + Assert.equal(publicSummary.progressTotalBytes, 0); + Assert.equal(publicSummary.progressCurrentBytes, 0); + + Assert.ok(privateSummary.allHaveStopped); + Assert.ok(privateSummary.allUnknownSize); + Assert.equal(privateSummary.progressTotalBytes, 0); + Assert.equal(privateSummary.progressCurrentBytes, 0); + + Assert.ok(combinedSummary.allHaveStopped); + Assert.ok(combinedSummary.allUnknownSize); + Assert.equal(combinedSummary.progressTotalBytes, 0); + Assert.equal(combinedSummary.progressCurrentBytes, 0); +}); + +/** + * Checks that views receive the summary change notification. This is tested on + * the combined summary when adding a public download, as we assume that if we + * pass the test in this case we will also pass it in the others. + */ +add_task(async function test_DownloadSummary_notifications() { + let list = await promiseNewList(); + let summary = await Downloads.getSummary(Downloads.ALL); + + let download = await promiseNewDownload(); + await list.add(download); + + // Check that we receive change notifications. + let receivedOnSummaryChanged = false; + await summary.addView({ + onSummaryChanged() { + receivedOnSummaryChanged = true; + }, + }); + await download.start(); + Assert.ok(receivedOnSummaryChanged); +}); + +/** + * Tests that if a new download is added, telemetry records the event and type of the file. + */ +add_task(async function test_downloadAddedTelemetry() { + Services.telemetry.clearEvents(); + Services.telemetry.setEventRecordingEnabled("downloads", true); + + let targetFile = getTempFile(TEST_TARGET_FILE_NAME); + + let download = await Downloads.createDownload({ + source: httpUrl("empty.txt"), + target: targetFile.path, + }); + + let list = await Downloads.getList(Downloads.ALL); + await list.add(download); + download.start(); + await promiseDownloadFinished(download); + + TelemetryTestUtils.assertEvents([ + { + category: "downloads", + method: "added", + object: "fileExtension", + value: "txt", + }, + ]); +}); -- cgit v1.2.3