/* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ /** * Tests the DownloadHistory module. */ "use strict"; const { DownloadHistory } = ChromeUtils.importESModule( "resource://gre/modules/DownloadHistory.sys.mjs" ); let baseDate = new Date("2000-01-01"); /** * Non-fatal assertion used to test whether the downloads in the list already * match the expected state. */ function areEqual(a, b) { if (a === b) { Assert.equal(a, b); return true; } info(a + " !== " + b); return false; } /** * This allows waiting for an expected list at various points during the test. */ class TestView { constructor(expected) { this.expected = [...expected]; this.downloads = []; this.resolveWhenExpected = () => {}; } onDownloadAdded(download, options = {}) { if (options.insertBefore) { let index = this.downloads.indexOf(options.insertBefore); this.downloads.splice(index, 0, download); } else { this.downloads.push(download); } this.checkForExpectedDownloads(); } onDownloadChanged() { this.checkForExpectedDownloads(); } onDownloadRemoved(download) { let index = this.downloads.indexOf(download); this.downloads.splice(index, 1); this.checkForExpectedDownloads(); } checkForExpectedDownloads() { // Wait for all the expected downloads to be added or removed before doing // the detailed tests. This is done to avoid creating irrelevant output. if (this.downloads.length != this.expected.length) { return; } for (let i = 0; i < this.downloads.length; i++) { if ( this.downloads[i].source.url != this.expected[i].source.url || this.downloads[i].target.path != this.expected[i].target.path ) { return; } } // Check and report the actual state of the downloads. Even if the items // are in the expected order, the metadata for history downloads might not // have been updated to the final state yet. for (let i = 0; i < this.downloads.length; i++) { let download = this.downloads[i]; let testDownload = this.expected[i]; info( "Checking download source " + download.source.url + " with target " + download.target.path ); if ( !areEqual(download.succeeded, !!testDownload.succeeded) || !areEqual(download.canceled, !!testDownload.canceled) || !areEqual(download.hasPartialData, !!testDownload.hasPartialData) || !areEqual(!!download.error, !!testDownload.error) ) { return; } // If the above properties match, the error details should be correct. if (download.error) { if (testDownload.error.becauseSourceFailed) { Assert.equal(download.error.message, "History download failed."); } Assert.equal( download.error.becauseBlockedByParentalControls, testDownload.error.becauseBlockedByParentalControls ); Assert.equal( download.error.becauseBlockedByReputationCheck, testDownload.error.becauseBlockedByReputationCheck ); } } this.resolveWhenExpected(); } async waitForExpected() { let promise = new Promise(resolve => (this.resolveWhenExpected = resolve)); this.checkForExpectedDownloads(); await promise; } } /** * Tests that various operations on session and history downloads are reflected * by the DownloadHistoryList object, and that the order of results is correct. */ add_task(async function test_DownloadHistory() { // Clean up at the beginning and at the end of the test. async function cleanup() { await PlacesUtils.history.clear(); } registerCleanupFunction(cleanup); await cleanup(); let testDownloads = [ // History downloads should appear in order at the beginning of the list. { offset: 10, canceled: true }, { offset: 20, succeeded: true }, { offset: 30, error: { becauseSourceFailed: true } }, { offset: 40, error: { becauseBlockedByParentalControls: true } }, { offset: 50, error: { becauseBlockedByReputationCheck: true } }, // Session downloads should show up after all the history download, in the // same order as they were added. { offset: 45, canceled: true, inSession: true }, { offset: 35, canceled: true, hasPartialData: true, inSession: true }, { offset: 55, succeeded: true, inSession: true }, ]; const NEXT_OFFSET = 60; let publicList = await promiseNewList(); let allList = await Downloads.getList(Downloads.ALL); async function addTestDownload(properties) { properties.source = { url: httpUrl("source" + properties.offset), isPrivate: properties.isPrivate, }; let targetFile = getTempFile(TEST_TARGET_FILE_NAME + properties.offset); properties.target = { path: targetFile.path }; properties.startTime = new Date(baseDate.getTime() + properties.offset); let download = await Downloads.createDownload(properties); if (properties.inSession) { await allList.add(download); } if (properties.isPrivate) { return; } // Add the download to history using the XPCOM service, then use the // DownloadHistory module to save the associated metadata. let promiseFileAnnotation = waitForAnnotation( properties.source.url, "downloads/destinationFileURI" ); let promiseMetaAnnotation = waitForAnnotation( properties.source.url, "downloads/metaData" ); let promiseVisit = promiseWaitForVisit(properties.source.url); await DownloadHistory.addDownloadToHistory(download); await promiseVisit; await DownloadHistory.updateMetaData(download); await Promise.all([promiseFileAnnotation, promiseMetaAnnotation]); } // Add all the test downloads to history. for (let properties of testDownloads) { await addTestDownload(properties); } // Initialize DownloadHistoryList only after having added the history and // session downloads, and check that they are loaded in the correct order. let historyList = await DownloadHistory.getList(); let view = new TestView(testDownloads); await historyList.addView(view); await view.waitForExpected(); // Remove a download from history and verify that the change is reflected. let downloadToRemove = view.expected[1]; view.expected.splice(1, 1); await PlacesUtils.history.remove(downloadToRemove.source.url); await view.waitForExpected(); // Add a download to history and verify it's placed before session downloads, // even if the start date is more recent. let downloadToAdd = { offset: NEXT_OFFSET, canceled: true }; view.expected.splice( view.expected.findIndex(d => d.inSession), 0, downloadToAdd ); await addTestDownload(downloadToAdd); await view.waitForExpected(); // Add a session download and verify it's placed after all session downloads, // even if the start date is less recent. let sessionDownloadToAdd = { offset: 0, inSession: true, succeeded: true }; view.expected.push(sessionDownloadToAdd); await addTestDownload(sessionDownloadToAdd); await view.waitForExpected(); // Add a session download for the same URI without a history entry, and verify // it's visible and placed after all session downloads. view.expected.push(sessionDownloadToAdd); await publicList.add(await Downloads.createDownload(sessionDownloadToAdd)); await view.waitForExpected(); // Create a new DownloadHistoryList that also shows private downloads. Since // we only have public downloads, the two lists should contain the same items. let allHistoryList = await DownloadHistory.getList({ type: Downloads.ALL }); let allView = new TestView(view.expected); await allHistoryList.addView(allView); await allView.waitForExpected(); // Add a new private download and verify it appears only on the complete list. let privateDownloadToAdd = { offset: NEXT_OFFSET + 10, inSession: true, succeeded: true, isPrivate: true, }; allView.expected.push(privateDownloadToAdd); await addTestDownload(privateDownloadToAdd); await view.waitForExpected(); await allView.waitForExpected(); // Now test the maxHistoryResults parameter. let allHistoryList2 = await DownloadHistory.getList({ type: Downloads.ALL, maxHistoryResults: 3, }); // Prepare the set of downloads to contain fewer history downloads by removing // the oldest ones. let allView2 = new TestView(allView.expected.slice(3)); await allHistoryList2.addView(allView2); await allView2.waitForExpected(); // Create a dummy list and view like the previous limited one to just add and // remove its view to make sure it doesn't break other lists' updates. let dummyList = await DownloadHistory.getList({ type: Downloads.ALL, maxHistoryResults: 3, }); let dummyView = new TestView([]); await dummyList.addView(dummyView); await dummyList.removeView(dummyView); // Clear history and check that session downloads with partial data remain. // Private downloads are also not cleared when clearing history. view.expected = view.expected.filter(d => d.hasPartialData); allView.expected = allView.expected.filter( d => d.hasPartialData || d.isPrivate ); await PlacesUtils.history.clear(); await view.waitForExpected(); await allView.waitForExpected(); // Check that the dummy view above did not prevent the limited from updating. allView2.expected = allView.expected; await allView2.waitForExpected(); });