/* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; const { SessionStoreTestUtils } = ChromeUtils.importESModule( "resource://testing-common/SessionStoreTestUtils.sys.mjs" ); const PREF_ID_ALWAYS_ASK = "browser.privatebrowsing.resetPBM.showConfirmationDialog"; const SELECTOR_TOOLBAR_BUTTON = "#reset-pbm-toolbar-button"; const SELECTOR_PANELVIEW = "panel #reset-pbm-panel"; const SELECTOR_CONTAINER = "#reset-pbm-panel-container"; const SELECTOR_PANEL_HEADING = "#reset-pbm-panel-header > description"; const SELECTOR_PANEL_DESCRIPTION = "#reset-pbm-panel-description"; const SELECTOR_PANEL_CHECKBOX = "#reset-pbm-panel-checkbox"; const SELECTOR_PANEL_CONFIRM_BTN = "#reset-pbm-panel-confirm-button"; const SELECTOR_PANEL_CANCEL_BTN = "#reset-pbm-panel-cancel-button"; const SELECTOR_PANEL_COMPLETION_TOAST = "#confirmation-hint"; /** * Wait for the reset pbm confirmation panel to open. May also be called if the * panel is already open. * @param {ChromeWindow} win - Chrome window in which the panel is embedded. * @returns {Promise} - Promise which resolves once the panel has been shown. * Resolves directly if the panel is already visible. */ async function waitForConfirmPanelShow(win) { // Check for the panel, if it's not present yet wait for it to be inserted. let panelview = win.document.querySelector(SELECTOR_PANELVIEW); if (!panelview) { let navToolbox = win.document.getElementById("navigator-toolbox"); await BrowserTestUtils.waitForMutationCondition( navToolbox, { childList: true, subtree: true }, () => { panelview = win.document.querySelector(SELECTOR_PANELVIEW); return !!panelview; } ); } // Panel already visible, we can exit early. if (BrowserTestUtils.isVisible(panelview)) { return; } // Wait for panel shown event. await BrowserTestUtils.waitForEvent(panelview.closest("panel"), "popupshown"); } /** * Hides the completion toast which is shown after the reset action has been * completed. * @param {ChromeWindow} win - Chrome window the toast is shown in. */ async function hideCompletionToast(win) { let promiseHidden = BrowserTestUtils.waitForEvent( win.ConfirmationHint._panel, "popuphidden" ); win.ConfirmationHint._panel.hidePopup(); await promiseHidden; } /** * Trigger the reset pbm toolbar button which may open the confirm panel in the * given window. * @param {nsIDOMWindow} win - PBM window to trigger the button in. * @param {boolean} [expectPanelOpen] - After the button action: whether the * panel is expected to open (true) or remain closed (false). * @returns {Promise} - Promise which resolves once the button has been * triggered and, if applicable, the panel has been opened. */ async function triggerResetBtn(win, expectPanelOpen = true) { Assert.ok( PrivateBrowsingUtils.isWindowPrivate(win), "Window to open panel is in PBM." ); let shownPromise; if (expectPanelOpen) { shownPromise = waitForConfirmPanelShow(win); } await BrowserTestUtils.synthesizeMouseAtCenter( SELECTOR_TOOLBAR_BUTTON, {}, win.browsingContext ); // Promise may not be defined at this point in which case this is a no-op. await shownPromise; } /** * Provides a promise that resolves once the reset confirmation panel has been hidden. * @param nsIDOMWindow win - Chrome window that has the panel. * @returns {Promise} */ function waitForConfirmPanelHidden(win) { return BrowserTestUtils.waitForEvent( win.document.querySelector(SELECTOR_PANELVIEW).closest("panel"), "popuphidden" ); } /** * Provides a promise that resolves once the completion toast has been shown. * @param nsIDOMWindow win - Chrome window that has the panel. * @returns {Promise} */ function waitForCompletionToastShown(win) { // Init the confirmation hint panel so we can listen for show events. win.ConfirmationHint._ensurePanel(); return BrowserTestUtils.waitForEvent( win.document.querySelector(SELECTOR_PANEL_COMPLETION_TOAST), "popupshown" ); } /** * Wait for private browsing data clearing to be triggered. * Clearing is not guaranteed to be done at this point. Bug 1846494 will add a * promise based mechanism and potentially a new triggering method for clearing, * at which point this helper should be updated. * @returns {Promise} Promise which resolves when the last-pb-context-exited * message has been dispatched. */ function waitForPBMDataClear() { return TestUtils.topicObserved("last-pb-context-exited"); } /** * Test panel visibility. * @param {nsIDOMWindow} win - Chrome window which is the parent of the panel. * @param {string} selector - Query selector for the panel. * @param {boolean} expectVisible - Whether the panel should be visible (true) or invisible or not present (false). */ function assertPanelVisibility(win, selector, expectVisible) { let panelview = win.document.querySelector(selector); if (expectVisible) { Assert.ok(panelview, `Panelview element ${selector} should exist.`); Assert.ok( BrowserTestUtils.isVisible(panelview), `Panelview ${selector} should be visible.` ); return; } Assert.ok( !panelview || !BrowserTestUtils.isVisible(panelview), `Panelview ${selector} should be invisible or non-existent.` ); } function transformGleanEvents(events) { if (!events) { return []; } return events.map(e => ({ ...e.extra })); } function assertTelemetry(expectedConfirmPanel, expectedResetAction, message) { info(message); let confirmPanelEvents = transformGleanEvents( Glean.privateBrowsingResetPbm.confirmPanel.testGetValue() ); Assert.deepEqual( confirmPanelEvents, expectedConfirmPanel, "confirmPanel events should match." ); let resetActionEvents = transformGleanEvents( Glean.privateBrowsingResetPbm.resetAction.testGetValue() ); Assert.deepEqual( resetActionEvents, expectedResetAction, "resetAction events should match." ); } /** * Tests that the reset button is only visible in private browsing windows and * when the feature is enabled. */ add_task(async function test_toolbar_button_visibility() { assertTelemetry([], [], "No glean events initially."); for (let isEnabled of [false, true]) { await SpecialPowers.pushPrefEnv({ set: [["browser.privatebrowsing.resetPBM.enabled", isEnabled]], }); info( "Test that the toolbar button is never visible in a normal browsing window." ); let toolbarButtonNormalBrowsing = document.querySelector( SELECTOR_TOOLBAR_BUTTON ); Assert.equal( !!toolbarButtonNormalBrowsing, isEnabled, "Normal browsing toolbar button element exists, depending on enabled pref state." ); if (toolbarButtonNormalBrowsing) { Assert.ok( !BrowserTestUtils.isVisible(toolbarButtonNormalBrowsing), "Toolbar button is not visible in normal browsing" ); } info( "Test that the toolbar button is visible in a private browsing window, depending on enabled pref state." ); let privateWindow = await BrowserTestUtils.openNewBrowserWindow({ private: true, }); let toolbarButtonPrivateBrowsing = privateWindow.document.querySelector( SELECTOR_TOOLBAR_BUTTON ); Assert.equal( !!toolbarButtonPrivateBrowsing, isEnabled, "Private browsing toolbar button element exists, depending on enabled pref state." ); if (toolbarButtonPrivateBrowsing) { Assert.equal( BrowserTestUtils.isVisible(toolbarButtonPrivateBrowsing), isEnabled, "Toolbar button is visible in private browsing if enabled." ); } await BrowserTestUtils.closeWindow(privateWindow); await SpecialPowers.popPrefEnv(); } assertTelemetry([], [], "No glean events after test."); }); /** * Tests the panel UI, the 'always ask' checkbox and the confirm and cancel * actions. */ add_task(async function test_panel() { assertTelemetry([], [], "No glean events initially."); await SpecialPowers.pushPrefEnv({ set: [["browser.privatebrowsing.resetPBM.enabled", true]], }); let privateWin = await BrowserTestUtils.openNewBrowserWindow({ private: true, }); // Panel does either not exist (because it's lazy and hasn't been opened yet), // or it is invisible. assertPanelVisibility(privateWin, SELECTOR_PANELVIEW, false); assertPanelVisibility(privateWin, SELECTOR_PANEL_COMPLETION_TOAST, false); info("Open the panel."); await triggerResetBtn(privateWin); assertTelemetry( [{ action: "show", reason: "toolbar-btn" }], [], "There should be a panel show event." ); info("Check that all expected elements are present and visible."); [ SELECTOR_PANEL_HEADING, SELECTOR_PANEL_DESCRIPTION, SELECTOR_PANEL_CHECKBOX, SELECTOR_PANEL_CONFIRM_BTN, SELECTOR_PANEL_CANCEL_BTN, ].forEach(elSelector => { let el = privateWin.document.querySelector(elSelector); Assert.ok(el, `Panel element ${elSelector} exists.`); if (el) { Assert.ok( BrowserTestUtils.isVisible(el), `Panel element ${elSelector} is visible.` ); } }); info("Inspect checkbox and pref state."); let checkbox = privateWin.document.querySelector(SELECTOR_PANEL_CHECKBOX); Assert.ok( checkbox.checked, "The 'always ask' checkbox should be checked initially." ); Assert.ok( Services.prefs.getBoolPref( "browser.privatebrowsing.resetPBM.showConfirmationDialog" ), "The always ask pref should be true." ); info("Accessibility checks"); let panel = privateWin.document.querySelector(SELECTOR_PANELVIEW); Assert.equal( panel.getAttribute("role"), "document", "Panel should have role document." ); let container = panel.querySelector(SELECTOR_CONTAINER); Assert.equal( container.getAttribute("role"), "alertdialog", "Panel container should have role alertdialog." ); Assert.equal( container.getAttribute("aria-labelledby"), "reset-pbm-panel-header", "aria-labelledby should point to heading." ); let heading = panel.querySelector(SELECTOR_PANEL_HEADING); Assert.equal( heading.getAttribute("role"), "heading", "Heading should have role heading." ); Assert.equal( heading.getAttribute("aria-level"), "2", "heading should have aria-level 2" ); info("Click the checkbox to uncheck it."); await BrowserTestUtils.synthesizeMouseAtCenter( SELECTOR_PANEL_CHECKBOX, {}, privateWin.browsingContext ); Assert.ok( !checkbox.checked, "The 'always ask' checkbox should no longer be checked." ); info( "The pref shouldn't update after clicking the checkbox. It only updates on panel confirm." ); Assert.ok( Services.prefs.getBoolPref( "browser.privatebrowsing.resetPBM.showConfirmationDialog" ), "The always ask pref should still be true." ); info("Close the panel via cancel."); let promisePanelHidden = waitForConfirmPanelHidden(privateWin); await BrowserTestUtils.synthesizeMouseAtCenter( SELECTOR_PANEL_CANCEL_BTN, {}, privateWin.browsingContext ); await promisePanelHidden; assertTelemetry( [ { action: "show", reason: "toolbar-btn" }, { action: "hide", reason: "cancel-btn" }, ], [], "There should be a panel show and a hide event." ); assertPanelVisibility(privateWin, SELECTOR_PANELVIEW, false); assertPanelVisibility(privateWin, SELECTOR_PANEL_COMPLETION_TOAST, false); info("Reopen the panel."); await triggerResetBtn(privateWin); assertTelemetry( [ { action: "show", reason: "toolbar-btn" }, { action: "hide", reason: "cancel-btn" }, { action: "show", reason: "toolbar-btn" }, ], [], "Should have added a show event." ); assertPanelVisibility(privateWin, SELECTOR_PANELVIEW, true); assertPanelVisibility(privateWin, SELECTOR_PANEL_COMPLETION_TOAST, false); Assert.ok( checkbox.checked, "The 'always ask' checkbox should be checked again." ); Assert.ok( Services.prefs.getBoolPref(PREF_ID_ALWAYS_ASK), "The always ask pref should be true." ); info("Test the checkbox on confirm."); await BrowserTestUtils.synthesizeMouseAtCenter( SELECTOR_PANEL_CHECKBOX, {}, privateWin.browsingContext ); Assert.ok( !checkbox.checked, "The 'always ask' checkbox should no longer be checked." ); info("Close the panel via confirm."); let promiseDataCleared = waitForPBMDataClear(); promisePanelHidden = waitForConfirmPanelHidden(privateWin); let promiseCompletionToastShown = waitForCompletionToastShown(privateWin); await BrowserTestUtils.synthesizeMouseAtCenter( SELECTOR_PANEL_CONFIRM_BTN, {}, privateWin.browsingContext ); await promisePanelHidden; assertTelemetry( [ { action: "show", reason: "toolbar-btn" }, { action: "hide", reason: "cancel-btn" }, { action: "show", reason: "toolbar-btn" }, { action: "hide", reason: "confirm-btn" }, ], [{ did_confirm: "true" }], "Should have added a hide and a reset event." ); await promiseCompletionToastShown; assertPanelVisibility(privateWin, SELECTOR_PANELVIEW, false); assertPanelVisibility(privateWin, SELECTOR_PANEL_COMPLETION_TOAST, true); await promiseDataCleared; Assert.ok( !Services.prefs.getBoolPref(PREF_ID_ALWAYS_ASK), "The always ask pref should now be false." ); // Need to hide the completion toast, otherwise the next click on the toolbar // button won't work. info("Hide the completion toast."); await hideCompletionToast(privateWin); info( "Simulate a click on the toolbar button. This time the panel should not open - we have unchecked 'always ask'." ); promiseDataCleared = waitForPBMDataClear(); promiseCompletionToastShown = waitForCompletionToastShown(privateWin); await triggerResetBtn(privateWin, false); info("Waiting for PBM session to end."); await promiseDataCleared; info("Data has been cleared."); assertPanelVisibility(privateWin, SELECTOR_PANELVIEW, false); info("Waiting for the completion toast to show."); await promiseCompletionToastShown; assertPanelVisibility(privateWin, SELECTOR_PANELVIEW, false); assertPanelVisibility(privateWin, SELECTOR_PANEL_COMPLETION_TOAST, true); assertTelemetry( [ { action: "show", reason: "toolbar-btn" }, { action: "hide", reason: "cancel-btn" }, { action: "show", reason: "toolbar-btn" }, { action: "hide", reason: "confirm-btn" }, ], [{ did_confirm: "true" }, { did_confirm: "false" }], "Should have added a reset event." ); await BrowserTestUtils.closeWindow(privateWin); Services.prefs.clearUserPref(PREF_ID_ALWAYS_ASK); // Clean up telemetry Services.fog.testResetFOG(); }); /** * Tests that the reset action closes all other private browsing windows and * tabs and triggers private browsing data clearing. */ add_task(async function test_reset_action() { assertTelemetry([], [], "No glean events initially."); await SpecialPowers.pushPrefEnv({ set: [["browser.privatebrowsing.resetPBM.enabled", true]], }); info("Open a few private browsing windows."); let privateBrowsingWindows = []; for (let i = 0; i < 3; i += 1) { privateBrowsingWindows.push( await BrowserTestUtils.openNewBrowserWindow({ private: true, }) ); } info( "Open an additional normal browsing window. It should remain open on reset PBM action." ); let additionalNormalWindow = await BrowserTestUtils.openNewBrowserWindow({ private: false, }); info("Use one of the PBM windows to trigger the PBM restart action."); let [win] = privateBrowsingWindows; win.focus(); Assert.ok( PrivateBrowsingUtils.isWindowPrivate(win), "Window for PBM reset trigger is private window." ); info("Load a bunch of tabs in the private window."); let loadPromises = [ "https://example.com", "https://example.org", "https://example.net", ].map(async url => { let tab = BrowserTestUtils.addTab(win.gBrowser, url); await BrowserTestUtils.browserLoaded(tab.linkedBrowser); }); await Promise.all(loadPromises); info("Switch to the last tab."); win.gBrowser.selectedTab = win.gBrowser.tabs[win.gBrowser.tabs.length - 1]; Assert.ok( win.gBrowser.selectedBrowser.currentURI.spec != "about:privatebrowsing", "The selected tab should not show about:privatebrowsing." ); let windowClosePromises = [ ...privateBrowsingWindows.filter(w => win != w), ].map(w => BrowserTestUtils.windowClosed(w)); // Create promises for tab close for all tabs in the triggering private browsing window. let promisesTabsClosed = win.gBrowser.tabs.map(tab => BrowserTestUtils.waitForTabClosing(tab) ); info("Trigger the restart PBM action"); let promiseDataClear = waitForPBMDataClear(); await ResetPBMPanel._restartPBM(win); info( "Wait for all the windows but the default normal window and the private window which triggered the reset action to be closed." ); await Promise.all(windowClosePromises); info("Wait for tabs in the trigger private window to close."); await Promise.all(promisesTabsClosed); info("Wait for data to be cleared."); await promiseDataClear; Assert.equal( win.gBrowser.tabs.length, 1, "Should only have 1 tab remaining." ); await BrowserTestUtils.waitForCondition( () => win.gBrowser.selectedBrowser.currentURI.spec == "about:privatebrowsing" ); Assert.equal( win.gBrowser.selectedBrowser.currentURI.spec, "about:privatebrowsing", "The remaining tab should point to about:privatebrowsing." ); // Close the private window that remained open. await BrowserTestUtils.closeWindow(win); await BrowserTestUtils.closeWindow(additionalNormalWindow); // We bypass telemetry by calling ResetPBMPanel._restartPBM directly. assertTelemetry([], [], "No glean events after the test."); }); /** * Ensure that we don't show the tab close warning when closing multiple tabs * with the reset PBM action. */ add_task(async function test_tab_close_warning_suppressed() { await SpecialPowers.pushPrefEnv({ set: [ ["browser.privatebrowsing.resetPBM.enabled", true], ["browser.tabs.warnOnClose", true], ["browser.tabs.warnOnCloseOtherTabs", true], ], }); info("Open a private browsing window."); let win = await BrowserTestUtils.openNewBrowserWindow({ private: true, }); info("Open enough tabs so the tab close warning would show."); let loadPromises = []; // warnAboutClosingTabs uses this number to determine whether to show the tab // close warning. let maxTabsUndo = Services.prefs.getIntPref( "browser.sessionstore.max_tabs_undo" ); for (let i = 0; i < maxTabsUndo + 2; i++) { let tab = BrowserTestUtils.addTab(win.gBrowser, "about:blank"); loadPromises.push(BrowserTestUtils.browserLoaded(tab.linkedBrowser)); } await Promise.all(loadPromises); // Create promises for tab close for all tabs in the triggering private browsing window. let promisesTabsClosed = win.gBrowser.tabs.map(tab => BrowserTestUtils.waitForTabClosing(tab) ); info("Trigger the restart PBM action"); let promiseDataClear = waitForPBMDataClear(); await ResetPBMPanel._restartPBM(win); info("Wait for tabs in the trigger private window to close."); await Promise.all(promisesTabsClosed); info("Wait for data to be cleared."); await promiseDataClear; Assert.equal( win.gBrowser.tabs.length, 1, "Should only have 1 tab remaining." ); await BrowserTestUtils.waitForCondition( () => win.gBrowser.selectedBrowser.currentURI.spec == "about:privatebrowsing" ); Assert.equal( win.gBrowser.selectedBrowser.currentURI.spec, "about:privatebrowsing", "The remaining tab should point to about:privatebrowsing." ); // Close the private window that remained open. await BrowserTestUtils.closeWindow(win); }); /** * Test that if the browser sidebar is open the reset action closes it. */ add_task(async function test_reset_action_closes_sidebar() { await SpecialPowers.pushPrefEnv({ set: [["browser.privatebrowsing.resetPBM.enabled", true]], }); info("Open a private browsing window."); let win = await BrowserTestUtils.openNewBrowserWindow({ private: true, }); info( "Open the sidebar of both the private browsing window and the normal browsing window." ); await SidebarUI.show("viewBookmarksSidebar"); await win.SidebarUI.show("viewBookmarksSidebar"); info("Trigger the restart PBM action"); await ResetPBMPanel._restartPBM(win); Assert.ok( SidebarUI.isOpen, "Normal browsing window sidebar should still be open." ); Assert.ok( !win.SidebarUI.isOpen, "Private browsing sidebar should be closed." ); // Cleanup: Close the sidebar of the normal browsing window. SidebarUI.hide(); // Cleanup: Close the private window that remained open. await BrowserTestUtils.closeWindow(win); }); /** * Test that the session store history gets purged by the reset action. */ add_task(async function test_reset_action_purges_session_store() { await SpecialPowers.pushPrefEnv({ set: [["browser.privatebrowsing.resetPBM.enabled", true]], }); info("Open a private browsing window."); let win = await BrowserTestUtils.openNewBrowserWindow({ private: true, }); Assert.equal( SessionStore.getClosedTabCountForWindow(win), 0, "Initially there should be no closed tabs recorded for the PBM window in SessionStore." ); info("Load a bunch of tabs in the private window."); let tab; let loadPromises = [ "https://example.com", "https://example.org", "https://example.net", ].map(async url => { tab = BrowserTestUtils.addTab(win.gBrowser, url); await BrowserTestUtils.browserLoaded(tab.linkedBrowser); }); await Promise.all(loadPromises); info("Manually close a tab"); await SessionStoreTestUtils.closeTab(tab); Assert.equal( SessionStore.getClosedTabCountForWindow(win), 1, "The manually closed tab should be recorded in SessionStore." ); info("Trigger the restart PBM action"); await ResetPBMPanel._restartPBM(win); Assert.equal( SessionStore.getClosedTabCountForWindow(win), 0, "After triggering the PBM reset action there should be no closed tabs recorded for the PBM window in SessionStore." ); // Cleanup: Close the private window that remained open. await BrowserTestUtils.closeWindow(win); }); /** * Test that the the reset action closes all tabs, including pinned and (multi-)selected * tabs. */ add_task(async function test_reset_action_closes_pinned_and_selected_tabs() { await SpecialPowers.pushPrefEnv({ set: [["browser.privatebrowsing.resetPBM.enabled", true]], }); info("Open a private browsing window."); let win = await BrowserTestUtils.openNewBrowserWindow({ private: true, }); info("Load a list of tabs."); let loadPromises = [ "https://example.com", "https://example.org", "https://example.net", "about:blank", ].map(async url => { let tab = BrowserTestUtils.addTab(win.gBrowser, url); await BrowserTestUtils.browserLoaded(tab.linkedBrowser); return tab; }); let tabs = await Promise.all(loadPromises); info("Pin a tab"); win.gBrowser.pinTab(tabs[0]); info("Multi-select tabs."); await BrowserTestUtils.switchTab(win.gBrowser, tabs[2]); win.gBrowser.addToMultiSelectedTabs(tabs[3]); // Create promises for tab close for all tabs in the triggering private browsing window. let promisesTabsClosed = win.gBrowser.tabs.map(tab => BrowserTestUtils.waitForTabClosing(tab) ); info("Trigger the restart PBM action"); await ResetPBMPanel._restartPBM(win); info("Wait for all tabs to be closed."); await promisesTabsClosed; Assert.equal( win.gBrowser.tabs.length, 1, "Should only have 1 tab remaining." ); await BrowserTestUtils.waitForCondition( () => win.gBrowser.selectedBrowser.currentURI.spec == "about:privatebrowsing" ); Assert.equal( win.gBrowser.selectedBrowser.currentURI.spec, "about:privatebrowsing", "The remaining tab should point to about:privatebrowsing." ); // Cleanup: Close the private window/ await BrowserTestUtils.closeWindow(win); });