diff options
Diffstat (limited to 'browser/base/content/test/sanitize/browser_sanitizeDialog.js')
-rw-r--r-- | browser/base/content/test/sanitize/browser_sanitizeDialog.js | 833 |
1 files changed, 833 insertions, 0 deletions
diff --git a/browser/base/content/test/sanitize/browser_sanitizeDialog.js b/browser/base/content/test/sanitize/browser_sanitizeDialog.js new file mode 100644 index 0000000000..aa94d50764 --- /dev/null +++ b/browser/base/content/test/sanitize/browser_sanitizeDialog.js @@ -0,0 +1,833 @@ +/* -*- 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", +}); + +const kMsecPerMin = 60 * 1000; +const kUsecPerMin = 60 * 1000000; + +/** + * 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` + ); + } +} + +add_setup(async function() { + requestLongerTimeout(3); + await blankSlate(); + registerCleanupFunction(async function() { + await blankSlate(); + await PlacesTestUtils.promiseAsyncUpdates(); + }); +}); + +/** + * 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("history", 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 combined history-downloads checkbox clears both history + * visits and downloads when checked; the dialog respects simple timespan. + */ +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("history", true); + this.acceptDialog(); + }; + dh.onunload = async function() { + intPrefIs( + "sanitize.timeSpan", + Sanitizer.TIMESPAN_HOUR, + "timeSpan pref should be hour after accepting dialog with " + + "hour selected" + ); + boolPrefIs( + "cpd.history", + true, + "history pref should be true after accepting dialog with " + + "history checkbox checked" + ); + boolPrefIs( + "cpd.downloads", + true, + "downloads pref should be true after accepting dialog with " + + "history checkbox checked" + ); + + 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; +}); + +/** + * Ensures that the combined history-downloads checkbox removes neither + * history visits nor downloads when not checked. + */ +add_task(async function test_history_downloads_unchecked() { + // Add form entries + let formEntries = []; + + for (let i = 0; i < 5; i++) { + formEntries.push(await promiseAddFormEntryWithMinutesAgo(i)); + } + + // Add downloads (within the past hour). + let downloadIDs = []; + for (let i = 0; i < 5; i++) { + await addDownloadWithMinutesAgo(downloadIDs, i); + } + + // Add history, downloads, form entries (within the past hour). + let uris = []; + let places = []; + let pURI; + for (let i = 0; i < 5; 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() { + is( + this.isWarningPanelVisible(), + false, + "Warning panel should be hidden after previously accepting dialog " + + "with a predefined timespan" + ); + this.selectDuration(Sanitizer.TIMESPAN_HOUR); + + // Remove only form entries, leave history (including downloads). + this.checkPrefCheckbox("history", false); + this.checkPrefCheckbox("formdata", true); + this.acceptDialog(); + }; + dh.onunload = async function() { + intPrefIs( + "sanitize.timeSpan", + Sanitizer.TIMESPAN_HOUR, + "timeSpan pref should be hour after accepting dialog with " + + "hour selected" + ); + boolPrefIs( + "cpd.history", + false, + "history pref should be false after accepting dialog with " + + "history checkbox unchecked" + ); + boolPrefIs( + "cpd.downloads", + false, + "downloads pref should be false after accepting dialog with " + + "history checkbox unchecked" + ); + + // Of the three only form entries should be cleared. + await promiseHistoryClearedState(uris, false); + await ensureDownloadsClearedState(downloadIDs, false); + + for (let entry of formEntries) { + let exists = await formNameExists(entry); + ok(!exists, "form entry " + entry + " should no longer exist"); + } + + // OK, done, cleanup after ourselves. + await blankSlate(); + await promiseHistoryClearedState(uris, true); + await ensureDownloadsClearedState(downloadIDs, 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("history", 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("history", 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; +}); + +/** + * 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() { + // Check that the relevant checkboxes are enabled + var cb = this.win.document.querySelectorAll( + "checkbox[preference='privacy.cpd.formdata']" + ); + ok( + cb.length == 1 && !cb[0].disabled, + "There is formdata, checkbox to clear formdata should be enabled." + ); + + cb = this.win.document.querySelectorAll( + "checkbox[preference='privacy.cpd.history']" + ); + 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() { + boolPrefIs( + "cpd.history", + true, + "history pref should be true after accepting dialog with " + + "history checkbox checked" + ); + boolPrefIs( + "cpd.formdata", + true, + "formdata pref should be true after accepting dialog with " + + "formdata checkbox checked" + ); + + var cb = this.win.document.querySelectorAll( + "checkbox[preference='privacy.cpd.history']" + ); + 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() { + boolPrefIs( + "cpd.formdata", + true, + "formdata pref should persist previous value after accepting " + + "dialog where you could not clear formdata." + ); + + var cb = this.win.document.querySelectorAll( + "checkbox[preference='privacy.cpd.formdata']" + ); + + info( + "There exists formEntries so the checkbox should be in sync with the pref." + ); + is(cb.length, 1, "There is only one checkbox for 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; +}); + +// Test for offline apps permission deletion +add_task(async function test_offline_apps_permissions() { + // Prepare stuff, we will work with www.example.com + var URL = "https://www.example.com"; + var URI = makeURI(URL); + var principal = Services.scriptSecurityManager.createContentPrincipal( + URI, + {} + ); + + let promiseSanitized = promiseSanitizationComplete(); + + // Open the dialog + let dh = new DialogHelper(); + dh.onload = function() { + this.selectDuration(Sanitizer.TIMESPAN_EVERYTHING); + // Clear only offlineApps + this.uncheckAllCheckboxes(); + this.checkPrefCheckbox("siteSettings", true); + this.acceptDialog(); + }; + dh.onunload = async function() { + await promiseSanitized; + + // Check all has been deleted (privileges, data, cache) + is( + Services.perms.testPermissionFromPrincipal(principal, "offline-app"), + 0, + "offline-app permissions removed" + ); + }; + dh.open(); + await dh.promiseClosed; +}); + +var now_mSec = Date.now(); +var now_uSec = now_mSec * 1000; + +/** + * 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. + */ +function DialogHelper(browserWin = window) { + this._browserWin = browserWin; + this.win = null; + 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 pref = "privacy.cpd." + aPrefName; + var cb = this.win.document.querySelectorAll( + "checkbox[preference='" + pref + "']" + ); + is(cb.length, 1, "found checkbox for " + pref + " preference"); + if (cb[0].checked != aCheckState) { + cb[0].click(); + } + }, + + /** + * Makes sure all the checkboxes are checked. + */ + _checkAllCheckboxesCustom(check) { + var cb = this.win.document.querySelectorAll("checkbox[preference]"); + ok(cb.length > 1, "found checkboxes for preferences"); + for (var i = 0; i < cb.length; ++i) { + var pref = this.win.Preferences.get(cb[i].getAttribute("preference")); + if (!!pref.value ^ 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.xhtml", + { + isSubDialog: true, + } + ); + + executeSoon(() => { + Sanitizer.showUI(this._browserWin); + }); + + this.win = await dialogPromise; + this.win.addEventListener( + "load", + () => { + // Run onload on next tick so that gSanitizePromptDialog.init can run first. + executeSoon(() => 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(); + } + 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" + ); + } + }, +}; + +function promiseSanitizationComplete() { + return TestUtils.topicObserved("sanitizer-sanitization-complete"); +} + +/** + * 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(now_mSec - 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 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 = now_uSec - aMinutesAgo * kUsecPerMin; + + return FormHistory.update({ + op: "add", + fieldname: name, + value: "dummy", + firstUsed: timestamp, + }); +} + +/** + * Checks if a form entry exists. + */ +async function formNameExists(name) { + return !!(await FormHistory.count({ fieldname: name })); +} + +/** + * 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(); +} + +/** + * 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 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" + ); + } +} + +/** + * 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 now_uSec - aMinutesAgo * kUsecPerMin; +} |