From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- .../test/sanitize/browser_sanitizeDialog_v2.js | 1429 ++++++++++++++++++++ 1 file changed, 1429 insertions(+) create mode 100644 browser/base/content/test/sanitize/browser_sanitizeDialog_v2.js (limited to 'browser/base/content/test/sanitize/browser_sanitizeDialog_v2.js') diff --git a/browser/base/content/test/sanitize/browser_sanitizeDialog_v2.js b/browser/base/content/test/sanitize/browser_sanitizeDialog_v2.js new file mode 100644 index 0000000000..29f760f57f --- /dev/null +++ b/browser/base/content/test/sanitize/browser_sanitizeDialog_v2.js @@ -0,0 +1,1429 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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/. */ + +/** + * Tests the sanitize dialog (a.k.a. the clear recent history dialog). + * See bug 480169. + * + * The purpose of this test is not to fully flex the sanitize timespan code; + * browser/base/content/test/sanitize/browser_sanitize-timespans.js does that. This + * test checks the UI of the dialog and makes sure it's correctly connected to + * the sanitize timespan code. + * + * Some of this code, especially the history creation parts, was taken from + * browser/base/content/test/sanitize/browser_sanitize-timespans.js. + */ +ChromeUtils.defineESModuleGetters(this, { + PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs", + Timer: "resource://gre/modules/Timer.sys.mjs", + PermissionTestUtils: "resource://testing-common/PermissionTestUtils.sys.mjs", + FileTestUtils: "resource://testing-common/FileTestUtils.sys.mjs", + Downloads: "resource://gre/modules/Downloads.sys.mjs", +}); + +const kMsecPerMin = 60 * 1000; +const kUsecPerMin = 60 * 1000000; +let today = Date.now() - new Date().setHours(0, 0, 0, 0); +let nowMSec = Date.now(); +let nowUSec = nowMSec * 1000; +let fileURL; + +const TEST_TARGET_FILE_NAME = "test-download.txt"; +const TEST_QUOTA_USAGE_HOST = "example.com"; +const TEST_QUOTA_USAGE_ORIGIN = "https://" + TEST_QUOTA_USAGE_HOST; +const TEST_QUOTA_USAGE_URL = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + TEST_QUOTA_USAGE_ORIGIN + ) + "site_data_test.html"; + +const siteOrigins = [ + "https://www.example.com", + "https://example.org", + "http://localhost:8000", + "http://localhost:3000", +]; + +/** + * Ensures that the specified URIs are either cleared or not. + * + * @param aURIs + * Array of page URIs + * @param aShouldBeCleared + * True if each visit to the URI should be cleared, false otherwise + */ +async function promiseHistoryClearedState(aURIs, aShouldBeCleared) { + for (let uri of aURIs) { + let visited = await PlacesUtils.history.hasVisits(uri); + Assert.equal( + visited, + !aShouldBeCleared, + `history visit ${uri.spec} should ${ + aShouldBeCleared ? "no longer" : "still" + } exist` + ); + } +} + +/** + * Ensures that the given pref is the expected value. + * + * @param {String} aPrefName + * The pref's sub-branch under the privacy branch + * @param {Boolean} aExpectedVal + * The pref's expected value + * @param {String} aMsg + * Passed to is() + */ +function boolPrefIs(aPrefName, aExpectedVal, aMsg) { + is(Services.prefs.getBoolPref("privacy." + aPrefName), aExpectedVal, aMsg); +} + +/** + * Checks to see if the download with the specified path exists. + * + * @param aPath + * The path of the download to check + * @return True if the download exists, false otherwise + */ +async function downloadExists(aPath) { + let publicList = await Downloads.getList(Downloads.PUBLIC); + let listArray = await publicList.getAll(); + return listArray.some(i => i.target.path == aPath); +} + +/** + * Ensures that the specified downloads are either cleared or not. + * + * @param aDownloadIDs + * Array of download database IDs + * @param aShouldBeCleared + * True if each download should be cleared, false otherwise + */ +async function ensureDownloadsClearedState(aDownloadIDs, aShouldBeCleared) { + let niceStr = aShouldBeCleared ? "no longer" : "still"; + for (let id of aDownloadIDs) { + is( + await downloadExists(id), + !aShouldBeCleared, + "download " + id + " should " + niceStr + " exist" + ); + } +} + +/** + * Checks if a form entry exists. + */ +async function formNameExists(name) { + return !!(await FormHistory.count({ fieldname: name })); +} + +/** + * Adds a form entry to history. + * + * @param aMinutesAgo + * The entry will be added this many minutes ago + */ +function promiseAddFormEntryWithMinutesAgo(aMinutesAgo) { + let name = aMinutesAgo + "-minutes-ago"; + + // Artifically age the entry to the proper vintage. + let timestamp = nowUSec - aMinutesAgo * kUsecPerMin; + + return FormHistory.update({ + op: "add", + fieldname: name, + value: "dummy", + firstUsed: timestamp, + }); +} + +/** + * Adds a download to history. + * + * @param aMinutesAgo + * The download will be downloaded this many minutes ago + */ +async function addDownloadWithMinutesAgo(aExpectedPathList, aMinutesAgo) { + let publicList = await Downloads.getList(Downloads.PUBLIC); + + let name = "fakefile-" + aMinutesAgo + "-minutes-ago"; + let download = await Downloads.createDownload({ + source: "https://bugzilla.mozilla.org/show_bug.cgi?id=480169", + target: name, + }); + download.startTime = new Date(nowMSec - aMinutesAgo * kMsecPerMin); + download.canceled = true; + publicList.add(download); + + ok( + await downloadExists(name), + "Sanity check: download " + name + " should exist after creating it" + ); + + aExpectedPathList.push(name); +} + +/** + * Adds multiple downloads to the PUBLIC download list + */ +async function addToDownloadList() { + const url = createFileURL(); + const downloadsList = await Downloads.getList(Downloads.PUBLIC); + let timeOptions = [1, 2, 4, 24, 128, 128]; + let buffer = 100000; + + for (let i = 0; i < timeOptions.length; i++) { + let timeDownloaded = 60 * kMsecPerMin * timeOptions[i]; + if (timeOptions[i] === 24) { + timeDownloaded = today; + } + + let download = await Downloads.createDownload({ + source: { url: url.spec, isPrivate: false }, + target: { path: FileTestUtils.getTempFile(TEST_TARGET_FILE_NAME).path }, + startTime: { + getTime: _ => { + return nowMSec - timeDownloaded + buffer; + }, + }, + }); + + Assert.ok(!!download); + downloadsList.add(download); + } + let items = await downloadsList.getAll(); + Assert.equal(items.length, 6, "Items were added to the list"); +} + +async function addToSiteUsage() { + // Fill indexedDB with test data. + // Don't wait for the page to load, to register the content event handler as quickly as possible. + // If this test goes intermittent, we might have to tell the page to wait longer before + // firing the event. + BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_QUOTA_USAGE_URL, false); + await BrowserTestUtils.waitForContentEvent( + gBrowser.selectedBrowser, + "test-indexedDB-done", + false, + null, + true + ); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + + let siteLastAccessed = [1, 2, 4, 24]; + + let staticUsage = 4096 * 6; + // Add a time buffer so the site access falls within the time range + const buffer = 10000; + + // Change lastAccessed of sites + for (let index = 0; index < siteLastAccessed.length; index++) { + let lastAccessedTime = 60 * kMsecPerMin * siteLastAccessed[index]; + if (siteLastAccessed[index] === 24) { + lastAccessedTime = today; + } + + let site = SiteDataManager._testInsertSite(siteOrigins[index], { + quotaUsage: staticUsage, + lastAccessed: (nowMSec - lastAccessedTime + buffer) * 1000, + }); + Assert.ok(site, "Site added successfully"); + } +} + +/** + * Helper function to create file URL to open + * + * @returns {Object} a file URL + */ +function createFileURL() { + if (!fileURL) { + let file = Services.dirsvc.get("TmpD", Ci.nsIFile); + file.append("foo.txt"); + file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600); + + fileURL = Services.io.newFileURI(file); + } + return fileURL; +} + +add_setup(async function () { + requestLongerTimeout(3); + await blankSlate(); + registerCleanupFunction(async function () { + await blankSlate(); + await PlacesTestUtils.promiseAsyncUpdates(); + }); + await SpecialPowers.pushPrefEnv({ + set: [["privacy.sanitize.useOldClearHistoryDialog", false]], + }); + + // open preferences to trigger an updateSites() + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +/** + * Removes all history visits, downloads, and form entries. + */ +async function blankSlate() { + let publicList = await Downloads.getList(Downloads.PUBLIC); + let downloads = await publicList.getAll(); + for (let download of downloads) { + await publicList.remove(download); + await download.finalize(true); + } + + await FormHistory.update({ op: "remove" }); + await PlacesUtils.history.clear(); +} + +/** + * This wraps the dialog and provides some convenience methods for interacting + * with it. + * + * @param browserWin (optional) + * The browser window that the dialog is expected to open in. If not + * supplied, the initial browser window of the test run is used. + * @param mode (optional) + * One of + * clear on shutdown settings context ("clearOnShutdown"), + * clear site data settings context ("clearSiteData"), + * clear history context ("clearHistory"), + * browser context ("browser") + * "browser" by default + */ +function DialogHelper(openContext = "browser") { + this._browserWin = window; + this.win = null; + this._mode = openContext; + this.promiseClosed = new Promise(resolve => { + this._resolveClosed = resolve; + }); +} + +DialogHelper.prototype = { + /** + * "Presses" the dialog's OK button. + */ + acceptDialog() { + let dialogEl = this.win.document.querySelector("dialog"); + is( + dialogEl.getButton("accept").disabled, + false, + "Dialog's OK button should not be disabled" + ); + dialogEl.acceptDialog(); + }, + + /** + * "Presses" the dialog's Cancel button. + */ + cancelDialog() { + this.win.document.querySelector("dialog").cancelDialog(); + }, + + /** + * (Un)checks a history scope checkbox (browser & download history, + * form history, etc.). + * + * @param aPrefName + * The final portion of the checkbox's privacy.cpd.* preference name + * @param aCheckState + * True if the checkbox should be checked, false otherwise + */ + checkPrefCheckbox(aPrefName, aCheckState) { + var cb = this.win.document.querySelectorAll( + "checkbox[id='" + aPrefName + "']" + ); + is(cb.length, 1, "found checkbox for " + aPrefName + " id"); + if (cb[0].checked != aCheckState) { + cb[0].click(); + } + }, + + /** + * @param {String} aCheckboxId + * The checkbox id name + * @param {Boolean} aCheckState + * True if the checkbox should be checked, false otherwise + */ + validateCheckbox(aCheckboxId, aCheckState) { + let cb = this.win.document.querySelectorAll( + "checkbox[id='" + aCheckboxId + "']" + ); + is(cb.length, 1, `found checkbox for id=${aCheckboxId}`); + is( + cb[0].checked, + aCheckState, + `checkbox for ${aCheckboxId} is ${aCheckState}` + ); + }, + + /** + * Makes sure all the checkboxes are checked. + */ + _checkAllCheckboxesCustom(check) { + var cb = this.win.document.querySelectorAll(".clearingItemCheckbox"); + ok(cb.length, "found checkboxes for ids"); + for (var i = 0; i < cb.length; ++i) { + if (cb[i].checked != check) { + cb[i].click(); + } + } + }, + + checkAllCheckboxes() { + this._checkAllCheckboxesCustom(true); + }, + + uncheckAllCheckboxes() { + this._checkAllCheckboxesCustom(false); + }, + + /** + * @return The dialog's duration dropdown + */ + getDurationDropdown() { + return this.win.document.getElementById("sanitizeDurationChoice"); + }, + + /** + * @return The clear-everything warning box + */ + getWarningPanel() { + return this.win.document.getElementById("sanitizeEverythingWarningBox"); + }, + + /** + * @return True if the "Everything" warning panel is visible (as opposed to + * the tree) + */ + isWarningPanelVisible() { + return !this.getWarningPanel().hidden; + }, + + /** + * Opens the clear recent history dialog. Before calling this, set + * this.onload to a function to execute onload. It should close the dialog + * when done so that the tests may continue. Set this.onunload to a function + * to execute onunload. this.onunload is optional. If it returns true, the + * caller is expected to call promiseAsyncUpdates at some point; if false is + * returned, promiseAsyncUpdates is called automatically. + */ + async open() { + let dialogPromise = BrowserTestUtils.promiseAlertDialogOpen( + null, + "chrome://browser/content/sanitize_v2.xhtml", + { + isSubDialog: true, + } + ); + + // We want to simulate opening the dialog inside preferences for clear history + // and clear site data + if (this._mode != "browser") { + await openPreferencesViaOpenPreferencesAPI("privacy", { + leaveOpen: true, + }); + let tabWindow = gBrowser.selectedBrowser.contentWindow; + let clearDialogOpenButtonId = this._mode + "Button"; + // the id for clear on shutdown is of a different format + if (this._mode == "clearOnShutdown") { + // set always clear to true to enable the clear on shutdown dialog + let enableSettingsCheckbox = + tabWindow.document.getElementById("alwaysClear"); + if (!enableSettingsCheckbox.checked) { + enableSettingsCheckbox.click(); + } + clearDialogOpenButtonId = "clearDataSettings"; + } + // open dialog + tabWindow.document.getElementById(clearDialogOpenButtonId).click(); + } + // We open the dialog in the chrome context in other cases + else { + executeSoon(() => { + Sanitizer.showUI(this._browserWin, this._mode); + }); + } + + this.win = await dialogPromise; + this.win.addEventListener( + "load", + () => { + // Run onload on next tick so that gSanitizePromptDialog.init can run first. + executeSoon(async () => { + await this.win.gSanitizePromptDialog.dataSizesFinishedUpdatingPromise; + this.onload(); + }); + }, + { once: true } + ); + this.win.addEventListener( + "unload", + () => { + // Some exceptions that reach here don't reach the test harness, but + // ok()/is() do... + (async () => { + if (this.onunload) { + await this.onunload(); + } + if (this._mode != "browser") { + BrowserTestUtils.removeTab(gBrowser.selectedTab); + } + await PlacesTestUtils.promiseAsyncUpdates(); + this._resolveClosed(); + this.win = null; + })(); + }, + { once: true } + ); + }, + + /** + * Selects a duration in the duration dropdown. + * + * @param aDurVal + * One of the Sanitizer.TIMESPAN_* values + */ + selectDuration(aDurVal) { + this.getDurationDropdown().value = aDurVal; + if (aDurVal === Sanitizer.TIMESPAN_EVERYTHING) { + is( + this.isWarningPanelVisible(), + true, + "Warning panel should be visible for TIMESPAN_EVERYTHING" + ); + } else { + is( + this.isWarningPanelVisible(), + false, + "Warning panel should not be visible for non-TIMESPAN_EVERYTHING" + ); + } + }, +}; + +/** + * Ensures that the given pref is the expected value. + * + * @param aPrefName + * The pref's sub-branch under the privacy branch + * @param aExpectedVal + * The pref's expected value + * @param aMsg + * Passed to is() + */ +function intPrefIs(aPrefName, aExpectedVal, aMsg) { + is(Services.prefs.getIntPref("privacy." + aPrefName), aExpectedVal, aMsg); +} + +/** + * Creates a visit time. + * + * @param aMinutesAgo + * The visit will be visited this many minutes ago + */ +function visitTimeForMinutesAgo(aMinutesAgo) { + return nowUSec - aMinutesAgo * kUsecPerMin; +} + +function promiseSanitizationComplete() { + return TestUtils.topicObserved("sanitizer-sanitization-complete"); +} + +/** + * Helper function to validate the data sizes shown for each time selection + * + * @param {DialogHelper} dh - dialog object to access window and timespan + */ +async function validateDataSizes(dialogHelper) { + let timespans = [ + "TIMESPAN_HOUR", + "TIMESPAN_2HOURS", + "TIMESPAN_4HOURS", + "TIMESPAN_TODAY", + "TIMESPAN_EVERYTHING", + ]; + + // get current data sizes from siteDataManager + let cacheUsage = await SiteDataManager.getCacheSize(); + let quotaUsage = await SiteDataManager.getQuotaUsageForTimeRanges(timespans); + + for (let i = 0; i < timespans.length; i++) { + // select timespan to check + dialogHelper.selectDuration(Sanitizer[timespans[i]]); + + // get the elements + let clearCookiesAndSiteDataCheckbox = + dialogHelper.win.document.getElementById("cookiesAndStorage"); + let clearCacheCheckbox = dialogHelper.win.document.getElementById("cache"); + + let [convertedQuotaUsage] = DownloadUtils.convertByteUnits( + quotaUsage[timespans[i]] + ); + let [, convertedCacheUnit] = DownloadUtils.convertByteUnits(cacheUsage); + + // Ensure l10n is finished before inspecting the category labels. + await dialogHelper.win.document.l10n.translateElements([ + clearCookiesAndSiteDataCheckbox, + clearCacheCheckbox, + ]); + ok( + clearCacheCheckbox.label.includes(convertedCacheUnit), + "Should show the cache usage" + ); + ok( + clearCookiesAndSiteDataCheckbox.label.includes(convertedQuotaUsage), + `Should show the quota usage as ${convertedQuotaUsage}` + ); + } +} + +/** + * + * Opens dialog in the provided context and selects the checkboxes + * as sent in the parameters + * + * @param {Object} context the dialog is opened in, timespan to select, + * if historyFormDataAndDownloads, cookiesAndStorage, cache or siteSettings + * are checked + */ +async function performActionsOnDialog({ + context = "browser", + timespan = Sanitizer.TIMESPAN_HOUR, + historyFormDataAndDownloads = true, + cookiesAndStorage = true, + cache = false, + siteSettings = false, +}) { + let dh = new DialogHelper(context); + dh.onload = function () { + this.selectDuration(timespan); + this.checkPrefCheckbox( + "historyFormDataAndDownloads", + historyFormDataAndDownloads + ); + this.checkPrefCheckbox("cookiesAndStorage", cookiesAndStorage); + this.checkPrefCheckbox("cache", cache); + this.checkPrefCheckbox("siteSettings", siteSettings); + this.acceptDialog(); + }; + dh.open(); + await dh.promiseClosed; +} + +/** + * Initializes the dialog to its default state. + */ +add_task(async function default_state() { + let dh = new DialogHelper(); + dh.onload = function () { + // Select "Last Hour" + this.selectDuration(Sanitizer.TIMESPAN_HOUR); + this.acceptDialog(); + }; + dh.open(); + await dh.promiseClosed; +}); + +/** + * Cancels the dialog, makes sure history not cleared. + */ +add_task(async function test_cancel() { + // Add history (within the past hour) + let uris = []; + let places = []; + let pURI; + for (let i = 0; i < 30; i++) { + pURI = makeURI("https://" + i + "-minutes-ago.com/"); + places.push({ uri: pURI, visitDate: visitTimeForMinutesAgo(i) }); + uris.push(pURI); + } + await PlacesTestUtils.addVisits(places); + + let dh = new DialogHelper(); + dh.onload = function () { + this.selectDuration(Sanitizer.TIMESPAN_HOUR); + this.checkPrefCheckbox("historyFormDataAndDownloads", false); + this.cancelDialog(); + }; + dh.onunload = async function () { + await promiseHistoryClearedState(uris, false); + await blankSlate(); + await promiseHistoryClearedState(uris, true); + }; + dh.open(); + await dh.promiseClosed; +}); + +/** + * Ensures that the "Everything" duration option works. + */ +add_task(async function test_everything() { + // Add history. + let uris = []; + let places = []; + let pURI; + // within past hour, within past two hours, within past four hours and + // outside past four hours + [10, 70, 130, 250].forEach(function (aValue) { + pURI = makeURI("https://" + aValue + "-minutes-ago.com/"); + places.push({ uri: pURI, visitDate: visitTimeForMinutesAgo(aValue) }); + uris.push(pURI); + }); + + let promiseSanitized = promiseSanitizationComplete(); + + await PlacesTestUtils.addVisits(places); + let dh = new DialogHelper(); + dh.onload = function () { + is( + this.isWarningPanelVisible(), + false, + "Warning panel should be hidden after previously accepting dialog " + + "with a predefined timespan" + ); + this.selectDuration(Sanitizer.TIMESPAN_EVERYTHING); + this.checkPrefCheckbox("historyFormDataAndDownloads", true); + this.acceptDialog(); + }; + dh.onunload = async function () { + await promiseSanitized; + intPrefIs( + "sanitize.timeSpan", + Sanitizer.TIMESPAN_EVERYTHING, + "timeSpan pref should be everything after accepting dialog " + + "with everything selected" + ); + + await promiseHistoryClearedState(uris, true); + }; + dh.open(); + await dh.promiseClosed; +}); + +/** + * Ensures that the "Everything" warning is visible on dialog open after + * the previous test. + */ +add_task(async function test_everything_warning() { + // Add history. + let uris = []; + let places = []; + let pURI; + // within past hour, within past two hours, within past four hours and + // outside past four hours + [10, 70, 130, 250].forEach(function (aValue) { + pURI = makeURI("https://" + aValue + "-minutes-ago.com/"); + places.push({ uri: pURI, visitDate: visitTimeForMinutesAgo(aValue) }); + uris.push(pURI); + }); + + let promiseSanitized = promiseSanitizationComplete(); + + await PlacesTestUtils.addVisits(places); + let dh = new DialogHelper(); + dh.onload = function () { + is( + this.isWarningPanelVisible(), + true, + "Warning panel should be visible after previously accepting dialog " + + "with clearing everything" + ); + this.selectDuration(Sanitizer.TIMESPAN_EVERYTHING); + this.checkPrefCheckbox("historyFormDataAndDownloads", true); + this.acceptDialog(); + }; + dh.onunload = async function () { + intPrefIs( + "sanitize.timeSpan", + Sanitizer.TIMESPAN_EVERYTHING, + "timeSpan pref should be everything after accepting dialog " + + "with everything selected" + ); + + await promiseSanitized; + + await promiseHistoryClearedState(uris, true); + }; + dh.open(); + await dh.promiseClosed; +}); + +/** + * Tests that the clearing button gets disabled if no checkboxes are checked + * and enabled when at least one checkbox is checked + */ +add_task(async function testAcceptButtonDisabled() { + let dh = new DialogHelper(); + dh.onload = async function () { + let clearButton = this.win.document + .querySelector("dialog") + .getButton("accept"); + this.uncheckAllCheckboxes(); + await new Promise(resolve => SimpleTest.executeSoon(resolve)); + is(clearButton.disabled, true, "Clear button should be disabled"); + // await BrowserTestUtils.waitForMutationCondition( + // clearButton, + // { attributes: true }, + // () => clearButton.disabled, + // "Clear button should be disabled" + // ); + + this.checkPrefCheckbox("cache", true); + await new Promise(resolve => SimpleTest.executeSoon(resolve)); + is(clearButton.disabled, false, "Clear button should not be disabled"); + + this.cancelDialog(); + }; + dh.open(); + await dh.promiseClosed; +}); + +/** + * Tests to see if the warning box is hidden when opened in the clear on shutdown context + */ +add_task(async function testWarningBoxInClearOnShutdown() { + let dh = new DialogHelper("clearSiteData"); + dh.onload = function () { + this.selectDuration(Sanitizer.TIMESPAN_EVERYTHING); + is( + BrowserTestUtils.isVisible(this.getWarningPanel()), + true, + `warning panel should be visible` + ); + this.acceptDialog(); + }; + dh.open(); + await dh.promiseClosed; + + dh = new DialogHelper("clearOnShutdown"); + dh.onload = function () { + is( + BrowserTestUtils.isVisible(this.getWarningPanel()), + false, + `warning panel should not be visible` + ); + + this.cancelDialog(); + }; + dh.open(); + await dh.promiseClosed; +}); + +/** + * Checks if clearing history and downloads for the simple timespan + * behaves as expected + */ +add_task(async function test_history_downloads_checked() { + // Add downloads (within the past hour). + let downloadIDs = []; + for (let i = 0; i < 5; i++) { + await addDownloadWithMinutesAgo(downloadIDs, i); + } + // Add downloads (over an hour ago). + let olderDownloadIDs = []; + for (let i = 0; i < 5; i++) { + await addDownloadWithMinutesAgo(olderDownloadIDs, 61 + i); + } + + // Add history (within the past hour). + let uris = []; + let places = []; + let pURI; + for (let i = 0; i < 30; i++) { + pURI = makeURI("https://" + i + "-minutes-ago.com/"); + places.push({ uri: pURI, visitDate: visitTimeForMinutesAgo(i) }); + uris.push(pURI); + } + // Add history (over an hour ago). + let olderURIs = []; + for (let i = 0; i < 5; i++) { + pURI = makeURI("https://" + (61 + i) + "-minutes-ago.com/"); + places.push({ uri: pURI, visitDate: visitTimeForMinutesAgo(61 + i) }); + olderURIs.push(pURI); + } + let promiseSanitized = promiseSanitizationComplete(); + + await PlacesTestUtils.addVisits(places); + + let dh = new DialogHelper(); + dh.onload = function () { + this.selectDuration(Sanitizer.TIMESPAN_HOUR); + this.checkPrefCheckbox("historyFormDataAndDownloads", true); + this.acceptDialog(); + }; + dh.onunload = async function () { + intPrefIs( + "sanitize.timeSpan", + Sanitizer.TIMESPAN_HOUR, + "timeSpan pref should be hour after accepting dialog with " + + "hour selected" + ); + + await promiseSanitized; + + // History visits and downloads within one hour should be cleared. + await promiseHistoryClearedState(uris, true); + await ensureDownloadsClearedState(downloadIDs, true); + + // Visits and downloads > 1 hour should still exist. + await promiseHistoryClearedState(olderURIs, false); + await ensureDownloadsClearedState(olderDownloadIDs, false); + + // OK, done, cleanup after ourselves. + await blankSlate(); + await promiseHistoryClearedState(olderURIs, true); + await ensureDownloadsClearedState(olderDownloadIDs, true); + }; + dh.open(); + await dh.promiseClosed; +}); + +/** + * The next three tests checks that when a certain history item cannot be + * cleared then the checkbox should be both disabled and unchecked. + * In addition, we ensure that this behavior does not modify the preferences. + */ +add_task(async function test_cannot_clear_history() { + // Add form entries + let formEntries = [await promiseAddFormEntryWithMinutesAgo(10)]; + + let promiseSanitized = promiseSanitizationComplete(); + + // Add history. + let pURI = makeURI("https://" + 10 + "-minutes-ago.com/"); + await PlacesTestUtils.addVisits({ + uri: pURI, + visitDate: visitTimeForMinutesAgo(10), + }); + let uris = [pURI]; + + let dh = new DialogHelper(); + dh.onload = function () { + var cb = this.win.document.querySelectorAll( + "checkbox[id='historyFormDataAndDownloads']" + ); + ok( + cb.length == 1 && !cb[0].disabled, + "There is history, checkbox to clear history should be enabled." + ); + + this.checkAllCheckboxes(); + this.acceptDialog(); + }; + dh.onunload = async function () { + await promiseSanitized; + + await promiseHistoryClearedState(uris, true); + + let exists = await formNameExists(formEntries[0]); + ok(!exists, "form entry " + formEntries[0] + " should no longer exist"); + }; + dh.open(); + await dh.promiseClosed; +}); + +add_task(async function test_no_formdata_history_to_clear() { + let promiseSanitized = promiseSanitizationComplete(); + let dh = new DialogHelper(); + dh.onload = function () { + var cb = this.win.document.querySelectorAll( + "checkbox[id='historyFormDataAndDownloads']" + ); + ok( + cb.length == 1 && !cb[0].disabled && cb[0].checked, + "There is no history, but history checkbox should always be enabled " + + "and will be checked from previous preference." + ); + + this.acceptDialog(); + }; + dh.open(); + await dh.promiseClosed; + await promiseSanitized; +}); + +add_task(async function test_form_entries() { + let formEntry = await promiseAddFormEntryWithMinutesAgo(10); + + let promiseSanitized = promiseSanitizationComplete(); + + let dh = new DialogHelper(); + dh.onload = function () { + var cb = this.win.document.querySelectorAll( + "checkbox[id='historyFormDataAndDownloads']" + ); + is(cb.length, 1, "There is only one checkbox for history and form data"); + ok(!cb[0].disabled, "The checkbox is enabled"); + ok(cb[0].checked, "The checkbox is checked"); + + this.acceptDialog(); + }; + dh.onunload = async function () { + await promiseSanitized; + let exists = await formNameExists(formEntry); + ok(!exists, "form entry " + formEntry + " should no longer exist"); + }; + dh.open(); + await dh.promiseClosed; +}); + +add_task(async function test_cookie_sizes() { + await clearAndValidateDataSizes({ + clearCookies: true, + clearCache: false, + clearDownloads: false, + timespan: Sanitizer.TIMESPAN_HOUR, + }); + await clearAndValidateDataSizes({ + clearCookies: true, + clearCache: false, + clearDownloads: false, + timespan: Sanitizer.TIMESPAN_4HOURS, + }); + await clearAndValidateDataSizes({ + clearCookies: true, + clearCache: false, + clearDownloads: false, + timespan: Sanitizer.TIMESPAN_EVERYTHING, + }); +}); + +add_task(async function test_cache_sizes() { + await clearAndValidateDataSizes({ + clearCookies: false, + clearCache: true, + clearDownloads: false, + timespan: Sanitizer.TIMESPAN_HOUR, + }); + await clearAndValidateDataSizes({ + clearCookies: false, + clearCache: true, + clearDownloads: false, + timespan: Sanitizer.TIMESPAN_4HOURS, + }); + await clearAndValidateDataSizes({ + clearCookies: false, + clearCache: true, + clearDownloads: false, + timespan: Sanitizer.TIMESPAN_EVERYTHING, + }); +}); + +add_task(async function test_downloads_sizes() { + await clearAndValidateDataSizes({ + clearCookies: false, + clearCache: false, + clearDownloads: true, + timespan: Sanitizer.TIMESPAN_HOUR, + }); + await clearAndValidateDataSizes({ + clearCookies: false, + clearCache: false, + clearDownloads: true, + timespan: Sanitizer.TIMESPAN_4HOURS, + }); + await clearAndValidateDataSizes({ + clearCookies: false, + clearCache: false, + clearDownloads: true, + timespan: Sanitizer.TIMESPAN_EVERYTHING, + }); +}); + +add_task(async function test_all_data_sizes() { + await clearAndValidateDataSizes({ + clearCookies: true, + clearCache: true, + clearDownloads: true, + timespan: Sanitizer.TIMESPAN_HOUR, + }); + await clearAndValidateDataSizes({ + clearCookies: true, + clearCache: true, + clearDownloads: true, + timespan: Sanitizer.TIMESPAN_4HOURS, + }); + await clearAndValidateDataSizes({ + clearCookies: true, + clearCache: true, + clearDownloads: true, + timespan: Sanitizer.TIMESPAN_EVERYTHING, + }); +}); + +// test the case when we open the dialog through the clear on shutdown settings +add_task(async function test_clear_on_shutdown() { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.sanitize.sanitizeOnShutdown", true]], + }); + + let dh = new DialogHelper("clearOnShutdown"); + dh.onload = async function () { + this.uncheckAllCheckboxes(); + this.checkPrefCheckbox("historyFormDataAndDownloads", false); + this.checkPrefCheckbox("cookiesAndStorage", true); + this.acceptDialog(); + }; + dh.open(); + await dh.promiseClosed; + + // Add downloads (within the past hour). + let downloadIDs = []; + for (let i = 0; i < 5; i++) { + await addDownloadWithMinutesAgo(downloadIDs, i); + } + // Add downloads (over an hour ago). + let olderDownloadIDs = []; + for (let i = 0; i < 5; i++) { + await addDownloadWithMinutesAgo(olderDownloadIDs, 61 + i); + } + + boolPrefIs( + "clearOnShutdown_v2.historyFormDataAndDownloads", + false, + "clearOnShutdown_v2 history should be false" + ); + + boolPrefIs( + "clearOnShutdown_v2.cookiesAndStorage", + true, + "clearOnShutdown_v2 cookies should be true" + ); + + boolPrefIs( + "clearOnShutdown_v2.cache", + false, + "clearOnShutdown_v2 cache should be false" + ); + + await createDummyDataForHost("example.org"); + await createDummyDataForHost("example.com"); + + ok( + await SiteDataTestUtils.hasIndexedDB("https://example.org"), + "We have indexedDB data for example.org" + ); + ok( + await SiteDataTestUtils.hasIndexedDB("https://example.com"), + "We have indexedDB data for example.com" + ); + + // Cleaning up + await Sanitizer.runSanitizeOnShutdown(); + + // Data for example.org should be cleared + ok( + !(await SiteDataTestUtils.hasIndexedDB("https://example.org")), + "We don't have indexedDB data for example.org" + ); + // Data for example.com should be cleared + ok( + !(await SiteDataTestUtils.hasIndexedDB("https://example.com")), + "We don't have indexedDB data for example.com" + ); + + // Downloads shouldn't have cleared + await ensureDownloadsClearedState(downloadIDs, false); + await ensureDownloadsClearedState(olderDownloadIDs, false); + + dh = new DialogHelper("clearOnShutdown"); + dh.onload = async function () { + this.uncheckAllCheckboxes(); + this.checkPrefCheckbox("historyFormDataAndDownloads", true); + this.acceptDialog(); + }; + dh.open(); + await dh.promiseClosed; + + boolPrefIs( + "clearOnShutdown_v2.historyFormDataAndDownloads", + true, + "clearOnShutdown_v2 history should be true" + ); + + boolPrefIs( + "clearOnShutdown_v2.cookiesAndStorage", + false, + "clearOnShutdown_v2 cookies should be false" + ); + + boolPrefIs( + "clearOnShutdown_v2.cache", + false, + "clearOnShutdown_v2 cache should be false" + ); + + ok( + await SiteDataTestUtils.hasIndexedDB("https://example.org"), + "We have indexedDB data for example.org" + ); + ok( + await SiteDataTestUtils.hasIndexedDB("https://example.com"), + "We have indexedDB data for example.com" + ); + + // Cleaning up + await Sanitizer.runSanitizeOnShutdown(); + + // Data for example.org should not be cleared + ok( + await SiteDataTestUtils.hasIndexedDB("https://example.org"), + "We have indexedDB data for example.org" + ); + // Data for example.com should not be cleared + ok( + await SiteDataTestUtils.hasIndexedDB("https://example.com"), + "We have indexedDB data for example.com" + ); + + // downloads should have cleared + await ensureDownloadsClearedState(downloadIDs, true); + await ensureDownloadsClearedState(olderDownloadIDs, true); + + // Clean up + await SiteDataTestUtils.clear(); +}); + +// test default prefs for entry points +add_task(async function test_defaults_prefs() { + let dh = new DialogHelper("clearSiteData"); + dh.onload = function () { + this.validateCheckbox("historyFormDataAndDownloads", false); + this.validateCheckbox("cache", true); + this.validateCheckbox("cookiesAndStorage", true); + this.validateCheckbox("siteSettings", false); + + this.cancelDialog(); + }; + dh.open(); + await dh.promiseClosed; + + // We don't need to specify the mode again, + // as the default mode is taken (browser, clear history) + + dh = new DialogHelper(); + dh.onload = function () { + // Default checked for browser and clear history mode + this.validateCheckbox("historyFormDataAndDownloads", true); + this.validateCheckbox("cache", true); + this.validateCheckbox("cookiesAndStorage", true); + this.validateCheckbox("siteSettings", false); + + this.cancelDialog(); + }; + dh.open(); + await dh.promiseClosed; +}); + +/** + * Helper function to simulate switching timespan selections and + * validate data sizes before and after clearing + * + * @param {Object} + * clearCookies - boolean + * clearDownloads - boolean + * clearCaches - boolean + * timespan - one of Sanitizer.TIMESPAN_* + */ +async function clearAndValidateDataSizes({ + clearCache, + clearDownloads, + clearCookies, + timespan, +}) { + await blankSlate(); + + await addToDownloadList(); + await addToSiteUsage(); + let promiseSanitized = promiseSanitizationComplete(); + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + + let dh = new DialogHelper(); + dh.onload = async function () { + await validateDataSizes(this); + this.checkPrefCheckbox("cache", clearCache); + this.checkPrefCheckbox("cookiesAndStorage", clearCookies); + this.checkPrefCheckbox("historyFormDataAndDownloads", clearDownloads); + this.selectDuration(timespan); + this.acceptDialog(); + }; + dh.onunload = async function () { + await promiseSanitized; + }; + dh.open(); + await dh.promiseClosed; + + let dh2 = new DialogHelper(); + // Check if the newly cleared values are reflected + dh2.onload = async function () { + await validateDataSizes(this); + this.acceptDialog(); + }; + dh2.open(); + await dh2.promiseClosed; + + await SiteDataTestUtils.clear(); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +} + +add_task(async function testEntryPointTelemetry() { + Services.fog.testResetFOG(); + + // Telemetry count we expect for each context + const EXPECTED_CONTEXT_COUNTS = { + browser: 3, + clearHistory: 2, + clearSiteData: 1, + }; + + for (let key in EXPECTED_CONTEXT_COUNTS) { + let count = 0; + + for (let i = 0; i < EXPECTED_CONTEXT_COUNTS[key]; i++) { + await performActionsOnDialog({ context: key }); + } + + let contextTelemetry = Glean.privacySanitize.dialogOpen.testGetValue(); + for (let object of contextTelemetry) { + if (object.extra.context == key) { + count += 1; + } + } + + is( + count, + EXPECTED_CONTEXT_COUNTS[key], + `There should be ${EXPECTED_CONTEXT_COUNTS[key]} opens from ${key} context` + ); + } +}); + +add_task(async function testTimespanTelemetry() { + Services.fog.testResetFOG(); + + // Expected timespan selections from telemetry + const EXPECTED_TIMESPANS = [ + Sanitizer.TIMESPAN_HOUR, + Sanitizer.TIMESPAN_2HOURS, + Sanitizer.TIMESPAN_4HOURS, + Sanitizer.TIMESPAN_EVERYTHING, + ]; + + for (let timespan of EXPECTED_TIMESPANS) { + await performActionsOnDialog({ timespan }); + } + + for (let index in EXPECTED_TIMESPANS) { + is( + Glean.privacySanitize.clearingTimeSpanSelected.testGetValue()[index].extra + .time_span, + EXPECTED_TIMESPANS[index].toString(), + `Selected timespan should be ${EXPECTED_TIMESPANS[index]}` + ); + } +}); + +add_task(async function testLoadtimeTelemetry() { + Services.fog.testResetFOG(); + + // loadtime metric is collected everytime that the dialog is opened + // expected number of times dialog will be opened for the test for each context + let EXPECTED_CONTEXT_COUNTS = { + browser: 2, + clearHistory: 3, + clearSiteData: 2, + }; + + // open dialog based on expected_context_counts + for (let context in EXPECTED_CONTEXT_COUNTS) { + for (let i = 0; i < EXPECTED_CONTEXT_COUNTS[context]; i++) { + await performActionsOnDialog({ context }); + } + } + + let loadTimeDistribution = Glean.privacySanitize.loadTime.testGetValue(); + + let expectedNumberOfCounts = Object.entries(EXPECTED_CONTEXT_COUNTS).reduce( + (acc, [key, value]) => acc + value, + 0 + ); + // No guarantees from timers means no guarantees on buckets. + // But we can guarantee it's only two samples. + is( + Object.entries(loadTimeDistribution.values).reduce( + (acc, [bucket, count]) => acc + count, + 0 + ), + expectedNumberOfCounts, + `Only ${expectedNumberOfCounts} buckets with samples` + ); +}); + +add_task(async function testClearingOptionsTelemetry() { + Services.fog.testResetFOG(); + + let expectedObject = { + context: "clearSiteData", + history_form_data_downloads: "true", + cookies_and_storage: "false", + cache: "true", + site_settings: "true", + }; + + await performActionsOnDialog({ + context: "clearSiteData", + historyFormDataAndDownloads: true, + cookiesAndStorage: false, + cache: true, + siteSettings: true, + }); + + let telemetryObject = Glean.privacySanitize.clear.testGetValue(); + Assert.equal( + telemetryObject.length, + 1, + "There should be only 1 telemetry object recorded" + ); + + Assert.deepEqual( + expectedObject, + telemetryObject[0].extra, + `Expected ${telemetryObject} to be the same as ${expectedObject}` + ); +}); + +add_task(async function testCheckboxStatesAfterMigration() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.clearOnShutdown.history", false], + ["privacy.clearOnShutdown.formdata", true], + ["privacy.clearOnShutdown.cookies", true], + ["privacy.clearOnShutdown.offlineApps", false], + ["privacy.clearOnShutdown.sessions", false], + ["privacy.clearOnShutdown.siteSettings", false], + ["privacy.clearOnShutdown.cache", true], + ["privacy.clearOnShutdown_v2.cookiesAndStorage", false], + ["privacy.sanitize.sanitizeOnShutdown.hasMigratedToNewPrefs", false], + ], + }); + + let dh = new DialogHelper("clearOnShutdown"); + dh.onload = function () { + this.validateCheckbox("cookiesAndStorage", true); + this.validateCheckbox("historyFormDataAndDownloads", false); + this.validateCheckbox("cache", true); + this.validateCheckbox("siteSettings", false); + this.cancelDialog(); + }; + dh.open(); + await dh.promiseClosed; +}); -- cgit v1.2.3