/* -*- 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 = Promise.withResolvers(); 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 = Promise.withResolvers(); 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 = Promise.withResolvers(); 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 = Promise.withResolvers(); 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", }, ]); });