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 --- .../privatebrowsing/ResetPBMPanel.sys.mjs | 247 ++++++ .../content/aboutPrivateBrowsing.css | 8 + .../content/aboutPrivateBrowsing.html | 132 ++++ .../content/aboutPrivateBrowsing.js | 406 ++++++++++ .../content/assets/cookie-banners-begone.svg | 34 + .../privatebrowsing/content/assets/focus-logo.svg | 112 +++ .../privatebrowsing/content/assets/focus-promo.png | Bin 0 -> 49712 bytes .../content/assets/focus-qr-code.svg | 114 +++ .../content/assets/klar-qr-code.svg | 114 +++ .../privatebrowsing/content/assets/moz-vpn.svg | 4 + .../content/assets/private-promo-asset.svg | 144 ++++ .../privatebrowsing/content/assets/vpn-logo.svg | 6 + browser/components/privatebrowsing/jar.mn | 9 + browser/components/privatebrowsing/metrics.yaml | 50 ++ browser/components/privatebrowsing/moz.build | 18 + .../privatebrowsing/test/browser/browser.toml | 123 +++ .../browser/browser_oa_private_browsing_window.js | 64 ++ ...owser_privatebrowsing_DownloadLastDirWithCPS.js | 445 +++++++++++ .../test/browser/browser_privatebrowsing_about.js | 266 +++++++ .../browser_privatebrowsing_aboutSessionRestore.js | 25 + ...r_privatebrowsing_about_cookie_banners_promo.js | 107 +++ ...wser_privatebrowsing_about_default_pin_promo.js | 110 +++ .../browser_privatebrowsing_about_default_promo.js | 224 ++++++ .../browser_privatebrowsing_about_focus_promo.js | 89 +++ .../browser_privatebrowsing_about_nimbus.js | 459 ++++++++++++ ...browser_privatebrowsing_about_nimbus_dismiss.js | 139 ++++ ...ser_privatebrowsing_about_nimbus_impressions.js | 126 ++++ ...owser_privatebrowsing_about_nimbus_messaging.js | 247 ++++++ .../browser_privatebrowsing_about_search_banner.js | 317 ++++++++ .../test/browser/browser_privatebrowsing_beacon.js | 46 ++ .../browser/browser_privatebrowsing_blobUrl.js | 69 ++ .../test/browser/browser_privatebrowsing_cache.js | 94 +++ .../browser_privatebrowsing_certexceptionsui.js | 65 ++ .../browser/browser_privatebrowsing_cleanup.js | 46 ++ .../browser/browser_privatebrowsing_concurrent.js | 101 +++ .../browser_privatebrowsing_concurrent_page.html | 33 + ...wser_privatebrowsing_context_and_chromeFlags.js | 69 ++ .../test/browser/browser_privatebrowsing_crh.js | 48 ++ .../browser_privatebrowsing_downloadLastDir.js | 133 ++++ .../browser_privatebrowsing_downloadLastDir_c.js | 146 ++++ ...owser_privatebrowsing_downloadLastDir_toggle.js | 118 +++ .../browser/browser_privatebrowsing_favicon.js | 322 ++++++++ .../browser_privatebrowsing_geoprompt_page.html | 13 + .../browser_privatebrowsing_history_shift_click.js | 69 ++ ...rowsing_last_private_browsing_context_exited.js | 66 ++ .../browser_privatebrowsing_lastpbcontextexited.js | 63 ++ .../browser_privatebrowsing_localStorage.js | 28 + ...er_privatebrowsing_localStorage_before_after.js | 46 ++ ...atebrowsing_localStorage_before_after_page.html | 11 + ...tebrowsing_localStorage_before_after_page2.html | 10 + ...browser_privatebrowsing_localStorage_page1.html | 10 + ...browser_privatebrowsing_localStorage_page2.html | 10 + .../browser_privatebrowsing_newtab_from_popup.js | 71 ++ ...r_privatebrowsing_noSessionRestoreMenuOption.js | 29 + .../browser/browser_privatebrowsing_nonbrowser.js | 21 + .../browser/browser_privatebrowsing_opendir.js | 175 +++++ ...rowser_privatebrowsing_placesTitleNoUpdate.html | 8 + .../browser_privatebrowsing_placesTitleNoUpdate.js | 59 ++ .../browser/browser_privatebrowsing_placestitle.js | 82 ++ .../browser_privatebrowsing_protocolhandler.js | 71 ++ ...owser_privatebrowsing_protocolhandler_page.html | 13 + .../browser_privatebrowsing_rememberprompt.js | 90 +++ .../browser/browser_privatebrowsing_resetPBM.js | 824 +++++++++++++++++++++ .../browser/browser_privatebrowsing_sidebar.js | 88 +++ .../browser/browser_privatebrowsing_theming.js | 46 ++ .../test/browser/browser_privatebrowsing_ui.js | 102 +++ .../browser/browser_privatebrowsing_urlbarfocus.js | 44 ++ .../browser/browser_privatebrowsing_windowtitle.js | 140 ++++ .../browser_privatebrowsing_windowtitle_page.html | 9 + .../browser_privatebrowsing_xrprompt_page.html | 11 + .../test/browser/browser_privatebrowsing_zoom.js | 46 ++ .../browser/browser_privatebrowsing_zoomrestore.js | 80 ++ .../privatebrowsing/test/browser/empty_file.html | 1 + .../privatebrowsing/test/browser/file_favicon.html | 11 + .../privatebrowsing/test/browser/file_favicon.png | Bin 0 -> 344 bytes .../test/browser/file_favicon.png^headers^ | 1 + .../test/browser/file_triggeringprincipal_oa.html | 10 + .../privatebrowsing/test/browser/head.js | 163 ++++ .../privatebrowsing/test/browser/title.sjs | 23 + 79 files changed, 7973 insertions(+) create mode 100644 browser/components/privatebrowsing/ResetPBMPanel.sys.mjs create mode 100644 browser/components/privatebrowsing/content/aboutPrivateBrowsing.css create mode 100644 browser/components/privatebrowsing/content/aboutPrivateBrowsing.html create mode 100644 browser/components/privatebrowsing/content/aboutPrivateBrowsing.js create mode 100644 browser/components/privatebrowsing/content/assets/cookie-banners-begone.svg create mode 100644 browser/components/privatebrowsing/content/assets/focus-logo.svg create mode 100644 browser/components/privatebrowsing/content/assets/focus-promo.png create mode 100644 browser/components/privatebrowsing/content/assets/focus-qr-code.svg create mode 100644 browser/components/privatebrowsing/content/assets/klar-qr-code.svg create mode 100644 browser/components/privatebrowsing/content/assets/moz-vpn.svg create mode 100644 browser/components/privatebrowsing/content/assets/private-promo-asset.svg create mode 100644 browser/components/privatebrowsing/content/assets/vpn-logo.svg create mode 100644 browser/components/privatebrowsing/jar.mn create mode 100644 browser/components/privatebrowsing/metrics.yaml create mode 100644 browser/components/privatebrowsing/moz.build create mode 100644 browser/components/privatebrowsing/test/browser/browser.toml create mode 100644 browser/components/privatebrowsing/test/browser/browser_oa_private_browsing_window.js create mode 100644 browser/components/privatebrowsing/test/browser/browser_privatebrowsing_DownloadLastDirWithCPS.js create mode 100644 browser/components/privatebrowsing/test/browser/browser_privatebrowsing_about.js create mode 100644 browser/components/privatebrowsing/test/browser/browser_privatebrowsing_aboutSessionRestore.js create mode 100644 browser/components/privatebrowsing/test/browser/browser_privatebrowsing_about_cookie_banners_promo.js create mode 100644 browser/components/privatebrowsing/test/browser/browser_privatebrowsing_about_default_pin_promo.js create mode 100644 browser/components/privatebrowsing/test/browser/browser_privatebrowsing_about_default_promo.js create mode 100644 browser/components/privatebrowsing/test/browser/browser_privatebrowsing_about_focus_promo.js create mode 100644 browser/components/privatebrowsing/test/browser/browser_privatebrowsing_about_nimbus.js create mode 100644 browser/components/privatebrowsing/test/browser/browser_privatebrowsing_about_nimbus_dismiss.js create mode 100644 browser/components/privatebrowsing/test/browser/browser_privatebrowsing_about_nimbus_impressions.js create mode 100644 browser/components/privatebrowsing/test/browser/browser_privatebrowsing_about_nimbus_messaging.js create mode 100644 browser/components/privatebrowsing/test/browser/browser_privatebrowsing_about_search_banner.js create mode 100644 browser/components/privatebrowsing/test/browser/browser_privatebrowsing_beacon.js create mode 100644 browser/components/privatebrowsing/test/browser/browser_privatebrowsing_blobUrl.js create mode 100644 browser/components/privatebrowsing/test/browser/browser_privatebrowsing_cache.js create mode 100644 browser/components/privatebrowsing/test/browser/browser_privatebrowsing_certexceptionsui.js create mode 100644 browser/components/privatebrowsing/test/browser/browser_privatebrowsing_cleanup.js create mode 100644 browser/components/privatebrowsing/test/browser/browser_privatebrowsing_concurrent.js create mode 100644 browser/components/privatebrowsing/test/browser/browser_privatebrowsing_concurrent_page.html create mode 100644 browser/components/privatebrowsing/test/browser/browser_privatebrowsing_context_and_chromeFlags.js create mode 100644 browser/components/privatebrowsing/test/browser/browser_privatebrowsing_crh.js create mode 100644 browser/components/privatebrowsing/test/browser/browser_privatebrowsing_downloadLastDir.js create mode 100644 browser/components/privatebrowsing/test/browser/browser_privatebrowsing_downloadLastDir_c.js create mode 100644 browser/components/privatebrowsing/test/browser/browser_privatebrowsing_downloadLastDir_toggle.js create mode 100644 browser/components/privatebrowsing/test/browser/browser_privatebrowsing_favicon.js create mode 100644 browser/components/privatebrowsing/test/browser/browser_privatebrowsing_geoprompt_page.html create mode 100644 browser/components/privatebrowsing/test/browser/browser_privatebrowsing_history_shift_click.js create mode 100644 browser/components/privatebrowsing/test/browser/browser_privatebrowsing_last_private_browsing_context_exited.js create mode 100644 browser/components/privatebrowsing/test/browser/browser_privatebrowsing_lastpbcontextexited.js create mode 100644 browser/components/privatebrowsing/test/browser/browser_privatebrowsing_localStorage.js create mode 100644 browser/components/privatebrowsing/test/browser/browser_privatebrowsing_localStorage_before_after.js create mode 100644 browser/components/privatebrowsing/test/browser/browser_privatebrowsing_localStorage_before_after_page.html create mode 100644 browser/components/privatebrowsing/test/browser/browser_privatebrowsing_localStorage_before_after_page2.html create mode 100644 browser/components/privatebrowsing/test/browser/browser_privatebrowsing_localStorage_page1.html create mode 100644 browser/components/privatebrowsing/test/browser/browser_privatebrowsing_localStorage_page2.html create mode 100644 browser/components/privatebrowsing/test/browser/browser_privatebrowsing_newtab_from_popup.js create mode 100644 browser/components/privatebrowsing/test/browser/browser_privatebrowsing_noSessionRestoreMenuOption.js create mode 100644 browser/components/privatebrowsing/test/browser/browser_privatebrowsing_nonbrowser.js create mode 100644 browser/components/privatebrowsing/test/browser/browser_privatebrowsing_opendir.js create mode 100644 browser/components/privatebrowsing/test/browser/browser_privatebrowsing_placesTitleNoUpdate.html create mode 100644 browser/components/privatebrowsing/test/browser/browser_privatebrowsing_placesTitleNoUpdate.js create mode 100644 browser/components/privatebrowsing/test/browser/browser_privatebrowsing_placestitle.js create mode 100644 browser/components/privatebrowsing/test/browser/browser_privatebrowsing_protocolhandler.js create mode 100644 browser/components/privatebrowsing/test/browser/browser_privatebrowsing_protocolhandler_page.html create mode 100644 browser/components/privatebrowsing/test/browser/browser_privatebrowsing_rememberprompt.js create mode 100644 browser/components/privatebrowsing/test/browser/browser_privatebrowsing_resetPBM.js create mode 100644 browser/components/privatebrowsing/test/browser/browser_privatebrowsing_sidebar.js create mode 100644 browser/components/privatebrowsing/test/browser/browser_privatebrowsing_theming.js create mode 100644 browser/components/privatebrowsing/test/browser/browser_privatebrowsing_ui.js create mode 100644 browser/components/privatebrowsing/test/browser/browser_privatebrowsing_urlbarfocus.js create mode 100644 browser/components/privatebrowsing/test/browser/browser_privatebrowsing_windowtitle.js create mode 100644 browser/components/privatebrowsing/test/browser/browser_privatebrowsing_windowtitle_page.html create mode 100644 browser/components/privatebrowsing/test/browser/browser_privatebrowsing_xrprompt_page.html create mode 100644 browser/components/privatebrowsing/test/browser/browser_privatebrowsing_zoom.js create mode 100644 browser/components/privatebrowsing/test/browser/browser_privatebrowsing_zoomrestore.js create mode 100644 browser/components/privatebrowsing/test/browser/empty_file.html create mode 100644 browser/components/privatebrowsing/test/browser/file_favicon.html create mode 100644 browser/components/privatebrowsing/test/browser/file_favicon.png create mode 100644 browser/components/privatebrowsing/test/browser/file_favicon.png^headers^ create mode 100644 browser/components/privatebrowsing/test/browser/file_triggeringprincipal_oa.html create mode 100644 browser/components/privatebrowsing/test/browser/head.js create mode 100644 browser/components/privatebrowsing/test/browser/title.sjs (limited to 'browser/components/privatebrowsing') diff --git a/browser/components/privatebrowsing/ResetPBMPanel.sys.mjs b/browser/components/privatebrowsing/ResetPBMPanel.sys.mjs new file mode 100644 index 0000000000..f5e818c2a8 --- /dev/null +++ b/browser/components/privatebrowsing/ResetPBMPanel.sys.mjs @@ -0,0 +1,247 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* 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/. */ +/* eslint-env mozilla/browser-window */ + +/** + * ResetPBMPanel contains the logic for the restart private browsing action. + * The feature is exposed via a toolbar button in private browsing windows. It + * allows users to restart their private browsing session, clearing all site + * data and closing all PBM tabs / windows. + * The toolbar button for triggering the panel is only shown in private browsing + * windows or if permanent private browsing mode is enabled. + */ + +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +const ENABLED_PREF = "browser.privatebrowsing.resetPBM.enabled"; +const SHOW_CONFIRM_DIALOG_PREF = + "browser.privatebrowsing.resetPBM.showConfirmationDialog"; + +const lazy = {}; +ChromeUtils.defineESModuleGetters(lazy, { + CustomizableUI: "resource:///modules/CustomizableUI.sys.mjs", + PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", + SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs", +}); + +export const ResetPBMPanel = { + // Button and view config for CustomizableUI. + _widgetConfig: null, + + /** + * Initialize the widget code depending on pref state. + */ + init() { + // Populate _widgetConfig during init to defer (lazy) CustomizableUI import. + this._widgetConfig ??= { + id: "reset-pbm-toolbar-button", + l10nId: "reset-pbm-toolbar-button", + type: "view", + viewId: "reset-pbm-panel", + defaultArea: lazy.CustomizableUI.AREA_NAVBAR, + onViewShowing(aEvent) { + ResetPBMPanel.onViewShowing(aEvent); + }, + }; + + if (this._enabled) { + lazy.CustomizableUI.createWidget(this._widgetConfig); + } else { + lazy.CustomizableUI.destroyWidget(this._widgetConfig.id); + } + }, + + /** + * Called when the reset pbm panelview is showing as the result of clicking + * the toolbar button. + */ + async onViewShowing(event) { + let triggeringWindow = event.target.ownerGlobal; + + // We may skip the confirmation panel if disabled via pref. + if (!this._shouldConfirmClear) { + // Prevent the panel from showing up. + event.preventDefault(); + + // If the action is triggered from the overflow menu make sure that the + // panel gets hidden. + lazy.CustomizableUI.hidePanelForNode(event.target); + + // Trigger the restart action. + await this._restartPBM(triggeringWindow); + + Glean.privateBrowsingResetPbm.resetAction.record({ did_confirm: false }); + return; + } + + // Before the panel is shown, update checkbox state based on pref. + this._rememberCheck(triggeringWindow).checked = this._shouldConfirmClear; + + Glean.privateBrowsingResetPbm.confirmPanel.record({ + action: "show", + reason: "toolbar-btn", + }); + }, + + /** + * Handles the confirmation panel cancel button. + * @param {MozButton} button - Cancel button that triggered the action. + */ + onCancel(button) { + if (!this._enabled) { + throw new Error("Not initialized."); + } + lazy.CustomizableUI.hidePanelForNode(button); + + Glean.privateBrowsingResetPbm.confirmPanel.record({ + action: "hide", + reason: "cancel-btn", + }); + }, + + /** + * Handles the confirmation panel confirm button which triggers the clear + * action. + * @param {MozButton} button - Confirm button that triggered the action. + */ + async onConfirm(button) { + if (!this._enabled) { + throw new Error("Not initialized."); + } + let triggeringWindow = button.ownerGlobal; + + // Write the checkbox state to pref. Only do this when the user + // confirms. + // Setting this pref to true means there is no way to see the panel + // again other than flipping the pref back via about:config or resetting + // the profile. This is by design. + Services.prefs.setBoolPref( + SHOW_CONFIRM_DIALOG_PREF, + this._rememberCheck(triggeringWindow).checked + ); + + lazy.CustomizableUI.hidePanelForNode(button); + + Glean.privateBrowsingResetPbm.confirmPanel.record({ + action: "hide", + reason: "confirm-btn", + }); + + // Clear the private browsing session. + await this._restartPBM(triggeringWindow); + + Glean.privateBrowsingResetPbm.resetAction.record({ did_confirm: true }); + }, + + /** + * Restart the private browsing session. This is achieved by closing all other + * PBM windows, closing all tabs in the current window but + * about:privatebrowsing and triggering PBM data clearing. + * + * @param {ChromeWindow} triggeringWindow - The (private browsing) chrome window which + * triggered the restart action. + */ + async _restartPBM(triggeringWindow) { + if ( + !triggeringWindow || + !lazy.PrivateBrowsingUtils.isWindowPrivate(triggeringWindow) + ) { + throw new Error("Invalid triggering window."); + } + + // 1. Close all PBM windows but the current one. + for (let w of Services.ww.getWindowEnumerator()) { + if ( + w != triggeringWindow && + lazy.PrivateBrowsingUtils.isWindowPrivate(w) + ) { + // This suppresses confirmation dialogs like the beforeunload + // handler and the tab close warning. + // Skip over windows that don't have the closeWindow method. + w.closeWindow?.(true, null, "restart-pbm"); + } + } + + // 2. For the current PBM window create a new tab which will be used for + // the initial newtab page. + let newTab = triggeringWindow.gBrowser.addTab( + triggeringWindow.BROWSER_NEW_TAB_URL, + { + triggeringPrincipal: + Services.scriptSecurityManager.getSystemPrincipal(), + } + ); + if (!newTab) { + throw new Error("Could not open new tab."); + } + + // 3. Close all other tabs. + triggeringWindow.gBrowser.removeAllTabsBut(newTab, { + skipPermitUnload: true, + // Instruct the SessionStore to not save closed tab data for these tabs. + // We don't want to leak them into the next private browsing session. + skipSessionStore: true, + animate: false, + skipWarnAboutClosingTabs: true, + skipPinnedOrSelectedTabs: false, + }); + + // In the remaining PBM window: If the sidebar is open close it. + triggeringWindow.SidebarUI?.hide(); + + // Clear session store data for the remaining PBM window. + lazy.SessionStore.purgeDataForPrivateWindow(triggeringWindow); + + // 4. Clear private browsing data. + // TODO: this doesn't wait for data to be cleared. This is probably + // fine since PBM data is stored in memory and can be cleared quick + // enough. The mechanism is brittle though, some callers still + // perform clearing async. Bug 1846494 will address this. + Services.obs.notifyObservers(null, "last-pb-context-exited"); + + // Once clearing is complete show a toast message. + + let toolbarButton = this._toolbarButton(triggeringWindow); + + // Find the anchor used for the confirmation hint panel. If the toolbar + // button is in the overflow menu we can't use it as an anchor. Instead we + // anchor off the overflow button as indicated by cui-anchorid. + let anchor; + let anchorID = toolbarButton.getAttribute("cui-anchorid"); + if (anchorID) { + anchor = triggeringWindow.document.getElementById(anchorID); + } + triggeringWindow.ConfirmationHint.show( + anchor ?? toolbarButton, + "reset-pbm-panel-complete", + { position: "bottomright topright" } + ); + }, + + _toolbarButton(win) { + return lazy.CustomizableUI.getWidget(this._widgetConfig.id).forWindow(win) + .node; + }, + + _rememberCheck(win) { + return win.document.getElementById("reset-pbm-panel-checkbox"); + }, +}; + +XPCOMUtils.defineLazyPreferenceGetter( + ResetPBMPanel, + "_enabled", + ENABLED_PREF, + false, + // On pref change update the init state. + ResetPBMPanel.init.bind(ResetPBMPanel) +); +XPCOMUtils.defineLazyPreferenceGetter( + ResetPBMPanel, + "_shouldConfirmClear", + SHOW_CONFIRM_DIALOG_PREF, + true +); diff --git a/browser/components/privatebrowsing/content/aboutPrivateBrowsing.css b/browser/components/privatebrowsing/content/aboutPrivateBrowsing.css new file mode 100644 index 0000000000..26d143a7b0 --- /dev/null +++ b/browser/components/privatebrowsing/content/aboutPrivateBrowsing.css @@ -0,0 +1,8 @@ +/* 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/. */ + +html.private .showNormal, +html.normal .showPrivate { + display: none !important; +} diff --git a/browser/components/privatebrowsing/content/aboutPrivateBrowsing.html b/browser/components/privatebrowsing/content/aboutPrivateBrowsing.html new file mode 100644 index 0000000000..261d6aad08 --- /dev/null +++ b/browser/components/privatebrowsing/content/aboutPrivateBrowsing.html @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + +

+ + +
+
+ +
+
+
+ +
+
+
+

+

+ +
+
+
+ + + + diff --git a/browser/components/privatebrowsing/content/aboutPrivateBrowsing.js b/browser/components/privatebrowsing/content/aboutPrivateBrowsing.js new file mode 100644 index 0000000000..0f0e23d81c --- /dev/null +++ b/browser/components/privatebrowsing/content/aboutPrivateBrowsing.js @@ -0,0 +1,406 @@ +/* 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/. */ + +/* eslint-env mozilla/remote-page */ + +/** + * Determines whether a given value is a fluent id or plain text and adds it to an element + * @param {Array<[HTMLElement, string]>} items An array of [element, value] where value is + * a fluent id starting with "fluent:" or plain text + */ +function translateElements(items) { + items.forEach(([element, value]) => { + // Skip empty text or elements + if (!element || !value) { + return; + } + const fluentId = value.replace(/^fluent:/, ""); + if (fluentId !== value) { + document.l10n.setAttributes(element, fluentId); + } else { + element.textContent = value; + element.removeAttribute("data-l10n-id"); + } + }); +} + +function renderInfo({ + infoEnabled, + infoTitle, + infoTitleEnabled, + infoBody, + infoLinkText, + infoLinkUrl, + infoIcon, +} = {}) { + const container = document.querySelector(".info"); + if (infoEnabled === false) { + container.hidden = true; + return; + } + container.hidden = false; + + const titleEl = document.getElementById("info-title"); + const bodyEl = document.getElementById("info-body"); + const linkEl = document.getElementById("private-browsing-myths"); + + let feltPrivacyEnabled = RPMGetBoolPref( + "browser.privatebrowsing.felt-privacy-v1", + false + ); + + if (infoIcon && !feltPrivacyEnabled) { + container.style.backgroundImage = `url(${infoIcon})`; + } + + if (feltPrivacyEnabled) { + // Record exposure event for Felt Privacy experiment + window.FeltPrivacyExposureTelemetry(); + + infoTitleEnabled = true; + infoTitle = "fluent:about-private-browsing-felt-privacy-v1-info-header"; + infoBody = "fluent:about-private-browsing-felt-privacy-v1-info-body"; + infoLinkText = "fluent:about-private-browsing-felt-privacy-v1-info-link"; + } + + titleEl.hidden = !infoTitleEnabled; + + translateElements([ + [titleEl, infoTitle], + [bodyEl, infoBody], + [linkEl, infoLinkText], + ]); + + if (infoLinkUrl) { + linkEl.setAttribute("href", infoLinkUrl); + } +} + +async function renderPromo({ + messageId = null, + promoEnabled = false, + promoType = "VPN", + promoTitle, + promoTitleEnabled, + promoLinkText, + promoLinkType, + promoSectionStyle, + promoHeader, + promoImageLarge, + promoImageSmall, + promoButton = null, +} = {}) { + const shouldShow = await RPMSendQuery("ShouldShowPromo", { type: promoType }); + const container = document.querySelector(".promo"); + + if (!promoEnabled || !shouldShow) { + container.remove(); + return false; + } + + const titleEl = document.getElementById("private-browsing-promo-text"); + const linkEl = document.getElementById("private-browsing-promo-link"); + const promoHeaderEl = document.getElementById("promo-header"); + const infoContainerEl = document.querySelector(".info"); + const promoImageLargeEl = document.querySelector(".promo-image-large img"); + const promoImageSmallEl = document.querySelector(".promo-image-small img"); + const dismissBtn = document.querySelector("#dismiss-btn"); + + if (promoLinkType === "link") { + linkEl.classList.remove("primary"); + linkEl.classList.add("text-link", "promo-link"); + } + + if (promoButton?.action) { + linkEl.addEventListener("click", async event => { + event.preventDefault(); + + // Record promo click telemetry and set metrics as allow for spotlight + // modal opened on promo click if user is enrolled in an experiment + let isExperiment = window.PrivateBrowsingRecordClick("promo_link"); + const promoButtonData = promoButton?.action?.data; + if ( + promoButton?.action?.type === "SHOW_SPOTLIGHT" && + promoButtonData?.content + ) { + promoButtonData.content.metrics = isExperiment ? "allow" : "block"; + } + + await RPMSendQuery("SpecialMessageActionDispatch", promoButton.action); + }); + } else { + // If the action doesn't exist, remove the promo completely + container.remove(); + return false; + } + + const onDismissBtnClick = () => { + window.ASRouterMessage({ + type: "BLOCK_MESSAGE_BY_ID", + data: { id: messageId }, + }); + window.PrivateBrowsingRecordClick("dismiss_button"); + container.remove(); + }; + + if (dismissBtn && messageId) { + dismissBtn.addEventListener("click", onDismissBtnClick, { once: true }); + } + + if (promoSectionStyle) { + container.classList.add(promoSectionStyle); + + switch (promoSectionStyle) { + case "below-search": + container.remove(); + infoContainerEl?.insertAdjacentElement("beforebegin", container); + break; + case "top": + container.remove(); + document.body.insertAdjacentElement("afterbegin", container); + } + } + + if (promoImageLarge) { + promoImageLargeEl.src = promoImageLarge; + } else { + promoImageLargeEl.parentNode.remove(); + } + + if (promoImageSmall) { + promoImageSmallEl.src = promoImageSmall; + } else { + promoImageSmallEl.parentNode.remove(); + } + + if (!promoTitleEnabled) { + titleEl.remove(); + } + + if (!promoHeader) { + promoHeaderEl.remove(); + } + + translateElements([ + [titleEl, promoTitle], + [linkEl, promoLinkText], + [promoHeaderEl, promoHeader], + ]); + + // Only make promo section visible after adding content + // and translations to prevent layout shifting in page + container.classList.add("promo-visible"); + return true; +} + +/** + * For every PB newtab loaded, a second is pre-rendered in the background. + * We need to guard against invalid impressions by checking visibility state. + * If visible, record. Otherwise, listen for visibility change and record later. + */ +function recordOnceVisible(message) { + const recordImpression = () => { + if (document.visibilityState === "visible") { + window.ASRouterMessage({ + type: "IMPRESSION", + data: message, + }); + // Similar telemetry, but for Nimbus experiments + window.PrivateBrowsingPromoExposureTelemetry(); + document.removeEventListener("visibilitychange", recordImpression); + } + }; + + if (document.visibilityState === "visible") { + window.ASRouterMessage({ + type: "IMPRESSION", + data: message, + }); + // Similar telemetry, but for Nimbus experiments + window.PrivateBrowsingPromoExposureTelemetry(); + } else { + document.addEventListener("visibilitychange", recordImpression); + } +} + +// The PB newtab may be pre-rendered. Once the tab is visible, check to make sure the message wasn't blocked after the initial render. If it was, remove the promo. +function handlePromoOnPreload(message) { + async function removePromoIfBlocked() { + if (document.visibilityState === "visible") { + let blocked = await RPMSendQuery("IsPromoBlocked", message); + if (blocked) { + const container = document.querySelector(".promo"); + container.remove(); + } + } + document.removeEventListener("visibilitychange", removePromoIfBlocked); + } + // Only add the listener to pre-rendered tabs that aren't visible + if (document.visibilityState !== "visible") { + document.addEventListener("visibilitychange", removePromoIfBlocked); + } +} + +async function setupMessageConfig(config = null) { + let message = null; + + if (!config) { + let hideDefault = window.PrivateBrowsingShouldHideDefault(); + try { + let response = await window.ASRouterMessage({ + type: "PBNEWTAB_MESSAGE_REQUEST", + data: { hideDefault: !!hideDefault }, + }); + message = response?.message; + config = message?.content; + config.messageId = message?.id; + } catch (e) {} + } + + renderInfo(config); + let hasRendered = await renderPromo(config); + if (hasRendered && message) { + recordOnceVisible(message); + handlePromoOnPreload(message); + } + // For tests + document.documentElement.setAttribute("PrivateBrowsingRenderComplete", true); +} + +let SHOW_DEVTOOLS_MESSAGE = "ShowDevToolsMessage"; + +function showDevToolsMessage(msg) { + msg.data.content.messageId = "DEVTOOLS_MESSAGE"; + setupMessageConfig(msg?.data?.content); + RPMRemoveMessageListener(SHOW_DEVTOOLS_MESSAGE, showDevToolsMessage); +} + +document.addEventListener("DOMContentLoaded", function () { + // check the url to see if we're rendering a devtools message + if (document.location.toString().includes("debug")) { + RPMAddMessageListener(SHOW_DEVTOOLS_MESSAGE, showDevToolsMessage); + return; + } + if (!RPMIsWindowPrivate()) { + document.documentElement.classList.remove("private"); + document.documentElement.classList.add("normal"); + document + .getElementById("startPrivateBrowsing") + .addEventListener("click", function () { + RPMSendAsyncMessage("OpenPrivateWindow"); + }); + return; + } + + // The default info content is already in the markup, but we need to use JS to + // set up the learn more link, since it's dynamically generated. + const linkEl = document.getElementById("private-browsing-myths"); + linkEl.setAttribute( + "href", + RPMGetFormatURLPref("app.support.baseURL") + "private-browsing-myths" + ); + linkEl.addEventListener("click", () => { + window.PrivateBrowsingRecordClick("info_link"); + }); + + // We don't do this setup until now, because we don't want to record any impressions until we're + // sure we're actually running a private window, not just about:privatebrowsing in a normal window. + setupMessageConfig(); + + // Set up the private search banner. + const privateSearchBanner = document.getElementById("search-banner"); + + RPMSendQuery("ShouldShowSearchBanner", {}).then(engineName => { + if (engineName) { + document.l10n.setAttributes( + document.getElementById("about-private-browsing-search-banner-title"), + "about-private-browsing-search-banner-title", + { engineName } + ); + privateSearchBanner.removeAttribute("hidden"); + document.body.classList.add("showBanner"); + } + + // We set this attribute so that tests know when we are done. + document.documentElement.setAttribute("SearchBannerInitialized", true); + }); + + function hideSearchBanner() { + privateSearchBanner.hidden = true; + document.body.classList.remove("showBanner"); + RPMSendAsyncMessage("SearchBannerDismissed"); + } + + document + .getElementById("search-banner-close-button") + .addEventListener("click", () => { + hideSearchBanner(); + }); + + let openSearchOptions = document.getElementById( + "about-private-browsing-search-banner-description" + ); + let openSearchOptionsEvtHandler = evt => { + if ( + evt.target.id == "open-search-options-link" && + (evt.keyCode == evt.DOM_VK_RETURN || evt.type == "click") + ) { + RPMSendAsyncMessage("OpenSearchPreferences"); + hideSearchBanner(); + } + }; + openSearchOptions.addEventListener("click", openSearchOptionsEvtHandler); + openSearchOptions.addEventListener("keypress", openSearchOptionsEvtHandler); + + // Setup the search hand-off box. + let btn = document.getElementById("search-handoff-button"); + + let editable = document.getElementById("fake-editable"); + let DISABLE_SEARCH_TOPIC = "DisableSearch"; + let SHOW_SEARCH_TOPIC = "ShowSearch"; + let SEARCH_HANDOFF_TOPIC = "SearchHandoff"; + + function showSearch() { + btn.classList.remove("focused"); + btn.classList.remove("disabled"); + RPMRemoveMessageListener(SHOW_SEARCH_TOPIC, showSearch); + } + + function disableSearch() { + btn.classList.add("disabled"); + } + + function handoffSearch(text) { + RPMSendAsyncMessage(SEARCH_HANDOFF_TOPIC, { text }); + RPMAddMessageListener(SHOW_SEARCH_TOPIC, showSearch); + if (text) { + disableSearch(); + } else { + btn.classList.add("focused"); + RPMAddMessageListener(DISABLE_SEARCH_TOPIC, disableSearch); + } + } + btn.addEventListener("focus", function () { + handoffSearch(); + }); + btn.addEventListener("click", function () { + handoffSearch(); + }); + + // Hand-off any text that gets dropped or pasted + editable.addEventListener("drop", function (ev) { + ev.preventDefault(); + let text = ev.dataTransfer.getData("text"); + if (text) { + handoffSearch(text); + } + }); + editable.addEventListener("paste", function (ev) { + ev.preventDefault(); + handoffSearch(ev.clipboardData.getData("Text")); + }); + + // Load contentSearchUI so it sets the search engine icon and name for us. + new window.ContentSearchHandoffUIController(); +}); diff --git a/browser/components/privatebrowsing/content/assets/cookie-banners-begone.svg b/browser/components/privatebrowsing/content/assets/cookie-banners-begone.svg new file mode 100644 index 0000000000..66e47020cd --- /dev/null +++ b/browser/components/privatebrowsing/content/assets/cookie-banners-begone.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/browser/components/privatebrowsing/content/assets/focus-logo.svg b/browser/components/privatebrowsing/content/assets/focus-logo.svg new file mode 100644 index 0000000000..30fa505b2a --- /dev/null +++ b/browser/components/privatebrowsing/content/assets/focus-logo.svg @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/browser/components/privatebrowsing/content/assets/focus-promo.png b/browser/components/privatebrowsing/content/assets/focus-promo.png new file mode 100644 index 0000000000..b7badd3cdd Binary files /dev/null and b/browser/components/privatebrowsing/content/assets/focus-promo.png differ diff --git a/browser/components/privatebrowsing/content/assets/focus-qr-code.svg b/browser/components/privatebrowsing/content/assets/focus-qr-code.svg new file mode 100644 index 0000000000..f182567314 --- /dev/null +++ b/browser/components/privatebrowsing/content/assets/focus-qr-code.svg @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/browser/components/privatebrowsing/content/assets/klar-qr-code.svg b/browser/components/privatebrowsing/content/assets/klar-qr-code.svg new file mode 100644 index 0000000000..2217ca055c --- /dev/null +++ b/browser/components/privatebrowsing/content/assets/klar-qr-code.svg @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/browser/components/privatebrowsing/content/assets/moz-vpn.svg b/browser/components/privatebrowsing/content/assets/moz-vpn.svg new file mode 100644 index 0000000000..038d2952b6 --- /dev/null +++ b/browser/components/privatebrowsing/content/assets/moz-vpn.svg @@ -0,0 +1,4 @@ + + diff --git a/browser/components/privatebrowsing/content/assets/private-promo-asset.svg b/browser/components/privatebrowsing/content/assets/private-promo-asset.svg new file mode 100644 index 0000000000..841384f015 --- /dev/null +++ b/browser/components/privatebrowsing/content/assets/private-promo-asset.svg @@ -0,0 +1,144 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/browser/components/privatebrowsing/content/assets/vpn-logo.svg b/browser/components/privatebrowsing/content/assets/vpn-logo.svg new file mode 100644 index 0000000000..01e7c72d8d --- /dev/null +++ b/browser/components/privatebrowsing/content/assets/vpn-logo.svg @@ -0,0 +1,6 @@ + + + + diff --git a/browser/components/privatebrowsing/jar.mn b/browser/components/privatebrowsing/jar.mn new file mode 100644 index 0000000000..f7f78e615f --- /dev/null +++ b/browser/components/privatebrowsing/jar.mn @@ -0,0 +1,9 @@ +# 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/. + +browser.jar: + content/browser/aboutPrivateBrowsing.css (content/aboutPrivateBrowsing.css) + content/browser/aboutPrivateBrowsing.html (content/aboutPrivateBrowsing.html) + content/browser/aboutPrivateBrowsing.js (content/aboutPrivateBrowsing.js) + content/browser/assets/ (content/assets/*) diff --git a/browser/components/privatebrowsing/metrics.yaml b/browser/components/privatebrowsing/metrics.yaml new file mode 100644 index 0000000000..54f55be40b --- /dev/null +++ b/browser/components/privatebrowsing/metrics.yaml @@ -0,0 +1,50 @@ +# 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/. + +# Adding a new metric? We have docs for that! +# https://firefox-source-docs.mozilla.org/toolkit/components/glean/user/new_definitions_file.html + +--- +$schema: moz://mozilla.org/schemas/glean/metrics/2-0-0 +$tags: + - 'Firefox :: Private Browsing' + +private_browsing.reset_pbm: + confirm_panel: + type: event + description: > + Confirm panel show / hide event. + extra_keys: + action: + type: string + description: Whether the panel was hidden or shown. + reason: + type: string + description: Reason for the action. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1853698 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1853698#c3 + data_sensitivity: + - interaction + notification_emails: + - pbz@mozilla.com + expires: never + reset_action: + type: event + description: > + Dispatched whenever PBM is restarted / reset via the resetPBM feature. + extra_keys: + did_confirm: + type: boolean + description: Whether the user confirmed the reset action via the confirmation dialog. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1853698 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1853698#c3 + data_sensitivity: + - interaction + notification_emails: + - pbz@mozilla.com + expires: never diff --git a/browser/components/privatebrowsing/moz.build b/browser/components/privatebrowsing/moz.build new file mode 100644 index 0000000000..822b875e2a --- /dev/null +++ b/browser/components/privatebrowsing/moz.build @@ -0,0 +1,18 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +BROWSER_CHROME_MANIFESTS += [ + "test/browser/browser.toml", +] + +JAR_MANIFESTS += ["jar.mn"] + +EXTRA_JS_MODULES += [ + "ResetPBMPanel.sys.mjs", +] + +with Files("**"): + BUG_COMPONENT = ("Firefox", "Private Browsing") diff --git a/browser/components/privatebrowsing/test/browser/browser.toml b/browser/components/privatebrowsing/test/browser/browser.toml new file mode 100644 index 0000000000..2c37cd8a48 --- /dev/null +++ b/browser/components/privatebrowsing/test/browser/browser.toml @@ -0,0 +1,123 @@ +[DEFAULT] +tags = "openwindow" +support-files = [ + "browser_privatebrowsing_concurrent_page.html", + "browser_privatebrowsing_geoprompt_page.html", + "browser_privatebrowsing_xrprompt_page.html", + "browser_privatebrowsing_localStorage_before_after_page.html", + "browser_privatebrowsing_localStorage_before_after_page2.html", + "browser_privatebrowsing_localStorage_page1.html", + "browser_privatebrowsing_localStorage_page2.html", + "browser_privatebrowsing_placesTitleNoUpdate.html", + "browser_privatebrowsing_protocolhandler_page.html", + "browser_privatebrowsing_windowtitle_page.html", + "head.js", + "title.sjs", + "empty_file.html", + "file_favicon.html", + "file_favicon.png", + "file_favicon.png^headers^", + "file_triggeringprincipal_oa.html", +] + +["browser_oa_private_browsing_window.js"] + +["browser_privatebrowsing_DownloadLastDirWithCPS.js"] + +["browser_privatebrowsing_about.js"] +skip-if = ["verify"] +tags = "trackingprotection" + +["browser_privatebrowsing_aboutSessionRestore.js"] + +["browser_privatebrowsing_about_cookie_banners_promo.js"] + +["browser_privatebrowsing_about_default_pin_promo.js"] +skip-if = [ + "os == 'mac'", + "os == 'linux'", + "os == 'win' && msix", # We don't support pinning in MSIX builds +] + +["browser_privatebrowsing_about_default_promo.js"] + +["browser_privatebrowsing_about_focus_promo.js"] + +["browser_privatebrowsing_about_nimbus.js"] + +["browser_privatebrowsing_about_nimbus_dismiss.js"] + +["browser_privatebrowsing_about_nimbus_impressions.js"] + +["browser_privatebrowsing_about_nimbus_messaging.js"] + +["browser_privatebrowsing_about_search_banner.js"] + +["browser_privatebrowsing_beacon.js"] + +["browser_privatebrowsing_blobUrl.js"] + +["browser_privatebrowsing_cache.js"] + +["browser_privatebrowsing_certexceptionsui.js"] + +["browser_privatebrowsing_cleanup.js"] + +["browser_privatebrowsing_concurrent.js"] +skip-if = ["release_or_beta"] + +["browser_privatebrowsing_context_and_chromeFlags.js"] + +["browser_privatebrowsing_crh.js"] + +["browser_privatebrowsing_downloadLastDir.js"] +skip-if = ["verify"] + +["browser_privatebrowsing_downloadLastDir_c.js"] + +["browser_privatebrowsing_downloadLastDir_toggle.js"] + +["browser_privatebrowsing_favicon.js"] + +["browser_privatebrowsing_history_shift_click.js"] + +["browser_privatebrowsing_last_private_browsing_context_exited.js"] + +["browser_privatebrowsing_lastpbcontextexited.js"] + +["browser_privatebrowsing_localStorage.js"] + +["browser_privatebrowsing_localStorage_before_after.js"] + +["browser_privatebrowsing_newtab_from_popup.js"] + +["browser_privatebrowsing_noSessionRestoreMenuOption.js"] + +["browser_privatebrowsing_nonbrowser.js"] + +["browser_privatebrowsing_opendir.js"] + +["browser_privatebrowsing_placesTitleNoUpdate.js"] + +["browser_privatebrowsing_placestitle.js"] + +["browser_privatebrowsing_protocolhandler.js"] + +["browser_privatebrowsing_rememberprompt.js"] +tags = "geolocation xr" + +["browser_privatebrowsing_resetPBM.js"] + +["browser_privatebrowsing_sidebar.js"] + +["browser_privatebrowsing_theming.js"] + +["browser_privatebrowsing_ui.js"] + +["browser_privatebrowsing_urlbarfocus.js"] + +["browser_privatebrowsing_windowtitle.js"] + +["browser_privatebrowsing_zoom.js"] + +["browser_privatebrowsing_zoomrestore.js"] diff --git a/browser/components/privatebrowsing/test/browser/browser_oa_private_browsing_window.js b/browser/components/privatebrowsing/test/browser/browser_oa_private_browsing_window.js new file mode 100644 index 0000000000..a1b9420171 --- /dev/null +++ b/browser/components/privatebrowsing/test/browser/browser_oa_private_browsing_window.js @@ -0,0 +1,64 @@ +"use strict"; + +const PATH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" +); +const TEST_PAGE = PATH + "file_triggeringprincipal_oa.html"; +const DUMMY_PAGE = PATH + "empty_file.html"; + +add_task( + async function test_principal_right_click_open_link_in_new_private_win() { + await BrowserTestUtils.withNewTab(TEST_PAGE, async function (browser) { + let promiseNewWindow = BrowserTestUtils.waitForNewWindow({ + url: DUMMY_PAGE, + }); + + // simulate right-click open link in new private window + BrowserTestUtils.waitForEvent(document, "popupshown", false, event => { + document.getElementById("context-openlinkprivate").doCommand(); + event.target.hidePopup(); + return true; + }); + BrowserTestUtils.synthesizeMouseAtCenter( + "#checkPrincipalOA", + { type: "contextmenu", button: 2 }, + gBrowser.selectedBrowser + ); + let privateWin = await promiseNewWindow; + + await SpecialPowers.spawn( + privateWin.gBrowser.selectedBrowser, + [{ DUMMY_PAGE, TEST_PAGE }], + // eslint-disable-next-line no-shadow + async function ({ DUMMY_PAGE, TEST_PAGE }) { + // eslint-disable-line + + let channel = content.docShell.currentDocumentChannel; + is( + channel.URI.spec, + DUMMY_PAGE, + "sanity check to ensure we check principal for right URI" + ); + + let triggeringPrincipal = channel.loadInfo.triggeringPrincipal; + ok( + triggeringPrincipal.isContentPrincipal, + "sanity check to ensure principal is a contentPrincipal" + ); + is( + triggeringPrincipal.spec, + TEST_PAGE, + "test page must be the triggering page" + ); + is( + triggeringPrincipal.originAttributes.privateBrowsingId, + 1, + "must have correct privateBrowsingId" + ); + } + ); + await BrowserTestUtils.closeWindow(privateWin); + }); + } +); diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_DownloadLastDirWithCPS.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_DownloadLastDirWithCPS.js new file mode 100644 index 0000000000..eeaeee033c --- /dev/null +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_DownloadLastDirWithCPS.js @@ -0,0 +1,445 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/. */ + +var gTests; +function test() { + waitForExplicitFinish(); + requestLongerTimeout(2); + runTest().catch(ex => ok(false, ex)); +} + +/* + * ================ + * Helper functions + * ================ + */ + +function createWindow(aOptions) { + return new Promise(resolve => whenNewWindowLoaded(aOptions, resolve)); +} + +function getFile(downloadLastDir, aURI) { + return downloadLastDir.getFileAsync(aURI); +} + +function setFile(downloadLastDir, aURI, aValue) { + downloadLastDir.setFile(aURI, aValue); + return new Promise(resolve => executeSoon(resolve)); +} + +function clearHistoryAndWait() { + clearHistory(); + return new Promise(resolve => executeSoon(_ => executeSoon(resolve))); +} + +/* + * =================== + * Function with tests + * =================== + */ + +async function runTest() { + let { FileUtils } = ChromeUtils.importESModule( + "resource://gre/modules/FileUtils.sys.mjs" + ); + let { DownloadLastDir } = ChromeUtils.importESModule( + "resource://gre/modules/DownloadLastDir.sys.mjs" + ); + + let tmpDir = FileUtils.getDir("TmpD", []); + let dir1 = newDirectory(); + let dir2 = newDirectory(); + let dir3 = newDirectory(); + + let uri1 = Services.io.newURI("http://test1.com/"); + let uri2 = Services.io.newURI("http://test2.com/"); + let uri3 = Services.io.newURI("http://test3.com/"); + let uri4 = Services.io.newURI("http://test4.com/"); + + // cleanup functions registration + registerCleanupFunction(function () { + Services.prefs.clearUserPref("browser.download.lastDir.savePerSite"); + Services.prefs.clearUserPref("browser.download.lastDir"); + [dir1, dir2, dir3].forEach(dir => dir.remove(true)); + win.close(); + pbWin.close(); + }); + + function checkDownloadLastDir(gDownloadLastDir, aLastDir) { + is( + gDownloadLastDir.file.path, + aLastDir.path, + "gDownloadLastDir should point to the expected last directory" + ); + return getFile(gDownloadLastDir, uri1); + } + + function checkDownloadLastDirNull(gDownloadLastDir) { + is(gDownloadLastDir.file, null, "gDownloadLastDir should be null"); + return getFile(gDownloadLastDir, uri1); + } + + /* + * ================================ + * Create a regular and a PB window + * ================================ + */ + + let win = await createWindow({ private: false }); + let pbWin = await createWindow({ private: true }); + + let downloadLastDir = new DownloadLastDir(win); + let pbDownloadLastDir = new DownloadLastDir(pbWin); + + /* + * ================== + * Beginning of tests + * ================== + */ + + is( + typeof downloadLastDir, + "object", + "downloadLastDir should be a valid object" + ); + is(downloadLastDir.file, null, "LastDir pref should be null to start with"); + + // set up last dir + await setFile(downloadLastDir, null, tmpDir); + is( + downloadLastDir.file.path, + tmpDir.path, + "LastDir should point to the tmpDir" + ); + isnot( + downloadLastDir.file, + tmpDir, + "downloadLastDir.file should not be pointing to tmpDir" + ); + + // set uri1 to dir1, all should now return dir1 + // also check that a new object is returned + await setFile(downloadLastDir, uri1, dir1); + is( + downloadLastDir.file.path, + dir1.path, + "downloadLastDir should return dir1" + ); + isnot( + downloadLastDir.file, + dir1, + "downloadLastDir.file should not return dir1" + ); + is( + (await getFile(downloadLastDir, uri1)).path, + dir1.path, + "uri1 should return dir1" + ); // set in CPS + isnot( + await getFile(downloadLastDir, uri1), + dir1, + "getFile on uri1 should not return dir1" + ); + is( + (await getFile(downloadLastDir, uri2)).path, + dir1.path, + "uri2 should return dir1" + ); // fallback + isnot( + await getFile(downloadLastDir, uri2), + dir1, + "getFile on uri2 should not return dir1" + ); + is( + (await getFile(downloadLastDir, uri3)).path, + dir1.path, + "uri3 should return dir1" + ); // fallback + isnot( + await getFile(downloadLastDir, uri3), + dir1, + "getFile on uri3 should not return dir1" + ); + is( + (await getFile(downloadLastDir, uri4)).path, + dir1.path, + "uri4 should return dir1" + ); // fallback + isnot( + await getFile(downloadLastDir, uri4), + dir1, + "getFile on uri4 should not return dir1" + ); + + // set uri2 to dir2, all except uri1 should now return dir2 + await setFile(downloadLastDir, uri2, dir2); + is( + downloadLastDir.file.path, + dir2.path, + "downloadLastDir should point to dir2" + ); + is( + (await getFile(downloadLastDir, uri1)).path, + dir1.path, + "uri1 should return dir1" + ); // set in CPS + is( + (await getFile(downloadLastDir, uri2)).path, + dir2.path, + "uri2 should return dir2" + ); // set in CPS + is( + (await getFile(downloadLastDir, uri3)).path, + dir2.path, + "uri3 should return dir2" + ); // fallback + is( + (await getFile(downloadLastDir, uri4)).path, + dir2.path, + "uri4 should return dir2" + ); // fallback + + // set uri3 to dir3, all except uri1 and uri2 should now return dir3 + await setFile(downloadLastDir, uri3, dir3); + is( + downloadLastDir.file.path, + dir3.path, + "downloadLastDir should point to dir3" + ); + is( + (await getFile(downloadLastDir, uri1)).path, + dir1.path, + "uri1 should return dir1" + ); // set in CPS + is( + (await getFile(downloadLastDir, uri2)).path, + dir2.path, + "uri2 should return dir2" + ); // set in CPS + is( + (await getFile(downloadLastDir, uri3)).path, + dir3.path, + "uri3 should return dir3" + ); // set in CPS + is( + (await getFile(downloadLastDir, uri4)).path, + dir3.path, + "uri4 should return dir4" + ); // fallback + + // set uri1 to dir2, all except uri3 should now return dir2 + await setFile(downloadLastDir, uri1, dir2); + is( + downloadLastDir.file.path, + dir2.path, + "downloadLastDir should point to dir2" + ); + is( + (await getFile(downloadLastDir, uri1)).path, + dir2.path, + "uri1 should return dir2" + ); // set in CPS + is( + (await getFile(downloadLastDir, uri2)).path, + dir2.path, + "uri2 should return dir2" + ); // set in CPS + is( + (await getFile(downloadLastDir, uri3)).path, + dir3.path, + "uri3 should return dir3" + ); // set in CPS + is( + (await getFile(downloadLastDir, uri4)).path, + dir2.path, + "uri4 should return dir2" + ); // fallback + + await clearHistoryAndWait(); + + // check clearHistory removes all data + is(downloadLastDir.file, null, "clearHistory removes all data"); + is(await getFile(downloadLastDir, uri1), null, "uri1 should point to null"); + is(await getFile(downloadLastDir, uri2), null, "uri2 should point to null"); + is(await getFile(downloadLastDir, uri3), null, "uri3 should point to null"); + is(await getFile(downloadLastDir, uri4), null, "uri4 should point to null"); + + await setFile(downloadLastDir, null, tmpDir); + + // check data set outside PB mode is remembered + is( + (await checkDownloadLastDir(pbDownloadLastDir, tmpDir)).path, + tmpDir.path, + "uri1 should return the expected last directory" + ); + is( + (await checkDownloadLastDir(downloadLastDir, tmpDir)).path, + tmpDir.path, + "uri1 should return the expected last directory" + ); + await clearHistoryAndWait(); + + await setFile(downloadLastDir, uri1, dir1); + + // check data set using CPS outside PB mode is remembered + is( + (await checkDownloadLastDir(pbDownloadLastDir, dir1)).path, + dir1.path, + "uri1 should return the expected last directory" + ); + is( + (await checkDownloadLastDir(downloadLastDir, dir1)).path, + dir1.path, + "uri1 should return the expected last directory" + ); + await clearHistoryAndWait(); + + // check data set inside PB mode is forgotten + await setFile(pbDownloadLastDir, null, tmpDir); + + is( + (await checkDownloadLastDir(pbDownloadLastDir, tmpDir)).path, + tmpDir.path, + "uri1 should return the expected last directory" + ); + is( + await checkDownloadLastDirNull(downloadLastDir), + null, + "uri1 should return the expected last directory" + ); + + await clearHistoryAndWait(); + + // check data set using CPS inside PB mode is forgotten + await setFile(pbDownloadLastDir, uri1, dir1); + + is( + (await checkDownloadLastDir(pbDownloadLastDir, dir1)).path, + dir1.path, + "uri1 should return the expected last directory" + ); + is( + await checkDownloadLastDirNull(downloadLastDir), + null, + "uri1 should return the expected last directory" + ); + + // check data set outside PB mode but changed inside is remembered correctly + await setFile(downloadLastDir, uri1, dir1); + await setFile(pbDownloadLastDir, uri1, dir2); + is( + (await checkDownloadLastDir(pbDownloadLastDir, dir2)).path, + dir2.path, + "uri1 should return the expected last directory" + ); + is( + (await checkDownloadLastDir(downloadLastDir, dir1)).path, + dir1.path, + "uri1 should return the expected last directory" + ); + + /* + * ==================== + * Create new PB window + * ==================== + */ + + // check that the last dir store got cleared in a new PB window + pbWin.close(); + // And give it time to close + await new Promise(resolve => executeSoon(resolve)); + + pbWin = await createWindow({ private: true }); + pbDownloadLastDir = new DownloadLastDir(pbWin); + + is( + (await checkDownloadLastDir(pbDownloadLastDir, dir1)).path, + dir1.path, + "uri1 should return the expected last directory" + ); + + await clearHistoryAndWait(); + + // check clearHistory inside PB mode clears data outside PB mode + await setFile(pbDownloadLastDir, uri1, dir2); + + await clearHistoryAndWait(); + + is( + await checkDownloadLastDirNull(downloadLastDir), + null, + "uri1 should return the expected last directory" + ); + is( + await checkDownloadLastDirNull(pbDownloadLastDir), + null, + "uri1 should return the expected last directory" + ); + + // check that disabling CPS works + Services.prefs.setBoolPref("browser.download.lastDir.savePerSite", false); + + await setFile(downloadLastDir, uri1, dir1); + is(downloadLastDir.file.path, dir1.path, "LastDir should be set to dir1"); + is( + (await getFile(downloadLastDir, uri1)).path, + dir1.path, + "uri1 should return dir1" + ); + is( + (await getFile(downloadLastDir, uri2)).path, + dir1.path, + "uri2 should return dir1" + ); + is( + (await getFile(downloadLastDir, uri3)).path, + dir1.path, + "uri3 should return dir1" + ); + is( + (await getFile(downloadLastDir, uri4)).path, + dir1.path, + "uri4 should return dir1" + ); + + downloadLastDir.setFile(uri2, dir2); + is(downloadLastDir.file.path, dir2.path, "LastDir should be set to dir2"); + is( + (await getFile(downloadLastDir, uri1)).path, + dir2.path, + "uri1 should return dir2" + ); + is( + (await getFile(downloadLastDir, uri2)).path, + dir2.path, + "uri2 should return dir2" + ); + is( + (await getFile(downloadLastDir, uri3)).path, + dir2.path, + "uri3 should return dir2" + ); + is( + (await getFile(downloadLastDir, uri4)).path, + dir2.path, + "uri4 should return dir2" + ); + + Services.prefs.clearUserPref("browser.download.lastDir.savePerSite"); + + // check that passing null to setFile clears the stored value + await setFile(downloadLastDir, uri3, dir3); + is( + (await getFile(downloadLastDir, uri3)).path, + dir3.path, + "LastDir should be set to dir3" + ); + await setFile(downloadLastDir, uri3, null); + is(await getFile(downloadLastDir, uri3), null, "uri3 should return null"); + + await clearHistoryAndWait(); + + finish(); +} diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_about.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_about.js new file mode 100644 index 0000000000..af8bac9727 --- /dev/null +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_about.js @@ -0,0 +1,266 @@ +/* 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/. */ + +ChromeUtils.defineESModuleGetters(this, { + UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs", +}); + +ChromeUtils.defineLazyGetter(this, "UrlbarTestUtils", () => { + const { UrlbarTestUtils: module } = ChromeUtils.importESModule( + "resource://testing-common/UrlbarTestUtils.sys.mjs" + ); + module.init(this); + return module; +}); + +/** + * Clicks the given link and checks this opens the given URI in the new tab. + * + * This function does not return to the previous page. + */ +async function testLinkOpensUrl({ win, tab, elementId, expectedUrl }) { + let loadedPromise = BrowserTestUtils.waitForNewTab(win.gBrowser, url => + url.startsWith(expectedUrl) + ); + await SpecialPowers.spawn(tab, [elementId], async function (elemId) { + content.document.getElementById(elemId).click(); + }); + await loadedPromise; + is( + win.gBrowser.selectedBrowser.currentURI.spec, + expectedUrl, + `Clicking ${elementId} opened ${expectedUrl} in the same tab.` + ); +} + +let expectedEngineAlias; +let expectedIconURL; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.search.separatePrivateDefault", true], + // Enable suggestions in this test. Otherwise, the behaviour of the + // content search box changes. + ["browser.search.suggest.enabled", true], + ], + }); + + const originalPrivateDefault = await Services.search.getDefaultPrivate(); + // We have to use a built-in engine as we are currently hard-coding the aliases. + const privateEngine = await Services.search.getEngineByName("DuckDuckGo"); + await Services.search.setDefaultPrivate( + privateEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + expectedEngineAlias = privateEngine.aliases[0]; + expectedIconURL = privateEngine.getIconURL(); + + registerCleanupFunction(async () => { + await Services.search.setDefaultPrivate( + originalPrivateDefault, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + }); +}); + +/** + * Tests the private-browsing-myths link in "about:privatebrowsing". + */ +add_task(async function test_myths_link() { + Services.prefs.setCharPref("app.support.baseURL", "https://example.com/"); + registerCleanupFunction(function () { + Services.prefs.clearUserPref("app.support.baseURL"); + }); + + let { win, tab } = await openAboutPrivateBrowsing(); + + await testLinkOpensUrl({ + win, + tab, + elementId: "private-browsing-myths", + expectedUrl: "https://example.com/private-browsing-myths", + }); + + await BrowserTestUtils.closeWindow(win); +}); + +function urlBarHasHiddenFocus(win) { + return win.gURLBar.focused && !win.gURLBar.hasAttribute("focused"); +} + +function urlBarHasNormalFocus(win) { + return win.gURLBar.hasAttribute("focused"); +} + +/** + * Tests that we have the correct icon displayed. + */ +add_task(async function test_search_icon() { + let { win, tab } = await openAboutPrivateBrowsing(); + + await SpecialPowers.spawn(tab, [expectedIconURL], async function (iconURL) { + is( + content.document.body.getAttribute("style"), + `--newtab-search-icon: url(${iconURL});`, + "Should have the correct icon URL for the logo" + ); + }); + + await BrowserTestUtils.closeWindow(win); +}); + +/** + * Tests the search hand-off on character keydown in "about:privatebrowsing". + */ +add_task(async function test_search_handoff_on_keydown() { + let { win, tab } = await openAboutPrivateBrowsing(); + + await SpecialPowers.spawn(tab, [], async function () { + let btn = content.document.getElementById("search-handoff-button"); + btn.click(); + ok(btn.classList.contains("focused"), "in-content search has focus styles"); + }); + ok(urlBarHasHiddenFocus(win), "Urlbar has hidden focus"); + + // Expect two searches, one to enter search mode and then another in search + // mode. + let searchPromise = UrlbarTestUtils.promiseSearchComplete(win); + + await new Promise(r => EventUtils.synthesizeKey("f", {}, win, r)); + await SpecialPowers.spawn(tab, [], async function () { + ok( + content.document + .getElementById("search-handoff-button") + .classList.contains("disabled"), + "in-content search is disabled" + ); + }); + await searchPromise; + ok(urlBarHasNormalFocus(win), "Urlbar has normal focus"); + is(win.gURLBar.value, "f", "url bar has search text"); + + // Close the popup. + await UrlbarTestUtils.promisePopupClose(win); + + // Hitting ESC should reshow the in-content search + await new Promise(r => EventUtils.synthesizeKey("KEY_Escape", {}, win, r)); + await SpecialPowers.spawn(tab, [], async function () { + ok( + !content.document + .getElementById("search-handoff-button") + .classList.contains("disabled"), + "in-content search is not disabled" + ); + }); + + await BrowserTestUtils.closeWindow(win); +}); + +/** + * Tests the search hand-off on composition start in "about:privatebrowsing". + */ +add_task(async function test_search_handoff_on_composition_start() { + let { win, tab } = await openAboutPrivateBrowsing(); + + await SpecialPowers.spawn(tab, [], async function () { + content.document.getElementById("search-handoff-button").click(); + }); + ok(urlBarHasHiddenFocus(win), "Urlbar has hidden focus"); + await new Promise(r => + EventUtils.synthesizeComposition({ type: "compositionstart" }, win, r) + ); + ok(urlBarHasNormalFocus(win), "Urlbar has normal focus"); + + await BrowserTestUtils.closeWindow(win); +}); + +/** + * Tests the search hand-off on paste in "about:privatebrowsing". + */ +add_task(async function test_search_handoff_on_paste() { + let { win, tab } = await openAboutPrivateBrowsing(); + + await SpecialPowers.spawn(tab, [], async function () { + content.document.getElementById("search-handoff-button").click(); + }); + ok(urlBarHasHiddenFocus(win), "Urlbar has hidden focus"); + var helper = SpecialPowers.Cc[ + "@mozilla.org/widget/clipboardhelper;1" + ].getService(SpecialPowers.Ci.nsIClipboardHelper); + helper.copyString("words"); + + // Expect two searches, one to enter search mode and then another in search + // mode. + let searchPromise = UrlbarTestUtils.promiseSearchComplete(win); + + await new Promise(r => + EventUtils.synthesizeKey("v", { accelKey: true }, win, r) + ); + + await searchPromise; + + ok(urlBarHasNormalFocus(win), "Urlbar has normal focus"); + is(win.gURLBar.value, "words", "Urlbar has search text"); + + await BrowserTestUtils.closeWindow(win); +}); + +/** + * Tests that handoff enters search mode when suggestions are disabled. + */ +add_task(async function test_search_handoff_search_mode() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.suggest.searches", false]], + }); + + let { win, tab } = await openAboutPrivateBrowsing(); + + await SpecialPowers.spawn(tab, [], async function () { + let btn = content.document.getElementById("search-handoff-button"); + btn.click(); + ok(btn.classList.contains("focused"), "in-content search has focus styles"); + }); + ok(urlBarHasHiddenFocus(win), "Urlbar has hidden focus"); + + // Expect two searches, one to enter search mode and then another in search + // mode. + let searchPromise = UrlbarTestUtils.promiseSearchComplete(win); + + await new Promise(r => EventUtils.synthesizeKey("f", {}, win, r)); + await SpecialPowers.spawn(tab, [], async function () { + ok( + content.document + .getElementById("search-handoff-button") + .classList.contains("disabled"), + "in-content search is disabled" + ); + }); + await searchPromise; + ok(urlBarHasNormalFocus(win), "Urlbar has normal focus"); + await UrlbarTestUtils.assertSearchMode(win, { + engineName: "DuckDuckGo", + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + entry: "handoff", + }); + is(win.gURLBar.value, "f", "url bar has search text"); + + // Close the popup. + await UrlbarTestUtils.exitSearchMode(win); + await UrlbarTestUtils.promisePopupClose(win); + + // Hitting ESC should reshow the in-content search + await new Promise(r => EventUtils.synthesizeKey("KEY_Escape", {}, win, r)); + await SpecialPowers.spawn(tab, [], async function () { + ok( + !content.document + .getElementById("search-handoff-button") + .classList.contains("disabled"), + "in-content search is not disabled" + ); + }); + + await BrowserTestUtils.closeWindow(win); + await SpecialPowers.popPrefEnv(); +}); diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_aboutSessionRestore.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_aboutSessionRestore.js new file mode 100644 index 0000000000..a838a7e52d --- /dev/null +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_aboutSessionRestore.js @@ -0,0 +1,25 @@ +/* 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/. */ + +// This test checks that the session restore button from about:sessionrestore +// is disabled in private mode +add_task(async function testNoSessionRestoreButton() { + // Opening, then closing, a private window shouldn't create session data. + (await BrowserTestUtils.openNewBrowserWindow({ private: true })).close(); + + let win = await BrowserTestUtils.openNewBrowserWindow({ private: true }); + let tab = BrowserTestUtils.addTab(win.gBrowser, "about:sessionrestore"); + let browser = tab.linkedBrowser; + + await BrowserTestUtils.browserLoaded(browser); + + await SpecialPowers.spawn(browser, [], async function () { + Assert.ok( + content.document.getElementById("errorTryAgain").disabled, + "The Restore about:sessionrestore button should be disabled" + ); + }); + + win.close(); +}); diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_about_cookie_banners_promo.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_about_cookie_banners_promo.js new file mode 100644 index 0000000000..2f9c87e1e1 --- /dev/null +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_about_cookie_banners_promo.js @@ -0,0 +1,107 @@ +const { ASRouter } = ChromeUtils.importESModule( + "resource:///modules/asrouter/ASRouter.sys.mjs" +); + +const promoImgSrc = "chrome://browser/content/assets/cookie-banners-begone.svg"; + +async function resetState() { + await Promise.all([ASRouter.resetMessageState(), ASRouter.unblockAll()]); +} + +add_setup(async function setup() { + registerCleanupFunction(resetState); + await resetState(); +}); + +add_task(async function test_cookie_banners_promo_user_set_prefs() { + await resetState(); + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.promo.cookiebanners.enabled", true], + // The message's targeting is looking for the following prefs not being 0 + ["cookiebanners.service.mode", 0], + ["cookiebanners.service.mode.privateBrowsing", 0], + ], + }); + await ASRouter.onPrefChange(); + + const { win, tab } = await openTabAndWaitForRender(); + + await SpecialPowers.spawn(tab, [promoImgSrc], async function (imgSrc) { + const promoImage = content.document.querySelector( + ".promo-image-large > img" + ); + Assert.notStrictEqual( + promoImage?.src, + imgSrc, + "Cookie banner reduction promo is not shown" + ); + }); + + await BrowserTestUtils.closeWindow(win); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function test_cookie_banners_promo() { + await resetState(); + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.promo.cookiebanners.enabled", true], + ["cookiebanners.service.mode.privateBrowsing", 1], + ], + }); + await ASRouter.onPrefChange(); + + const sandbox = sinon.createSandbox(); + const expectedUrl = Services.urlFormatter.formatURL( + "https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/cookie-banner-reduction" + ); + + const { win, tab } = await openTabAndWaitForRender(); + let triedToOpenTab = new Promise(resolve => { + sandbox.stub(win, "openLinkIn").callsFake((url, where) => { + is(url, expectedUrl, "The link should open the expected URL"); + is( + where, + "tabshifted", + "The link should open the expected URL in a new foreground tab" + ); + resolve(); + }); + }); + + await SpecialPowers.spawn(tab, [promoImgSrc], async function (imgSrc) { + const promoImage = content.document.querySelector( + ".promo-image-large > img" + ); + Assert.strictEqual( + promoImage?.src, + imgSrc, + "Cookie banner reduction promo is shown" + ); + let linkEl = content.document.getElementById("private-browsing-promo-link"); + linkEl.click(); + }); + + await triedToOpenTab; + sandbox.restore(); + + ok(true, "The link was clicked and the new tab opened"); + + let { win: win2, tab: tab2 } = await openTabAndWaitForRender(); + + await SpecialPowers.spawn(tab2, [promoImgSrc], async function (imgSrc) { + const promoImage = content.document.querySelector( + ".promo-image-large > img" + ); + Assert.notStrictEqual( + promoImage?.src, + imgSrc, + "Cookie banner reduction promo is no longer shown after clicking the link" + ); + }); + + await BrowserTestUtils.closeWindow(win2); + await BrowserTestUtils.closeWindow(win); + await SpecialPowers.popPrefEnv(); +}); diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_about_default_pin_promo.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_about_default_pin_promo.js new file mode 100644 index 0000000000..bc62556b12 --- /dev/null +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_about_default_pin_promo.js @@ -0,0 +1,110 @@ +/* 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/. */ + +const { sinon } = ChromeUtils.importESModule( + "resource://testing-common/Sinon.sys.mjs" +); + +const sandbox = sinon.createSandbox(); + +add_setup(async function () { + ASRouter.resetMessageState(); + await SpecialPowers.pushPrefEnv({ + set: [["browser.promo.pin.enabled", true]], + }); + await ASRouter.onPrefChange(); + // Stub out the doesAppNeedPin to true so that Pin Promo targeting evaluates true + + sandbox.stub(ShellService, "doesAppNeedPin").withArgs(true).returns(true); + registerCleanupFunction(async () => { + sandbox.restore(); + }); +}); + +add_task(async function test_pin_promo() { + let { win: win1, tab: tab1 } = await openTabAndWaitForRender(); + + await SpecialPowers.spawn(tab1, [], async function () { + const promoContainer = content.document.querySelector(".promo"); + const promoHeader = content.document.getElementById("promo-header"); + + ok(promoContainer, "Pin promo is shown"); + is( + promoHeader.getAttribute("data-l10n-id"), + "about-private-browsing-pin-promo-header", + "Correct default values are shown" + ); + }); + + let { win: win2 } = await openTabAndWaitForRender(); + let { win: win3 } = await openTabAndWaitForRender(); + let { win: win4, tab: tab4 } = await openTabAndWaitForRender(); + + await SpecialPowers.spawn(tab4, [], async function () { + is( + content.document.getElementById(".private-browsing-promo-link"), + null, + "should no longer render the promo after 3 impressions" + ); + }); + + await BrowserTestUtils.closeWindow(win1); + await BrowserTestUtils.closeWindow(win2); + await BrowserTestUtils.closeWindow(win3); + await BrowserTestUtils.closeWindow(win4); +}); + +add_task(async function test_pin_promo_mr2022_holdback() { + ASRouter.resetMessageState(); + // Set majorRelease2022 feature onboarding variable fallback pref + // for inMr2022Holdback targeting to evaluate true + await SpecialPowers.pushPrefEnv({ + set: [["browser.majorrelease.onboarding", false]], + }); + await ASRouter.onPrefChange(); + let { win: win1, tab: tab1 } = await openTabAndWaitForRender(); + + await SpecialPowers.spawn(tab1, [], async function () { + const promoContainer = content.document.querySelector(".promo"); + const promoButton = content.document.querySelector( + "#private-browsing-promo-link" + ); + + ok(promoContainer, "Promo is shown"); + + Assert.equal( + promoButton.getAttribute("data-l10n-id"), + "about-private-browsing-focus-promo-cta", + "Pin Promo not shown for holdback user" + ); + }); + + await BrowserTestUtils.closeWindow(win1); +}); + +add_task(async function test_pin_promo_mr2022_not_holdback() { + ASRouter.resetMessageState(); + // Set majorRelease2022 feature onboarding variable fallback pref + // for inMr2022Holdback targeting to evaluate false + await SpecialPowers.pushPrefEnv({ + set: [["browser.majorrelease.onboarding", true]], + }); + await ASRouter.onPrefChange(); + let { win: win1, tab: tab1 } = await openTabAndWaitForRender(); + + await SpecialPowers.spawn(tab1, [], async function () { + const promoContainer = content.document.querySelector(".promo"); + const promoHeader = content.document.getElementById("promo-header"); + + ok(promoContainer, "Promo is shown"); + + is( + promoHeader.getAttribute("data-l10n-id"), + "about-private-browsing-pin-promo-header", + "Pin Promo is shown" + ); + }); + + await BrowserTestUtils.closeWindow(win1); +}); diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_about_default_promo.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_about_default_promo.js new file mode 100644 index 0000000000..82c6fea011 --- /dev/null +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_about_default_promo.js @@ -0,0 +1,224 @@ +/* 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/. */ + +const PromoInfo = { + FOCUS: { enabledPref: "browser.promo.focus.enabled" }, + VPN: { enabledPref: "browser.vpn_promo.enabled" }, + PIN: { enabledPref: "browser.promo.pin.enabled" }, + COOKIE_BANNERS: { enabledPref: "browser.promo.cookiebanners.enabled" }, +}; + +const sandbox = sinon.createSandbox(); + +async function resetState() { + await Promise.all([ + ASRouter.resetMessageState(), + ASRouter.resetGroupsState(), + ASRouter.unblockAll(), + sandbox.restore(), + ]); +} + +add_setup(async function () { + registerCleanupFunction(resetState); + await resetState(); + await SpecialPowers.pushPrefEnv({ + set: [["browser.promo.pin.enabled", false]], + }); + await ASRouter.onPrefChange(); +}); + +add_task(async function test_privatebrowsing_asrouter_messages_state() { + await resetState(); + let pinPromoMessage = ASRouter.state.messages.find( + m => m.id === "PB_NEWTAB_PIN_PROMO" + ); + Assert.ok(pinPromoMessage, "Pin Promo message found"); + + const initialMessages = JSON.parse(JSON.stringify(ASRouter.state.messages)); + + let { win, tab } = await openTabAndWaitForRender(); + + await SpecialPowers.spawn(tab, [], async function () { + const promoContainer = content.document.querySelector(".promo"); + ok(promoContainer, "Focus promo is shown"); + }); + + Assert.equal( + ASRouter.state.messages.filter(m => m.id === "PB_NEWTAB_PIN_PROMO").length, + 0, + "Pin Promo message removed from state when Promotype Pin is disabled" + ); + + for (let msg of initialMessages) { + let shouldPersist = + msg.template !== "pb_newtab" || + Services.prefs.getBoolPref( + PromoInfo[msg.content?.promoType]?.enabledPref, + true + ); + Assert.equal( + !!ASRouter.state.messages.find(m => m.id === msg.id), + shouldPersist, + shouldPersist + ? "Message persists in ASRouter state" + : "Promo message with disabled promoType removed from ASRouter state" + ); + } + await BrowserTestUtils.closeWindow(win); +}); + +add_task(async function test_default_promo() { + await resetState(); + + let { win: win1, tab: tab1 } = await openTabAndWaitForRender(); + + await SpecialPowers.spawn(tab1, [], async function () { + const promoContainer = content.document.querySelector(".promo"); // container which is present if promo is enabled and should show + const promoHeader = content.document.getElementById("promo-header"); + + ok(promoContainer, "Focus promo is shown"); + is( + promoHeader.getAttribute("data-l10n-id"), + "about-private-browsing-focus-promo-header-c", + "Correct default values are shown" + ); + }); + + let { win: win2 } = await openTabAndWaitForRender(); + let { win: win3 } = await openTabAndWaitForRender(); + + let { win: win4, tab: tab4 } = await openTabAndWaitForRender(); + + await SpecialPowers.spawn(tab4, [], async function () { + is( + content.document.querySelector(".promo button"), + null, + "should no longer render the promo after 3 impressions" + ); + }); + + await BrowserTestUtils.closeWindow(win1); + await BrowserTestUtils.closeWindow(win2); + await BrowserTestUtils.closeWindow(win3); + await BrowserTestUtils.closeWindow(win4); +}); + +// Verify that promos are correctly removed if blocked in another tab. +// See handlePromoOnPreload() in aboutPrivateBrowsing.js +add_task(async function test_remove_promo_from_prerendered_tab_if_blocked() { + await resetState(); + + const { win, tab: tab1 } = await openTabAndWaitForRender(); + + await SpecialPowers.spawn(tab1, [], async function () { + // container which is present if promo message is not blocked + const promoContainer = content.document.querySelector(".promo"); + ok(promoContainer, "Focus promo is shown in tab 1"); + }); + + // Open a new background tab (tab 2) while the promo message is unblocked + win.openTrustedLinkIn(win.BROWSER_NEW_TAB_URL, "tabshifted"); + + // Block the promo in tab 1 + await SpecialPowers.spawn(tab1, [], async function () { + content.document.getElementById("dismiss-btn").click(); + await ContentTaskUtils.waitForCondition(() => { + return !content.document.querySelector(".promo"); + }, "The promo container is removed."); + }); + + // Switch to tab 2, invoking the `visibilitychange` handler in + // handlePromoOnPreload() + await BrowserTestUtils.switchTab(win.gBrowser, win.gBrowser.tabs[1]); + + // Verify that the promo has now been removed from tab 2 + await SpecialPowers.spawn( + win.gBrowser.tabs[1].linkedBrowser, + [], + // The timing may be weird in Chaos Mode, so wait for it to be removed + // instead of a single assertion. + async function () { + await ContentTaskUtils.waitForCondition( + () => !content.document.querySelector(".promo"), + "Focus promo is not shown in a new tab after being dismissed in another tab" + ); + } + ); + + await BrowserTestUtils.closeWindow(win); +}); + +// Test that some default content is rendered while waiting for ASRouter to +// return a message. +add_task(async function test_default_content_deferred_message_load() { + await resetState(); + + let messageRequestedPromiseResolver; + const messageRequestedPromise = new Promise(resolve => { + messageRequestedPromiseResolver = resolve; + }); + let messageReadyPromiseResolver; + const messageReadyPromise = new Promise(resolve => { + messageReadyPromiseResolver = resolve; + }); + // Force ASRouter to "hang" until we resolve the promise so we can test what + // happens when there is a delay in loading the message. + const sendMessageStub = sandbox + .stub(ASRouter, "sendPBNewTabMessage") + .callsFake(async (...args) => { + messageRequestedPromiseResolver(); + await messageReadyPromise; + return sendMessageStub.wrappedMethod.apply(ASRouter, args); + }); + + const { win, tab } = await openAboutPrivateBrowsing(); + await messageRequestedPromise; + + await SpecialPowers.spawn(tab, [], async function () { + const promoContainer = content.document.querySelector(".promo"); + ok( + promoContainer && !promoContainer.classList.contains("promo-visible"), + "Focus promo is hidden but not removed" + ); + const infoContainer = content.document.querySelector(".info"); + ok(infoContainer && !infoContainer.hidden, "Info container is shown"); + const infoTitle = content.document.getElementById("info-title"); + ok(infoTitle && infoTitle.hidden, "Info title is hidden"); + const infoBody = content.document.getElementById("info-body"); + ok(infoBody, "Info body is shown"); + is( + infoBody.getAttribute("data-l10n-id"), + "about-private-browsing-info-description-private-window", + "Info body has the correct Fluent id" + ); + await ContentTaskUtils.waitForCondition( + () => infoBody.textContent, + "Info body has been translated" + ); + const infoLink = content.document.getElementById("private-browsing-myths"); + ok(infoLink, "Info link is shown"); + is( + infoLink.getAttribute("data-l10n-id"), + "about-private-browsing-learn-more-link", + "Info link has the correct Fluent id" + ); + await ContentTaskUtils.waitForCondition( + () => infoLink.textContent && infoLink.href, + "Info body has been translated" + ); + }); + + messageReadyPromiseResolver(); + await messageReadyPromise; + + await SpecialPowers.spawn(tab, [], async function () { + await ContentTaskUtils.waitForCondition(() => { + const promoContainer = content.document.querySelector(".promo"); + return promoContainer?.classList.contains("promo-visible"); + }, "The promo container is shown."); + }); + + await BrowserTestUtils.closeWindow(win); +}); diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_about_focus_promo.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_about_focus_promo.js new file mode 100644 index 0000000000..7e5c6540b0 --- /dev/null +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_about_focus_promo.js @@ -0,0 +1,89 @@ +const { Region } = ChromeUtils.importESModule( + "resource://gre/modules/Region.sys.mjs" +); +const { ASRouter } = ChromeUtils.importESModule( + "resource:///modules/asrouter/ASRouter.sys.mjs" +); + +const initialHomeRegion = Region._home; +const intialCurrentRegion = Region._current; +const initialLocale = Services.locale.appLocaleAsBCP47; + +// Helper to run tests for specific regions +async function setupRegions(home, current) { + Region._setHomeRegion(home || ""); + Region._setCurrentRegion(current || ""); +} + +// Helper to run tests for specific locales +function setLocale(locale) { + Services.locale.availableLocales = [locale]; + Services.locale.requestedLocales = [locale]; +} + +add_task(async function test_focus_promo_in_allowed_region() { + ASRouter.resetMessageState(); + + const allowedRegion = "ES"; // Spain + setupRegions(allowedRegion, allowedRegion); + + const { win, tab } = await openTabAndWaitForRender(); + + await SpecialPowers.spawn(tab, [], async function () { + const promoContainer = content.document.querySelector(".promo"); // container which is present if promo is enabled and should show + + ok(promoContainer, "Focus promo is shown for allowed region"); + }); + + await BrowserTestUtils.closeWindow(win); + setupRegions(initialHomeRegion, intialCurrentRegion); // revert changes to regions +}); + +add_task(async function test_focus_promo_in_disallowed_region() { + ASRouter.resetMessageState(); + + const disallowedRegion = "CN"; // China + setupRegions(disallowedRegion); + + const { win, tab } = await openTabAndWaitForRender(); + + await SpecialPowers.spawn(tab, [], async function () { + const promoContainer = content.document.querySelector(".promo"); // container which is removed if promo is disabled and/or should not show + + ok(!promoContainer, "Focus promo is not shown for disallowed region"); + }); + + await BrowserTestUtils.closeWindow(win); + setupRegions(initialHomeRegion, intialCurrentRegion); // revert changes to regions +}); + +add_task( + async function test_klar_promo_in_certain_regions_with_English_locale() { + const testLocale = "en-US"; // US English + setLocale(testLocale); + + const testRegion = async region => { + setupRegions(region); + ASRouter.resetMessageState(); + const { win, tab } = await openTabAndWaitForRender(); + await SpecialPowers.spawn(tab, [], async function () { + const buttonText = content.document.querySelector( + "#private-browsing-promo-link" + ).textContent; + Assert.equal( + buttonText, + "Download Firefox Klar", + "The promo button text reads 'Download Firefox Klar'" + ); + }); + await BrowserTestUtils.closeWindow(win); + }; + + await testRegion("AT"); // Austria + await testRegion("DE"); // Germany + await testRegion("CH"); // Switzerland + + setupRegions(initialHomeRegion, intialCurrentRegion); // revert changes to regions + setLocale(initialLocale); // revert changes to locale + } +); diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_about_nimbus.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_about_nimbus.js new file mode 100644 index 0000000000..489c9c91b2 --- /dev/null +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_about_nimbus.js @@ -0,0 +1,459 @@ +/* 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/. */ + +requestLongerTimeout(2); + +add_task(async function test_experiment_plain_text() { + const defaultMessageContent = (await PanelTestProvider.getMessages()).find( + m => m.template === "pb_newtab" + ).content; + let doExperimentCleanup = await setupMSExperimentWithMessage({ + id: "PB_NEWTAB_MESSAGING_SYSTEM", + template: "pb_newtab", + content: { + ...defaultMessageContent, + infoTitle: "Hello world", + infoTitleEnabled: true, + infoBody: "This is some text", + infoLinkText: "This is a link", + infoIcon: "chrome://branding/content/about-logo.png", + promoTitle: "Promo title", + promoLinkText: "Promo link", + promoLinkType: "link", + promoButton: { + action: { + type: "OPEN_URL", + data: { + args: "https://example.com", + where: "tabshifted", + }, + }, + }, + }, + // Priority ensures this message is picked over the one in + // OnboardingMessageProvider + priority: 5, + targeting: "true", + }); + + let { win, tab } = await openTabAndWaitForRender(); + + await SpecialPowers.spawn(tab, [], async function () { + const infoContainer = content.document.querySelector(".info"); + const infoTitle = content.document.getElementById("info-title"); + const infoBody = content.document.getElementById("info-body"); + const infoLink = content.document.getElementById("private-browsing-myths"); + const promoText = content.document.getElementById( + "private-browsing-promo-text" + ); + const promoLink = content.document.getElementById( + "private-browsing-promo-link" + ); + + // Check experiment values are rendered + ok(!infoContainer.hidden, ".info container should be visible"); + ok( + infoContainer.style.backgroundImage.includes( + "chrome://branding/content/about-logo.png" + ), + "should render icon" + ); + is(infoTitle.textContent, "Hello world", "should render infoTitle"); + is(infoBody.textContent, "This is some text", "should render infoBody"); + is(infoLink.textContent, "This is a link", "should render infoLink"); + is(promoText.textContent, "Promo title", "should render promoTitle"); + is(promoLink.textContent, "Promo link", "should render promoLinkText"); + }); + + await BrowserTestUtils.closeWindow(win); + await doExperimentCleanup(); +}); + +add_task(async function test_experiment_info_disabled() { + let doExperimentCleanup = await setupMSExperimentWithMessage({ + id: "PB_NEWTAB_MESSAGING_SYSTEM", + template: "pb_newtab", + content: { + infoEnabled: false, + }, + // Priority ensures this message is picked over the one in + // OnboardingMessageProvider + priority: 5, + targeting: "true", + }); + + let { win, tab } = await openTabAndWaitForRender(); + + await SpecialPowers.spawn(tab, [], async function () { + ok( + content.document.querySelector(".info").hidden, + "should hide .info element" + ); + }); + + await BrowserTestUtils.closeWindow(win); + await doExperimentCleanup(); +}); + +add_task(async function test_experiment_promo_disabled() { + let doExperimentCleanup = await setupMSExperimentWithMessage({ + id: "PB_NEWTAB_MESSAGING_SYSTEM", + template: "pb_newtab", + content: { + promoEnabled: false, + }, + // Priority ensures this message is picked over the one in + // OnboardingMessageProvider + priority: 5, + targeting: "true", + }); + + let { win, tab } = await openTabAndWaitForRender(); + + await SpecialPowers.spawn(tab, [], async function () { + is( + content.document.querySelector(".promo"), + undefined, + "should remove .promo element" + ); + }); + + await BrowserTestUtils.closeWindow(win); + await doExperimentCleanup(); +}); + +add_task(async function test_experiment_format_urls() { + const LOCALE = Services.locale.appLocaleAsBCP47; + let doExperimentCleanup = await setupMSExperimentWithMessage({ + id: "PB_NEWTAB_MESSAGING_SYSTEM", + template: "pb_newtab", + content: { + infoEnabled: true, + promoEnabled: true, + infoLinkUrl: "http://foo.mozilla.com/%LOCALE%", + promoButton: { + action: { + data: { + args: "http://bar.mozilla.com/%LOCALE%", + where: "tabshifted", + }, + type: "OPEN_URL", + }, + }, + }, + // Priority ensures this message is picked over the one in + // OnboardingMessageProvider + priority: 5, + targeting: "true", + }); + + let { win, tab } = await openTabAndWaitForRender(); + + await SpecialPowers.spawn(tab, [LOCALE], async function (locale) { + is( + content.document.querySelector(".info a").getAttribute("href"), + "http://foo.mozilla.com/" + locale, + "should format the infoLinkUrl url" + ); + + ok( + content.document.querySelector(".promo button"), + "should render promo button" + ); + }); + + await BrowserTestUtils.closeWindow(win); + await doExperimentCleanup(); +}); + +add_task(async function test_experiment_click_info_telemetry() { + let doExperimentCleanup = await setupMSExperimentWithMessage({ + id: "PB_NEWTAB_MESSAGING_SYSTEM_CLICK_INFO_TELEM", + template: "pb_newtab", + content: { + infoEnabled: true, + infoLinkUrl: "http://example.com", + }, + // Priority ensures this message is picked over the one in + // OnboardingMessageProvider + priority: 5, + targeting: "true", + }); + + // Required for `mach test --verify` + Services.telemetry.clearEvents(); + + let { win, tab } = await openTabAndWaitForRender(); + + await SpecialPowers.spawn(tab, [], () => { + const el = content.document.querySelector(".info a"); + el.click(); + }); + + let event = await waitForTelemetryEvent("aboutprivatebrowsing"); + + ok( + event[2] == "click" && event[3] == "info_link", + "recorded telemetry for info link" + ); + + await BrowserTestUtils.closeWindow(win); + await doExperimentCleanup(); +}); + +add_task(async function test_experiment_click_promo_telemetry() { + let doExperimentCleanup = await setupMSExperimentWithMessage({ + id: `PB_NEWTAB_MESSAGING_SYSTEM_PROMO_TELEM_${Math.random()}`, + template: "pb_newtab", + content: { + promoEnabled: true, + promoLinkType: "link", + promoButton: { + action: { + type: "OPEN_URL", + data: { + args: "https://example.com", + where: "tabshifted", + }, + }, + }, + }, + // Priority ensures this message is picked over the one in + // OnboardingMessageProvider + priority: 5, + targeting: "true", + }); + + let { win, tab } = await openTabAndWaitForRender(); + + Services.telemetry.clearEvents(); + + await SpecialPowers.spawn(tab, [], () => { + is( + content.document + .querySelector(".promo-cta button") + .classList.contains("promo-link"), + true, + "Should have a button styled as a link" + ); + + const el = content.document.querySelector(".promo button"); + el.click(); + }); + + let event = await waitForTelemetryEvent("aboutprivatebrowsing"); + + ok( + event[2] == "click" && event[3] == "promo_link", + "recorded telemetry for promo link" + ); + + await BrowserTestUtils.closeWindow(win); + await doExperimentCleanup(); +}); + +add_task(async function test_experiment_bottom_promo() { + const defaultMessageContent = (await PanelTestProvider.getMessages()).find( + m => m.template === "pb_newtab" + ).content; + + let doExperimentCleanup = await setupMSExperimentWithMessage({ + id: "PB_NEWTAB_MESSAGING_SYSTEM", + template: "pb_newtab", + content: { + ...defaultMessageContent, + promoEnabled: true, + promoLinkType: "button", + promoSectionStyle: "bottom", + promoHeader: "Need more privacy?", + infoTitleEnabled: true, + promoTitleEnabled: false, + promoImageLarge: "", + promoImageSmall: "chrome://browser/content/assets/vpn-logo.svg", + promoButton: { + action: { + data: { + args: "http://bar.example.com/%LOCALE%", + where: "tabshifted", + }, + type: "OPEN_URL", + }, + }, + }, + // Priority ensures this message is picked over the one in + // OnboardingMessageProvider + priority: 5, + targeting: "true", + }); + + let { win, tab } = await openTabAndWaitForRender(); + + await SpecialPowers.spawn(tab, [], async function () { + is( + content.document + .querySelector(".promo-cta button") + .classList.contains("primary"), + true, + "Should have a button CTA" + ); + is( + content.document.querySelector(".promo-image-small img").src, + "chrome://browser/content/assets/vpn-logo.svg", + "Should have logo image" + ); + ok( + content.document.querySelector(".promo.bottom"), + "Should have .bottom for the promo section" + ); + const infoTitle = content.document.getElementById("info-title"); + ok( + infoTitle && !infoTitle.hidden, + "Should render info title if infoTitleEnabled is true" + ); + ok( + !content.document.querySelector("#private-browsing-promo-text"), + "Should not render promo title if promoTitleEnabled is false" + ); + }); + + await BrowserTestUtils.closeWindow(win); + await doExperimentCleanup(); +}); + +add_task(async function test_experiment_below_search_promo() { + const defaultMessageContent = (await PanelTestProvider.getMessages()).find( + m => m.template === "pb_newtab" + ).content; + let doExperimentCleanup = await setupMSExperimentWithMessage({ + id: "PB_NEWTAB_MESSAGING_SYSTEM", + template: "pb_newtab", + content: { + ...defaultMessageContent, + promoEnabled: true, + promoLinkType: "button", + promoSectionStyle: "below-search", + promoHeader: "Need more privacy?", + promoTitle: + "Mozilla VPN. Security, reliability and speed — on every device, anywhere you go.", + promoImageLarge: "chrome://browser/content/assets/moz-vpn.svg", + promoImageSmall: "chrome://browser/content/assets/vpn-logo.svg", + infoTitleEnabled: false, + promoButton: { + action: { + data: { + args: "https://foo.example.com", + where: "tabshifted", + }, + type: "OPEN_URL", + }, + }, + }, + // Priority ensures this message is picked over the one in + // OnboardingMessageProvider + priority: 5, + targeting: "true", + }); + + let { win, tab } = await openTabAndWaitForRender(); + + await SpecialPowers.spawn(tab, [], async function () { + is( + content.document + .querySelector(".promo-cta button") + .classList.contains("primary"), + true, + "Should have a button CTA" + ); + is( + content.document.querySelector(".promo-image-small img").src, + "chrome://browser/content/assets/vpn-logo.svg", + "Should have logo image" + ); + is( + content.document.querySelector(".promo-image-large img").src, + "chrome://browser/content/assets/moz-vpn.svg", + "Should have a product image" + ); + ok( + content.document.querySelector(".promo.below-search"), + "Should have .below-search for the promo section" + ); + ok( + content.document.getElementById("info-title").hidden, + "Should not render info title if infoTitleEnabled is false" + ); + ok( + content.document.querySelector("#private-browsing-promo-text"), + "Should render promo title if promoTitleEnabled is true" + ); + }); + + await BrowserTestUtils.closeWindow(win); + await doExperimentCleanup(); +}); + +add_task(async function test_experiment_top_promo() { + const defaultMessageContent = (await PanelTestProvider.getMessages()).find( + m => m.template === "pb_newtab" + ).content; + let doExperimentCleanup = await setupMSExperimentWithMessage({ + id: `PB_NEWTAB_MESSAGING_SYSTEM_DISMISS_${Math.random()}`, + template: "pb_newtab", + content: { + ...defaultMessageContent, + promoEnabled: true, + promoLinkType: "button", + promoSectionStyle: "top", + promoHeader: "Need more privacy?", + promoTitle: + "Mozilla VPN. Security, reliability and speed — on every device, anywhere you go.", + promoImageLarge: "chrome://browser/content/assets/moz-vpn.svg", + promoImageSmall: "chrome://browser/content/assets/vpn-logo.svg", + infoTitleEnabled: false, + promoButton: { + action: { + data: { + args: "https://foo.example.com", + where: "tabshifted", + }, + type: "OPEN_URL", + }, + }, + }, + // Priority ensures this message is picked over the one in + // OnboardingMessageProvider + priority: 5, + targeting: "true", + }); + + let { win, tab } = await openTabAndWaitForRender(); + + await SpecialPowers.spawn(tab, [], async function () { + is( + content.document.querySelector(".promo-image-small img").src, + "chrome://browser/content/assets/vpn-logo.svg", + "Should have logo image" + ); + is( + content.document.querySelector(".promo-image-large img").src, + "chrome://browser/content/assets/moz-vpn.svg", + "Should have a product image" + ); + ok( + content.document.querySelector(".promo.top"), + "Should have .below-search for the promo section" + ); + ok( + content.document.getElementById("info-title").hidden, + "Should hide info title if infoTitleEnabled is false" + ); + ok( + content.document.querySelector("#private-browsing-promo-text"), + "Should render promo title if promoTitleEnabled is true" + ); + }); + + await BrowserTestUtils.closeWindow(win); + await doExperimentCleanup(); +}); diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_about_nimbus_dismiss.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_about_nimbus_dismiss.js new file mode 100644 index 0000000000..bfe5708a5b --- /dev/null +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_about_nimbus_dismiss.js @@ -0,0 +1,139 @@ +/* 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/. */ + +add_setup(async function () { + ASRouter.resetMessageState(); + await SpecialPowers.pushPrefEnv({ + set: [["browser.promo.pin.enabled", false]], + }); + await ASRouter.onPrefChange(); +}); + +add_task(async function test_experiment_messaging_system_dismiss() { + const LOCALE = Services.locale.appLocaleAsBCP47; + let doExperimentCleanup = await setupMSExperimentWithMessage({ + id: `PB_NEWTAB_MESSAGING_SYSTEM_${Math.random()}`, + template: "pb_newtab", + content: { + hideDefault: true, + promoEnabled: true, + infoEnabled: true, + infoBody: "fluent:about-private-browsing-info-title", + promoLinkText: "fluent:about-private-browsing-prominent-cta", + infoLinkUrl: "http://foo.example.com/%LOCALE%", + promoLinkType: "link", + promoButton: { + action: { + data: { + args: "http://bar.example.com/%LOCALE%", + where: "tabshifted", + }, + type: "OPEN_URL", + }, + }, + }, + // Priority ensures this message is picked over the one in + // OnboardingMessageProvider + priority: 5, + targeting: "true", + }); + + let { win: win1, tab: tab1 } = await openTabAndWaitForRender(); + + await SpecialPowers.spawn(tab1, [LOCALE], async function (locale) { + content.document.querySelector("#dismiss-btn").click(); + info("button clicked"); + }); + + let telemetryEvent = await waitForTelemetryEvent("aboutprivatebrowsing"); + + ok( + telemetryEvent[2] == "click" && telemetryEvent[3] == "dismiss_button", + "recorded the dismiss button click" + ); + + let { win: win2, tab: tab2 } = await openTabAndWaitForRender(); + + await SpecialPowers.spawn(tab2, [], async function () { + is( + content.document.querySelector(".promo button"), + null, + "should no longer render the experiment message after dismissing" + ); + }); + + await BrowserTestUtils.closeWindow(win1); + await BrowserTestUtils.closeWindow(win2); + await doExperimentCleanup(); +}); + +add_task(async function test_experiment_messaging_show_default_on_dismiss() { + registerCleanupFunction(() => { + ASRouter.resetMessageState(); + }); + let doExperimentCleanup = await setupMSExperimentWithMessage({ + id: `PB_NEWTAB_MESSAGING_SYSTEM_${Math.random()}`, + template: "pb_newtab", + content: { + hideDefault: false, + promoEnabled: true, + infoEnabled: true, + infoBody: "fluent:about-private-browsing-info-title", + promoLinkText: "fluent:about-private-browsing-prominent-cta", + infoLinkUrl: "http://foo.example.com", + promoLinkType: "link", + promoButton: { + action: { + data: { + args: "http://bar.example.com", + where: "tabshifted", + }, + type: "OPEN_URL", + }, + }, + }, + // Priority ensures this message is picked over the one in + // OnboardingMessageProvider + priority: 5, + targeting: "true", + }); + + let { win: win1, tab: tab1 } = await openTabAndWaitForRender(); + + await SpecialPowers.spawn(tab1, [], async function () { + ok( + content.document.querySelector(".promo"), + "should render the promo experiment message" + ); + + content.document.querySelector("#dismiss-btn").click(); + info("button clicked"); + }); + + let telemetryEvent = await waitForTelemetryEvent("aboutprivatebrowsing"); + + ok( + telemetryEvent[2] == "click" && telemetryEvent[3] == "dismiss_button", + "recorded the dismiss button click" + ); + + let { win: win2, tab: tab2 } = await openTabAndWaitForRender(); + + await SpecialPowers.spawn(tab2, [], async function () { + const promoHeader = content.document.getElementById("promo-header"); + ok( + content.document.querySelector(".promo"), + "should render the default promo message after dismissing experiment promo" + ); + is( + promoHeader.getAttribute("data-l10n-id"), + "about-private-browsing-focus-promo-header-c", + "Correct default values are shown" + ); + }); + + await BrowserTestUtils.closeWindow(win1); + await BrowserTestUtils.closeWindow(win2); + await doExperimentCleanup(); +}); diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_about_nimbus_impressions.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_about_nimbus_impressions.js new file mode 100644 index 0000000000..ac42caa2dd --- /dev/null +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_about_nimbus_impressions.js @@ -0,0 +1,126 @@ +/* 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 that use TelemetryTestUtils.assertEvents (at the very least, those with + * `{ process: "content" }`) seem to be super flaky and intermittent-prone when they + * share a file with other telemetry tests, so each one gets its own file. + */ + +add_task(async function test_experiment_messaging_system_impressions() { + registerCleanupFunction(() => { + ASRouter.resetMessageState(); + }); + const LOCALE = Services.locale.appLocaleAsBCP47; + let experimentId = `pb_newtab_${Math.random()}`; + + let doExperimentCleanup = await setupMSExperimentWithMessage({ + id: experimentId, + template: "pb_newtab", + content: { + hideDefault: true, + promoEnabled: true, + infoEnabled: true, + infoBody: "fluent:about-private-browsing-info-title", + promoLinkText: "fluent:about-private-browsing-prominent-cta", + infoLinkUrl: "http://foo.example.com/%LOCALE%", + promoButton: { + action: { + data: { + args: "https://bar.example.com/%LOCALE%", + where: "tabshifted", + }, + type: "OPEN_URL", + }, + }, + }, + frequency: { + lifetime: 2, + }, + // Priority ensures this message is picked over the one in + // OnboardingMessageProvider + priority: 5, + targeting: "true", + }); + + Services.telemetry.clearEvents(); + + let { win: win1, tab: tab1 } = await openTabAndWaitForRender(); + + await SpecialPowers.spawn(tab1, [LOCALE], async function (locale) { + is( + content.document + .querySelector(".promo button") + .classList.contains("primary"), + true, + "should render the promo button as a button" + ); + }); + + let event = await waitForTelemetryEvent("normandy", experimentId); + + ok( + event[1] == "normandy" && + event[2] == "expose" && + event[3] == "nimbus_experiment" && + event[4].includes(experimentId) && + event[5].featureId == "pbNewtab", + "recorded telemetry for expose" + ); + + Services.telemetry.clearEvents(); + + let { win: win2, tab: tab2 } = await openTabAndWaitForRender(); + + await SpecialPowers.spawn(tab2, [LOCALE], async function (locale) { + is( + content.document + .querySelector(".promo button") + .classList.contains("primary"), + true, + "should render the promo button as a button" + ); + }); + + let event2 = await waitForTelemetryEvent("normandy", experimentId); + + ok( + event2[1] == "normandy" && + event2[2] == "expose" && + event2[3] == "nimbus_experiment" && + event2[4].includes(experimentId) && + event2[5].featureId == "pbNewtab", + "recorded telemetry for expose" + ); + + Services.telemetry.clearEvents(); + + let { win: win3, tab: tab3 } = await openTabAndWaitForRender(); + + await SpecialPowers.spawn(tab3, [], async function () { + is( + content.document.querySelector(".promo button"), + null, + "should no longer render the experiment message after 2 impressions" + ); + }); + + // Verify that the telemetry events array does not + // contain an expose event for pbNewtab + info("Should not have promo expose"); + TelemetryTestUtils.assertEvents([], { + category: "normandy", + method: "expose", + object: "nimbus_experiment", + extra_keys: { + featureId: "pbNewtab", + }, + }); + + Services.telemetry.clearEvents(); + + await BrowserTestUtils.closeWindow(win1); + await BrowserTestUtils.closeWindow(win2); + await BrowserTestUtils.closeWindow(win3); + await doExperimentCleanup(); +}); diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_about_nimbus_messaging.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_about_nimbus_messaging.js new file mode 100644 index 0000000000..1463cee961 --- /dev/null +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_about_nimbus_messaging.js @@ -0,0 +1,247 @@ +/* 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/. */ + +const { sinon } = ChromeUtils.importESModule( + "resource://testing-common/Sinon.sys.mjs" +); + +add_task(async function test_experiment_messaging_system() { + const LOCALE = Services.locale.appLocaleAsBCP47; + let doExperimentCleanup = await setupMSExperimentWithMessage({ + id: "PB_NEWTAB_MESSAGING_SYSTEM", + template: "pb_newtab", + content: { + hideDefault: true, + promoEnabled: true, + infoEnabled: true, + infoBody: "fluent:about-private-browsing-info-title", + promoLinkText: "fluent:about-private-browsing-prominent-cta", + infoLinkUrl: "http://foo.example.com/%LOCALE%", + promoButton: { + action: { + data: { + args: "http://bar.example.com/%LOCALE%", + where: "tabshifted", + }, + type: "OPEN_URL", + }, + }, + }, + // Priority ensures this message is picked over the one in + // OnboardingMessageProvider + priority: 5, + targeting: "true", + }); + + let { win, tab } = await openTabAndWaitForRender(); + + await SpecialPowers.spawn(tab, [LOCALE], async function (locale) { + const infoBody = content.document.getElementById("info-body"); + const promoLink = content.document.getElementById( + "private-browsing-promo-link" + ); + + // Check experiment values are rendered + is( + infoBody.getAttribute("data-l10n-id"), + "about-private-browsing-info-title", + "should render infoBody with fluent" + ); + is( + promoLink.getAttribute("data-l10n-id"), + "about-private-browsing-prominent-cta", + "should render promoLinkText with fluent" + ); + is( + content.document.querySelector(".info a").getAttribute("href"), + "http://foo.example.com/" + locale, + "should format the infoLinkUrl url" + ); + is( + content.document.querySelector(".info a").getAttribute("target"), + "_blank", + "should open info url in new tab" + ); + }); + + await BrowserTestUtils.closeWindow(win); + await doExperimentCleanup(); +}); + +add_task(async function test_experiment_promo_action() { + let doExperimentCleanup = await setupMSExperimentWithMessage({ + id: "PB_NEWTAB_TEST_URL", + template: "pb_newtab", + content: { + hideDefault: true, + promoEnabled: true, + infoEnabled: true, + infoBody: "fluent:about-private-browsing-info-title", + promoLinkText: "fluent:about-private-browsing-prominent-cta", + infoLinkUrl: "http://foo.example.com/%LOCALE%", + promoLinkType: "button", + promoButton: { + action: { + data: { + args: "https://foo.example.com", + where: "tabshifted", + }, + type: "OPEN_URL", + }, + }, + }, + // Priority ensures this message is picked over the one in + // OnboardingMessageProvider + priority: 5, + targeting: "true", + }); + + let { win, tab } = await openTabAndWaitForRender(); + const sandbox = sinon.createSandbox(); + registerCleanupFunction(() => { + ASRouter.resetMessageState(); + sandbox.restore(); + BrowserTestUtils.closeWindow(win); + }); + + let windowGlobalParent = + win.gBrowser.selectedBrowser.browsingContext.currentWindowGlobal; + let aboutPrivateBrowsingActor = windowGlobalParent.getActor( + "AboutPrivateBrowsing" + ); + + let specialActionSpy = sandbox.spy( + aboutPrivateBrowsingActor, + "receiveMessage" + ); + + let expectedUrl = "https://foo.example.com"; + + await SpecialPowers.spawn(tab, [], async function () { + ok( + content.document.querySelector(".promo"), + "should render the promo experiment message" + ); + + is( + content.document + .querySelector(".promo button") + .classList.contains("primary"), + true, + "should render the promo button styled as a button" + ); + + content.document.querySelector(".promo button").click(); + info("promo button clicked"); + }); + + Assert.equal( + specialActionSpy.callCount, + 1, + "Should be called by promo action" + ); + + let promoAction = specialActionSpy.firstCall.args[0].data; + + Assert.equal( + promoAction.type, + "OPEN_URL", + "Should be called with promo button action" + ); + + Assert.equal( + promoAction.data.args, + expectedUrl, + "Should be called with right URL" + ); + + await doExperimentCleanup(); +}); + +add_task(async function test_experiment_open_spotlight_action() { + let doExperimentCleanup = await setupMSExperimentWithMessage({ + id: "PB_NEWTAB_TEST_SPOTLIGHT", + template: "pb_newtab", + content: { + hideDefault: true, + promoEnabled: true, + infoEnabled: true, + infoBody: "fluent:about-private-browsing-info-title", + promoLinkText: "fluent:about-private-browsing-prominent-cta", + infoLinkUrl: "http://foo.example.com/", + promoLinkType: "button", + promoButton: { + action: { + type: "SHOW_SPOTLIGHT", + data: { + content: { + template: "multistage", + screens: [ + { + content: { + title: "Test", + subtitle: "Sub Title", + }, + }, + ], + }, + }, + }, + }, + }, + // Priority ensures this message is picked over the one in + // OnboardingMessageProvider + priority: 5, + targeting: "true", + }); + + let { win, tab } = await openTabAndWaitForRender(); + const sandbox = sinon.createSandbox(); + registerCleanupFunction(() => { + ASRouter.resetMessageState(); + sandbox.restore(); + BrowserTestUtils.closeWindow(win); + }); + + let windowGlobalParent = + win.gBrowser.selectedBrowser.browsingContext.currentWindowGlobal; + let aboutPrivateBrowsingActor = windowGlobalParent.getActor( + "AboutPrivateBrowsing" + ); + + let specialActionSpy = sandbox.spy( + aboutPrivateBrowsingActor, + "receiveMessage" + ); + + await SpecialPowers.spawn(tab, [], async function () { + ok( + content.document.querySelector(".promo"), + "should render the promo experiment message" + ); + content.document.querySelector(".promo button").click(); + }); + + Assert.equal( + specialActionSpy.callCount, + 1, + "Should be called by promo action" + ); + + let promoAction = specialActionSpy.firstCall.args[0].data; + + Assert.equal( + promoAction.type, + "SHOW_SPOTLIGHT", + "Should be called with promo button spotlight action" + ); + + Assert.equal( + promoAction.data.content.metrics, + "allow", + "Should be called with metrics property set as allow for experiments" + ); + + await doExperimentCleanup(); +}); diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_about_search_banner.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_about_search_banner.js new file mode 100644 index 0000000000..4d8c2c407a --- /dev/null +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_about_search_banner.js @@ -0,0 +1,317 @@ +/* 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/. */ + +// This test makes sure that about:privatebrowsing correctly shows the search +// banner. + +const { AboutPrivateBrowsingParent } = ChromeUtils.importESModule( + "resource:///actors/AboutPrivateBrowsingParent.sys.mjs" +); + +const PREF_UI_ENABLED = "browser.search.separatePrivateDefault.ui.enabled"; +const PREF_BANNER_SHOWN = + "browser.search.separatePrivateDefault.ui.banner.shown"; +const PREF_MAX_SEARCH_BANNER_SHOW_COUNT = + "browser.search.separatePrivateDefault.ui.banner.max"; +const MAX_SHOW_COUNT = 5; + +add_setup(async function () { + SpecialPowers.pushPrefEnv({ + set: [ + [PREF_UI_ENABLED, false], + [PREF_BANNER_SHOWN, 0], + [PREF_MAX_SEARCH_BANNER_SHOW_COUNT, MAX_SHOW_COUNT], + ], + }); + + AboutPrivateBrowsingParent.setShownThisSession(false); +}); + +add_task(async function test_not_shown_if_pref_off() { + SpecialPowers.pushPrefEnv({ + set: [ + [PREF_UI_ENABLED, false], + [PREF_MAX_SEARCH_BANNER_SHOW_COUNT, 5], + ], + }); + + const { win, tab } = await openAboutPrivateBrowsing(); + + await SpecialPowers.spawn(tab, [], async function () { + await ContentTaskUtils.waitForCondition( + () => + content.document.documentElement.hasAttribute( + "SearchBannerInitialized" + ), + "Should have initialized" + ); + ok( + content.document.getElementById("search-banner").hasAttribute("hidden"), + "should be hiding the in-content search banner" + ); + }); + + await BrowserTestUtils.closeWindow(win); +}); + +add_task(async function test_not_shown_if_max_count_0() { + // To avoid having to restart Firefox and slow down tests, we manually reset + // the session pref. + AboutPrivateBrowsingParent.setShownThisSession(false); + + SpecialPowers.pushPrefEnv({ + set: [ + [PREF_UI_ENABLED, true], + [PREF_MAX_SEARCH_BANNER_SHOW_COUNT, 0], + ], + }); + const { win, tab } = await openAboutPrivateBrowsing(); + + await SpecialPowers.spawn(tab, [], async function () { + await ContentTaskUtils.waitForCondition( + () => + content.document.documentElement.hasAttribute( + "SearchBannerInitialized" + ), + "Should have initialized" + ); + ok( + content.document.getElementById("search-banner").hasAttribute("hidden"), + "should be hiding the in-content search banner" + ); + }); + + await BrowserTestUtils.closeWindow(win); +}); + +add_task(async function test_show_banner_first() { + // To avoid having to restart Firefox and slow down tests, we manually reset + // the session pref. + AboutPrivateBrowsingParent.setShownThisSession(false); + + SpecialPowers.pushPrefEnv({ + set: [ + [PREF_UI_ENABLED, true], + [PREF_MAX_SEARCH_BANNER_SHOW_COUNT, MAX_SHOW_COUNT], + ], + }); + + let prefChanged = TestUtils.waitForPrefChange(PREF_BANNER_SHOWN); + + const { win, tab } = await openAboutPrivateBrowsing(); + + Assert.equal( + await prefChanged, + 1, + "Should have incremented the amount of times shown." + ); + + await SpecialPowers.spawn(tab, [], async function () { + await ContentTaskUtils.waitForCondition( + () => + content.document.documentElement.hasAttribute( + "SearchBannerInitialized" + ), + "Should have initialized" + ); + + ok( + !content.document.getElementById("search-banner").hasAttribute("hidden"), + "should be showing the in-content search banner" + ); + }); + + await BrowserTestUtils.closeWindow(win); + + const { win: win1, tab: tab1 } = await openAboutPrivateBrowsing(); + + await SpecialPowers.spawn(tab1, [], async function () { + await ContentTaskUtils.waitForCondition( + () => + content.document.documentElement.hasAttribute( + "SearchBannerInitialized" + ), + "Should have initialized" + ); + + ok( + content.document.getElementById("search-banner").hasAttribute("hidden"), + "should not be showing the banner in a second window." + ); + }); + + await BrowserTestUtils.closeWindow(win1); + + Assert.equal( + Services.prefs.getIntPref(PREF_BANNER_SHOWN, -1), + 1, + "Should not have changed the preference further" + ); +}); + +add_task(async function test_show_banner_max_times() { + // We've already shown the UI once, so show it a few more times. + for (let i = 1; i < MAX_SHOW_COUNT; i++) { + // To avoid having to restart Firefox and slow down tests, we manually reset + // the session pref. + AboutPrivateBrowsingParent.setShownThisSession(false); + + let prefChanged = TestUtils.waitForPrefChange(PREF_BANNER_SHOWN); + const { win, tab } = await openAboutPrivateBrowsing(); + + Assert.equal( + await prefChanged, + i + 1, + "Should have incremented the amount of times shown." + ); + + await SpecialPowers.spawn(tab, [], async function () { + await ContentTaskUtils.waitForCondition( + () => + content.document.documentElement.hasAttribute( + "SearchBannerInitialized" + ), + "Should have initialized" + ); + + ok( + !content.document + .getElementById("search-banner") + .hasAttribute("hidden"), + "Should be showing the banner again" + ); + }); + + await BrowserTestUtils.closeWindow(win); + } + + // Final time! + + AboutPrivateBrowsingParent.setShownThisSession(false); + + const { win, tab } = await openAboutPrivateBrowsing(); + + await SpecialPowers.spawn(tab, [], async function () { + await ContentTaskUtils.waitForCondition( + () => + content.document.documentElement.hasAttribute( + "SearchBannerInitialized" + ), + "Should have initialized" + ); + + ok( + content.document.getElementById("search-banner").hasAttribute("hidden"), + "should not be showing the banner again" + ); + }); + + await BrowserTestUtils.closeWindow(win); +}); + +add_task(async function test_show_banner_close_no_more() { + SpecialPowers.pushPrefEnv({ + set: [[PREF_BANNER_SHOWN, 0]], + }); + + AboutPrivateBrowsingParent.setShownThisSession(false); + + const { win, tab } = await openAboutPrivateBrowsing(); + + await SpecialPowers.spawn(tab, [], async function () { + await ContentTaskUtils.waitForCondition( + () => + content.document.documentElement.hasAttribute( + "SearchBannerInitialized" + ), + "Should have initialized" + ); + + ok( + !content.document.getElementById("search-banner").hasAttribute("hidden"), + "should be showing the banner again before closing" + ); + + content.document.getElementById("search-banner-close-button").click(); + + await ContentTaskUtils.waitForCondition( + () => + ContentTaskUtils.isHidden( + content.document.getElementById("search-banner") + ), + "should have closed the in-content search banner after clicking close" + ); + }); + + await BrowserTestUtils.closeWindow(win); + + Assert.equal( + Services.prefs.getIntPref(PREF_BANNER_SHOWN, -1), + MAX_SHOW_COUNT, + "Should have set the shown preference to the maximum" + ); +}); + +add_task(async function test_show_banner_open_preferences_and_no_more() { + SpecialPowers.pushPrefEnv({ + set: [[PREF_BANNER_SHOWN, 0]], + }); + + AboutPrivateBrowsingParent.setShownThisSession(false); + + const { win, tab } = await openAboutPrivateBrowsing(); + + // This is "borrowed" from the preferences test code, as waiting for the + // full preferences to load helps avoid leaking a window. + const finalPaneEvent = Services.prefs.getBoolPref( + "identity.fxaccounts.enabled" + ) + ? "sync-pane-loaded" + : "privacy-pane-loaded"; + let finalPrefPaneLoaded = TestUtils.topicObserved(finalPaneEvent, () => true); + const waitForInitialized = new Promise(resolve => { + tab.addEventListener( + "Initialized", + () => { + tab.contentWindow.addEventListener( + "load", + async function () { + await finalPrefPaneLoaded; + resolve(); + }, + { once: true } + ); + }, + { capture: true, once: true } + ); + }); + + await SpecialPowers.spawn(tab, [], async function () { + await ContentTaskUtils.waitForCondition( + () => + content.document.documentElement.hasAttribute( + "SearchBannerInitialized" + ), + "Should have initialized" + ); + + ok( + !content.document.getElementById("search-banner").hasAttribute("hidden"), + "should be showing the banner again before opening prefs" + ); + + content.document.getElementById("open-search-options-link").click(); + }); + + info("Waiting for preference window load"); + await waitForInitialized; + + await BrowserTestUtils.closeWindow(win); + + Assert.equal( + Services.prefs.getIntPref(PREF_BANNER_SHOWN, -1), + MAX_SHOW_COUNT, + "Should have set the shown preference to the maximum" + ); +}); diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_beacon.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_beacon.js new file mode 100644 index 0000000000..034061a91a --- /dev/null +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_beacon.js @@ -0,0 +1,46 @@ +/* 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/. */ + +const TEST_DOMAIN = "example.com"; +const TEST_TOP = `https://${TEST_DOMAIN}`; +const TEST_URL = `${TEST_TOP}/browser/browser/components/privatebrowsing/test/browser/title.sjs`; + +add_task(async function () { + let privateWin = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + + let tab = await BrowserTestUtils.openNewForegroundTab( + privateWin.gBrowser, + TEST_TOP + ); + + // Create a promise to wait the http response of the beacon request. + let promise = BrowserUtils.promiseObserved( + "http-on-examine-response", + subject => { + let channel = subject.QueryInterface(Ci.nsIHttpChannel); + let url = channel.URI.spec; + + return url == TEST_URL; + } + ); + + // Open a tab and send a beacon. + await SpecialPowers.spawn(tab.linkedBrowser, [TEST_URL], async url => { + content.navigator.sendBeacon(url); + }); + + // Close the entire private window directly. + await BrowserTestUtils.closeWindow(privateWin); + + // Wait the response. + await promise; + + const cookies = Services.cookies.getCookiesFromHost(TEST_DOMAIN, { + privateBrowsingId: 1, + }); + + is(cookies.length, 0, "No cookies after close the private window."); +}); diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_blobUrl.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_blobUrl.js new file mode 100644 index 0000000000..f5fd40d4ed --- /dev/null +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_blobUrl.js @@ -0,0 +1,69 @@ +"use strict"; + +// Here we want to test that blob URLs are not available between private and +// non-private browsing. + +const BASE_URI = + "http://mochi.test:8888/browser/browser/components/" + + "privatebrowsing/test/browser/empty_file.html"; + +add_task(async function test() { + const loaded = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + BASE_URI + ); + BrowserTestUtils.startLoadingURIString(gBrowser.selectedBrowser, BASE_URI); + await loaded; + + let blobURL; + info("Creating a blob URL..."); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () { + return Promise.resolve( + content.window.URL.createObjectURL( + new Blob([123], { type: "text/plain" }) + ) + ); + }).then(newURL => { + blobURL = newURL; + }); + + info("Blob URL: " + blobURL); + + info("Creating a private window..."); + + let privateWin = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + let privateTab = privateWin.gBrowser.selectedBrowser; + + const privateTabLoaded = BrowserTestUtils.browserLoaded( + privateTab, + false, + BASE_URI + ); + BrowserTestUtils.startLoadingURIString(privateTab, BASE_URI); + await privateTabLoaded; + + await SpecialPowers.spawn(privateTab, [blobURL], function (url) { + return new Promise(resolve => { + var xhr = new content.window.XMLHttpRequest(); + xhr.onerror = function () { + resolve("SendErrored"); + }; + xhr.onload = function () { + resolve("SendLoaded"); + }; + xhr.open("GET", url); + xhr.send(); + }); + }).then(status => { + is( + status, + "SendErrored", + "Using a blob URI from one user context id in another should not work" + ); + }); + + await BrowserTestUtils.closeWindow(privateWin); +}); diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_cache.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_cache.js new file mode 100644 index 0000000000..de6aa1f6ba --- /dev/null +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_cache.js @@ -0,0 +1,94 @@ +/* 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/. */ + +// Check about:cache after private browsing +// This test covers MozTrap test 6047 +// bug 880621 + +var tmp = {}; + +function test() { + waitForExplicitFinish(); + + SpecialPowers.pushPrefEnv( + { + set: [["privacy.partition.network_state", false]], + }, + function () { + Sanitizer.sanitize(["cache"], { ignoreTimespan: false }); + + getStorageEntryCount("regular", function (nrEntriesR1) { + is(nrEntriesR1, 0, "Disk cache reports 0KB and has no entries"); + + get_cache_for_private_window(); + }); + } + ); +} + +function getStorageEntryCount(device, goon) { + var storage; + switch (device) { + case "private": + storage = Services.cache2.diskCacheStorage( + Services.loadContextInfo.private + ); + break; + case "regular": + storage = Services.cache2.diskCacheStorage( + Services.loadContextInfo.default + ); + break; + default: + throw new Error(`Unknown device ${device} at getStorageEntryCount`); + } + + var visitor = { + entryCount: 0, + onCacheStorageInfo(aEntryCount, aConsumption) {}, + onCacheEntryInfo(uri) { + var urispec = uri.asciiSpec; + info(device + ":" + urispec + "\n"); + if (urispec.match(/^https:\/\/example.com\//)) { + ++this.entryCount; + } + }, + onCacheEntryVisitCompleted() { + goon(this.entryCount); + }, + }; + + storage.asyncVisitStorage(visitor, true); +} + +function get_cache_for_private_window() { + let win = whenNewWindowLoaded({ private: true }, function () { + executeSoon(function () { + ok(true, "The private window got loaded"); + + let tab = BrowserTestUtils.addTab(win.gBrowser, "https://example.com"); + win.gBrowser.selectedTab = tab; + let newTabBrowser = win.gBrowser.getBrowserForTab(tab); + + BrowserTestUtils.browserLoaded(newTabBrowser).then(function () { + executeSoon(function () { + getStorageEntryCount("private", function (nrEntriesP) { + Assert.greaterOrEqual( + nrEntriesP, + 1, + "Memory cache reports some entries from example.org domain" + ); + + getStorageEntryCount("regular", function (nrEntriesR2) { + is(nrEntriesR2, 0, "Disk cache reports 0KB and has no entries"); + + win.close(); + finish(); + }); + }); + }); + }); + }); + }); +} diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_certexceptionsui.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_certexceptionsui.js new file mode 100644 index 0000000000..9b796613a9 --- /dev/null +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_certexceptionsui.js @@ -0,0 +1,65 @@ +/* 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/. */ + +// This test makes sure that certificate exceptions UI behaves correctly +// in private browsing windows, based on whether it's opened from the prefs +// window or from the SSL error page (see bug 461627). + +function test() { + const EXCEPTIONS_DLG_URL = "chrome://pippki/content/exceptionDialog.xhtml"; + const EXCEPTIONS_DLG_FEATURES = "chrome,centerscreen"; + const INVALID_CERT_LOCATION = "https://nocert.example.com/"; + waitForExplicitFinish(); + + // open a private browsing window + var pbWin = OpenBrowserWindow({ private: true }); + pbWin.addEventListener( + "load", + function () { + doTest(); + }, + { once: true } + ); + + // Test the certificate exceptions dialog + function doTest() { + let params = { + exceptionAdded: false, + location: INVALID_CERT_LOCATION, + prefetchCert: true, + }; + function testCheckbox() { + win.removeEventListener("load", testCheckbox); + Services.obs.addObserver(function onCertUI(aSubject, aTopic, aData) { + Services.obs.removeObserver(onCertUI, "cert-exception-ui-ready"); + ok(win.gCert, "The certificate information should be available now"); + + let checkbox = win.document.getElementById("permanent"); + ok( + checkbox.hasAttribute("disabled"), + "the permanent checkbox should be disabled when handling the private browsing mode" + ); + ok( + !checkbox.hasAttribute("checked"), + "the permanent checkbox should not be checked when handling the private browsing mode" + ); + win.close(); + cleanup(); + }, "cert-exception-ui-ready"); + } + var win = pbWin.openDialog( + EXCEPTIONS_DLG_URL, + "", + EXCEPTIONS_DLG_FEATURES, + params + ); + win.addEventListener("load", testCheckbox); + } + + function cleanup() { + // close the private browsing window + pbWin.close(); + finish(); + } +} diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_cleanup.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_cleanup.js new file mode 100644 index 0000000000..39e41589b4 --- /dev/null +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_cleanup.js @@ -0,0 +1,46 @@ +/* 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/. */ + +"use strict"; + +const DOMAIN = "http://example.com/"; +const PATH = "browser/browser/components/privatebrowsing/test/browser/"; +const TOP_PAGE = DOMAIN + PATH + "empty_file.html"; + +add_task(async () => { + // Create a private browsing window. + let privateWindow = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + + let privateTab = privateWindow.gBrowser.selectedBrowser; + BrowserTestUtils.startLoadingURIString(privateTab, TOP_PAGE); + await BrowserTestUtils.browserLoaded(privateTab); + + let observerExited = { + observe(aSubject, aTopic, aData) { + ok(false, "Notification received!"); + }, + }; + Services.obs.addObserver(observerExited, "last-pb-context-exited"); + + let popup = BrowserTestUtils.waitForNewWindow(); + + await SpecialPowers.spawn(privateTab, [], () => { + content.window.open("empty_file.html", "_blank", "width=300,height=300"); + }); + + popup = await popup; + ok(!!popup, "Popup shown"); + + await BrowserTestUtils.closeWindow(privateWindow); + Services.obs.removeObserver(observerExited, "last-pb-context-exited"); + + let notificationPromise = TestUtils.topicObserved("last-pb-context-exited"); + + popup.close(); + + await notificationPromise; + ok(true, "Notification received!"); +}); diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_concurrent.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_concurrent.js new file mode 100644 index 0000000000..0029cdc852 --- /dev/null +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_concurrent.js @@ -0,0 +1,101 @@ +/* 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/. */ + +// Test opening two tabs that share a localStorage, but keep one in private mode. +// Ensure that values from one don't leak into the other, and that values from +// earlier private storage sessions aren't visible later. + +// Step 1: create new tab, load a page that sets test=value in non-private storage +// Step 2: create a new tab, load a page that sets test2=value2 in private storage +// Step 3: load a page in the tab from step 1 that checks the value of test2 is value2 and the total count in non-private storage is 1 +// Step 4: load a page in the tab from step 2 that checks the value of test is value and the total count in private storage is 1 + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["dom.ipc.processCount", 1]], + }); +}); + +add_task(async function test() { + let prefix = + "http://mochi.test:8888/browser/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_concurrent_page.html"; + + function getElts(browser) { + return browser.contentTitle.split("|"); + } + + // Step 1 + let non_private_browser = gBrowser.selectedBrowser; + let url = prefix + "?action=set&name=test&value=value&initial=true"; + BrowserTestUtils.startLoadingURIString(non_private_browser, url); + await BrowserTestUtils.browserLoaded(non_private_browser, false, url); + + // Step 2 + let private_window = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + let private_browser = private_window.gBrowser.selectedBrowser; + url = prefix + "?action=set&name=test2&value=value2"; + BrowserTestUtils.startLoadingURIString(private_browser, url); + await BrowserTestUtils.browserLoaded(private_browser, false, url); + + // Step 3 + url = prefix + "?action=get&name=test2"; + BrowserTestUtils.startLoadingURIString(non_private_browser, url); + await BrowserTestUtils.browserLoaded(non_private_browser, false, url); + let elts = await getElts(non_private_browser); + isnot(elts[0], "value2", "public window shouldn't see private storage"); + is(elts[1], "1", "public window should only see public items"); + + // Step 4 + url = prefix + "?action=get&name=test"; + BrowserTestUtils.startLoadingURIString(private_browser, url); + await BrowserTestUtils.browserLoaded(private_browser, false, url); + elts = await getElts(private_browser); + isnot(elts[0], "value", "private window shouldn't see public storage"); + is(elts[1], "1", "private window should only see private items"); + + // Reopen the private window again, without privateBrowsing, which should clear the + // the private storage. + private_window.close(); + private_window = await BrowserTestUtils.openNewBrowserWindow({ + private: false, + }); + private_browser = null; + await new Promise(resolve => Cu.schedulePreciseGC(resolve)); + private_browser = private_window.gBrowser.selectedBrowser; + + url = prefix + "?action=get&name=test2"; + BrowserTestUtils.startLoadingURIString(private_browser, url); + await BrowserTestUtils.browserLoaded(private_browser, false, url); + elts = await getElts(private_browser); + isnot( + elts[0], + "value2", + "public window shouldn't see cleared private storage" + ); + is(elts[1], "1", "public window should only see public items"); + + // Making it private again should clear the storage and it shouldn't + // be able to see the old private storage as well. + private_window.close(); + private_window = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + private_browser = null; + await new Promise(resolve => Cu.schedulePreciseGC(resolve)); + private_browser = private_window.gBrowser.selectedBrowser; + + url = prefix + "?action=set&name=test3&value=value3"; + BrowserTestUtils.startLoadingURIString(private_browser, url); + await BrowserTestUtils.browserLoaded(private_browser, false, url); + elts = await getElts(private_browser); + is(elts[1], "1", "private window should only see new private items"); + + // Cleanup. + url = prefix + "?final=true"; + BrowserTestUtils.startLoadingURIString(non_private_browser, url); + await BrowserTestUtils.browserLoaded(non_private_browser, false, url); + private_window.close(); +}); diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_concurrent_page.html b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_concurrent_page.html new file mode 100644 index 0000000000..96d3b74c7c --- /dev/null +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_concurrent_page.html @@ -0,0 +1,33 @@ + + + + + + + diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_context_and_chromeFlags.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_context_and_chromeFlags.js new file mode 100644 index 0000000000..66e8dea359 --- /dev/null +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_context_and_chromeFlags.js @@ -0,0 +1,69 @@ +"use strict"; + +/** + * Given some window in the parent process, ensure that + * the nsIAppWindow has the CHROME_PRIVATE_WINDOW chromeFlag, + * and that the usePrivateBrowsing property is set to true on + * both the window's nsILoadContext, as well as on the initial + * browser's content docShell nsILoadContext. + * + * @param win (nsIDOMWindow) + * An nsIDOMWindow in the parent process. + * @return Promise + */ +function assertWindowIsPrivate(win) { + let winDocShell = win.docShell; + let chromeFlags = winDocShell.treeOwner + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIAppWindow).chromeFlags; + + if (!win.gBrowser.selectedBrowser.hasContentOpener) { + Assert.ok( + chromeFlags & Ci.nsIWebBrowserChrome.CHROME_PRIVATE_WINDOW, + "Should have the private window chrome flag" + ); + } + + let loadContext = winDocShell.QueryInterface(Ci.nsILoadContext); + Assert.ok( + loadContext.usePrivateBrowsing, + "The parent window should be using private browsing" + ); + + return SpecialPowers.spawn( + win.gBrowser.selectedBrowser, + [], + async function () { + let contentLoadContext = docShell.QueryInterface(Ci.nsILoadContext); + Assert.ok( + contentLoadContext.usePrivateBrowsing, + "Content docShell should be using private browsing" + ); + } + ); +} + +/** + * Tests that chromeFlags bits and the nsILoadContext.usePrivateBrowsing + * attribute are properly set when opening a new private browsing + * window. + */ +add_task(async function test_context_and_chromeFlags() { + let win = await BrowserTestUtils.openNewBrowserWindow({ private: true }); + await assertWindowIsPrivate(win); + + let browser = win.gBrowser.selectedBrowser; + + let newWinPromise = BrowserTestUtils.waitForNewWindow({ + url: "https://example.com/", + }); + await SpecialPowers.spawn(browser, [], async function () { + content.open("https://example.com", "_blank", "width=100,height=100"); + }); + + let win2 = await newWinPromise; + await assertWindowIsPrivate(win2); + + await BrowserTestUtils.closeWindow(win2); + await BrowserTestUtils.closeWindow(win); +}); diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_crh.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_crh.js new file mode 100644 index 0000000000..ddfb53f1a8 --- /dev/null +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_crh.js @@ -0,0 +1,48 @@ +/* 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/. */ + +// This test makes sure that the Clear Recent History menu item and command +// is disabled inside the private browsing mode. + +add_task(async function test() { + function checkDisableOption(aPrivateMode, aWindow) { + let crhCommand = aWindow.document.getElementById("Tools:Sanitize"); + ok(crhCommand, "The clear recent history command should exist"); + + is( + PrivateBrowsingUtils.isWindowPrivate(aWindow), + aPrivateMode, + "PrivateBrowsingUtils should report the correct per-window private browsing status" + ); + is( + crhCommand.hasAttribute("disabled"), + aPrivateMode, + "Clear Recent History command should be disabled according to the private browsing mode" + ); + } + + let testURI = "http://mochi.test:8888/"; + + let privateWin = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + let privateBrowser = privateWin.gBrowser.selectedBrowser; + BrowserTestUtils.startLoadingURIString(privateBrowser, testURI); + await BrowserTestUtils.browserLoaded(privateBrowser); + + info("Test on private window"); + checkDisableOption(true, privateWin); + + let win = await BrowserTestUtils.openNewBrowserWindow(); + let browser = win.gBrowser.selectedBrowser; + BrowserTestUtils.startLoadingURIString(browser, testURI); + await BrowserTestUtils.browserLoaded(browser); + + info("Test on public window"); + checkDisableOption(false, win); + + // Cleanup + await BrowserTestUtils.closeWindow(privateWin); + await BrowserTestUtils.closeWindow(win); +}); diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_downloadLastDir.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_downloadLastDir.js new file mode 100644 index 0000000000..dd358bee73 --- /dev/null +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_downloadLastDir.js @@ -0,0 +1,133 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/. */ + +function test() { + waitForExplicitFinish(); + + let { FileUtils } = ChromeUtils.importESModule( + "resource://gre/modules/FileUtils.sys.mjs" + ); + let { DownloadLastDir } = ChromeUtils.importESModule( + "resource://gre/modules/DownloadLastDir.sys.mjs" + ); + let MockFilePicker = SpecialPowers.MockFilePicker; + let launcher = { + source: Services.io.newURI("http://test1.com/file"), + }; + + MockFilePicker.init(window); + MockFilePicker.returnValue = Ci.nsIFilePicker.returnOK; + + let prefs = Services.prefs.getBranch("browser.download."); + let launcherDialog = Cc["@mozilla.org/helperapplauncherdialog;1"].getService( + Ci.nsIHelperAppLauncherDialog + ); + let tmpDir = FileUtils.getDir("TmpD", []); + let dir1 = newDirectory(); + let dir2 = newDirectory(); + let dir3 = newDirectory(); + let file1 = newFileInDirectory(dir1); + let file2 = newFileInDirectory(dir2); + let file3 = newFileInDirectory(dir3); + + // cleanup functions registration + registerCleanupFunction(function () { + Services.prefs.clearUserPref("browser.download.lastDir"); + [dir1, dir2, dir3].forEach(dir => dir.remove(true)); + MockFilePicker.cleanup(); + }); + prefs.setComplexValue("lastDir", Ci.nsIFile, tmpDir); + + function testOnWindow(aPrivate, aCallback) { + whenNewWindowLoaded({ private: aPrivate }, function (win) { + let gDownloadLastDir = new DownloadLastDir(win); + aCallback(win, gDownloadLastDir); + gDownloadLastDir.cleanupPrivateFile(); + }); + } + + function testDownloadDir( + aWin, + gDownloadLastDir, + aFile, + aDisplayDir, + aLastDir, + aGlobalLastDir, + aCallback + ) { + // Check lastDir preference. + is( + prefs.getComplexValue("lastDir", Ci.nsIFile).path, + aDisplayDir.path, + "LastDir should be the expected display dir" + ); + // Check gDownloadLastDir value. + is( + gDownloadLastDir.file.path, + aDisplayDir.path, + "gDownloadLastDir should be the expected display dir" + ); + + MockFilePicker.setFiles([aFile]); + MockFilePicker.displayDirectory = null; + + launcher.saveDestinationAvailable = function (file) { + ok(!!file, "promptForSaveToFile correctly returned a file"); + + // File picker should start with expected display dir. + is( + MockFilePicker.displayDirectory.path, + aDisplayDir.path, + "File picker should start with browser.download.lastDir" + ); + // browser.download.lastDir should be modified on not private windows + is( + prefs.getComplexValue("lastDir", Ci.nsIFile).path, + aLastDir.path, + "LastDir should be the expected last dir" + ); + // gDownloadLastDir should be usable outside of private windows + is( + gDownloadLastDir.file.path, + aGlobalLastDir.path, + "gDownloadLastDir should be the expected global last dir" + ); + + launcher.saveDestinationAvailable = null; + aWin.close(); + aCallback(); + }; + + launcherDialog.promptForSaveToFileAsync(launcher, aWin, "", "", false); + } + + testOnWindow(false, function (win, downloadDir) { + testDownloadDir(win, downloadDir, file1, tmpDir, dir1, dir1, function () { + testOnWindow(true, function (win1, downloadDir1) { + testDownloadDir( + win1, + downloadDir1, + file2, + dir1, + dir1, + dir2, + function () { + testOnWindow(false, function (win2, downloadDir2) { + testDownloadDir( + win2, + downloadDir2, + file3, + dir1, + dir3, + dir3, + finish + ); + }); + } + ); + }); + }); + }); +} diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_downloadLastDir_c.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_downloadLastDir_c.js new file mode 100644 index 0000000000..04e510096a --- /dev/null +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_downloadLastDir_c.js @@ -0,0 +1,146 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/. */ + +function test() { + waitForExplicitFinish(); + + let { FileUtils } = ChromeUtils.importESModule( + "resource://gre/modules/FileUtils.sys.mjs" + ); + let { DownloadLastDir } = ChromeUtils.importESModule( + "resource://gre/modules/DownloadLastDir.sys.mjs" + ); + let MockFilePicker = SpecialPowers.MockFilePicker; + + MockFilePicker.init(window); + MockFilePicker.returnValue = Ci.nsIFilePicker.returnOK; + + let validateFileNameToRestore = validateFileName; + let prefs = Services.prefs.getBranch("browser.download."); + let tmpDir = FileUtils.getDir("TmpD", []); + let dir1 = newDirectory(); + let dir2 = newDirectory(); + let dir3 = newDirectory(); + let file1 = newFileInDirectory(dir1); + let file2 = newFileInDirectory(dir2); + let file3 = newFileInDirectory(dir3); + + // cleanup function registration + registerCleanupFunction(function () { + Services.prefs.clearUserPref("browser.download.lastDir"); + [dir1, dir2, dir3].forEach(dir => dir.remove(true)); + MockFilePicker.cleanup(); + validateFileName = validateFileNameToRestore; + }); + + // Overwrite validateFileName to validate everything + validateFileName = foo => foo; + + let params = { + fileInfo: new FileInfo( + "test.txt", + "test.txt", + "test", + "txt", + "http://mozilla.org/test.txt" + ), + contentType: "text/plain", + saveMode: SAVEMODE_FILEONLY, + saveAsType: kSaveAsType_Complete, + file: null, + }; + + prefs.setComplexValue("lastDir", Ci.nsIFile, tmpDir); + + function testOnWindow(aPrivate, aCallback) { + whenNewWindowLoaded({ private: aPrivate }, function (win) { + let gDownloadLastDir = new DownloadLastDir(win); + aCallback(win, gDownloadLastDir); + }); + } + + function testDownloadDir( + aWin, + gDownloadLastDir, + aFile, + aDisplayDir, + aLastDir, + aGlobalLastDir, + aCallback + ) { + // Check lastDir preference. + is( + prefs.getComplexValue("lastDir", Ci.nsIFile).path, + aDisplayDir.path, + "LastDir should be the expected display dir" + ); + // Check gDownloadLastDir value. + is( + gDownloadLastDir.file.path, + aDisplayDir.path, + "gDownloadLastDir should be the expected display dir" + ); + + MockFilePicker.setFiles([aFile]); + MockFilePicker.displayDirectory = null; + aWin + .promiseTargetFile(params) + .then(function () { + // File picker should start with expected display dir. + is( + MockFilePicker.displayDirectory.path, + aDisplayDir.path, + "File picker should start with browser.download.lastDir" + ); + // browser.download.lastDir should be modified on not private windows + is( + prefs.getComplexValue("lastDir", Ci.nsIFile).path, + aLastDir.path, + "LastDir should be the expected last dir" + ); + // gDownloadLastDir should be usable outside of private windows + is( + gDownloadLastDir.file.path, + aGlobalLastDir.path, + "gDownloadLastDir should be the expected global last dir" + ); + + gDownloadLastDir.cleanupPrivateFile(); + aWin.close(); + aCallback(); + }) + .catch(function () { + ok(false); + }); + } + + testOnWindow(false, function (win, downloadDir) { + testDownloadDir(win, downloadDir, file1, tmpDir, dir1, dir1, function () { + testOnWindow(true, function (win1, downloadDir1) { + testDownloadDir( + win1, + downloadDir1, + file2, + dir1, + dir1, + dir2, + function () { + testOnWindow(false, function (win2, downloadDir2) { + testDownloadDir( + win2, + downloadDir2, + file3, + dir1, + dir3, + dir3, + finish + ); + }); + } + ); + }); + }); + }); +} diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_downloadLastDir_toggle.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_downloadLastDir_toggle.js new file mode 100644 index 0000000000..4b88bbddf9 --- /dev/null +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_downloadLastDir_toggle.js @@ -0,0 +1,118 @@ +/** + * Tests how the browser remembers the last download folder + * from download to download, with a particular emphasis + * on how it behaves when private browsing windows open. + */ +add_task(async function test_downloads_last_dir_toggle() { + let tmpDir = FileUtils.getDir("TmpD", []); + let dir1 = newDirectory(); + + registerCleanupFunction(function () { + Services.prefs.clearUserPref("browser.download.lastDir"); + dir1.remove(true); + }); + + let win = await BrowserTestUtils.openNewBrowserWindow(); + let gDownloadLastDir = new DownloadLastDir(win); + is( + typeof gDownloadLastDir, + "object", + "gDownloadLastDir should be a valid object" + ); + is( + gDownloadLastDir.file, + null, + "gDownloadLastDir.file should be null to start with" + ); + + gDownloadLastDir.file = tmpDir; + is( + gDownloadLastDir.file.path, + tmpDir.path, + "LastDir should point to the temporary directory" + ); + isnot( + gDownloadLastDir.file, + tmpDir, + "gDownloadLastDir.file should not be pointing to the tmpDir" + ); + + gDownloadLastDir.file = 1; // not an nsIFile + is(gDownloadLastDir.file, null, "gDownloadLastDir.file should be null"); + + gDownloadLastDir.file = tmpDir; + clearHistory(); + is(gDownloadLastDir.file, null, "gDownloadLastDir.file should be null"); + + gDownloadLastDir.file = tmpDir; + await BrowserTestUtils.closeWindow(win); + + info("Opening the first private window"); + await testHelper({ private: true, expectedDir: tmpDir }); + info("Opening a non-private window"); + await testHelper({ private: false, expectedDir: tmpDir }); + info("Opening a private window and setting download directory"); + await testHelper({ private: true, setDir: dir1, expectedDir: dir1 }); + info("Opening a non-private window and checking download directory"); + await testHelper({ private: false, expectedDir: tmpDir }); + info("Opening private window and clearing history"); + await testHelper({ private: true, clearHistory: true, expectedDir: null }); + info("Opening a non-private window and checking download directory"); + await testHelper({ private: true, expectedDir: null }); +}); + +/** + * Opens a new window and performs some test actions on it based + * on the options object that have been passed in. + * + * @param options (Object) + * An object with the following properties: + * + * clearHistory (bool, optional): + * Whether or not to simulate clearing session history. + * Defaults to false. + * + * setDir (nsIFile, optional): + * An nsIFile for setting the last download directory. + * If not set, the load download directory is not changed. + * + * expectedDir (nsIFile, expectedDir): + * An nsIFile for what we expect the last download directory + * should be. The nsIFile is not compared directly - only + * paths are compared. If expectedDir is not set, then the + * last download directory is expected to be null. + * + * @returns Promise + */ +async function testHelper(options) { + let win = await BrowserTestUtils.openNewBrowserWindow(options); + let gDownloadLastDir = new DownloadLastDir(win); + + if (options.clearHistory) { + clearHistory(); + } + + if (options.setDir) { + gDownloadLastDir.file = options.setDir; + } + + let expectedDir = options.expectedDir; + + if (expectedDir) { + is( + gDownloadLastDir.file.path, + expectedDir.path, + "gDownloadLastDir should point to the expected last directory" + ); + isnot( + gDownloadLastDir.file, + expectedDir, + "gDownloadLastDir.file should not be pointing to the last directory" + ); + } else { + is(gDownloadLastDir.file, null, "gDownloadLastDir should be null"); + } + + gDownloadLastDir.cleanupPrivateFile(); + await BrowserTestUtils.closeWindow(win); +} diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_favicon.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_favicon.js new file mode 100644 index 0000000000..eea0ab07ca --- /dev/null +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_favicon.js @@ -0,0 +1,322 @@ +/* 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/. */ + +// This test make sure that the favicon of the private browsing is isolated. + +const TEST_SITE = "https://example.com"; +const TEST_CACHE_SITE = "https://test1.example.com"; +const TEST_DIRECTORY = + "/browser/browser/components/privatebrowsing/test/browser/"; + +const TEST_PAGE = TEST_SITE + TEST_DIRECTORY + "file_favicon.html"; +const TEST_CACHE_PAGE = TEST_CACHE_SITE + TEST_DIRECTORY + "file_favicon.html"; +const FAVICON_URI = TEST_SITE + TEST_DIRECTORY + "file_favicon.png"; +const FAVICON_CACHE_URI = TEST_CACHE_SITE + TEST_DIRECTORY + "file_favicon.png"; + +let systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal(); + +function clearAllImageCaches() { + let tools = SpecialPowers.Cc["@mozilla.org/image/tools;1"].getService( + SpecialPowers.Ci.imgITools + ); + let imageCache = tools.getImgCacheForDocument(window.document); + imageCache.clearCache(true); // true=chrome + imageCache.clearCache(false); // false=content +} + +function clearAllPlacesFavicons() { + let faviconService = Cc["@mozilla.org/browser/favicon-service;1"].getService( + Ci.nsIFaviconService + ); + + return new Promise(resolve => { + let observer = { + observe(aSubject, aTopic, aData) { + if (aTopic === "places-favicons-expired") { + resolve(); + Services.obs.removeObserver(observer, "places-favicons-expired"); + } + }, + }; + + Services.obs.addObserver(observer, "places-favicons-expired"); + faviconService.expireAllFavicons(); + }); +} + +function observeFavicon(aIsPrivate, aExpectedCookie, aPageURI) { + let attr = {}; + + if (aIsPrivate) { + attr.privateBrowsingId = 1; + } + + let expectedPrincipal = Services.scriptSecurityManager.createContentPrincipal( + aPageURI, + attr + ); + + return new Promise(resolve => { + let observer = { + observe(aSubject, aTopic, aData) { + // Make sure that the topic is 'http-on-modify-request'. + if (aTopic === "http-on-modify-request") { + // We check the privateBrowsingId for the originAttributes of the loading + // channel. All requests for the favicon should contain the correct + // privateBrowsingId. There are two requests for a favicon loading, one + // from the Places library and one from the XUL image. The difference + // of them is the loading principal. The Places will use the content + // principal and the XUL image will use the system principal. + + let httpChannel = aSubject.QueryInterface(Ci.nsIHttpChannel); + let reqLoadInfo = httpChannel.loadInfo; + let loadingPrincipal = reqLoadInfo.loadingPrincipal; + + // Make sure this is a favicon request. + if (httpChannel.URI.spec !== FAVICON_URI) { + return; + } + + // Check the privateBrowsingId. + if (aIsPrivate) { + is( + reqLoadInfo.originAttributes.privateBrowsingId, + 1, + "The loadInfo has correct privateBrowsingId" + ); + } else { + is( + reqLoadInfo.originAttributes.privateBrowsingId, + 0, + "The loadInfo has correct privateBrowsingId" + ); + } + + ok( + loadingPrincipal.equals(expectedPrincipal), + "The loadingPrincipal of favicon loading from Places should be the content prinicpal" + ); + + let faviconCookie = httpChannel.getRequestHeader("cookie"); + + is( + faviconCookie, + aExpectedCookie, + "The cookie of the favicon loading is correct." + ); + } else { + ok(false, "Received unexpected topic: ", aTopic); + } + + resolve(); + Services.obs.removeObserver(observer, "http-on-modify-request"); + }, + }; + + Services.obs.addObserver(observer, "http-on-modify-request"); + }); +} + +function waitOnFaviconResponse(aFaviconURL) { + return new Promise(resolve => { + let observer = { + observe(aSubject, aTopic, aData) { + if ( + aTopic === "http-on-examine-response" || + aTopic === "http-on-examine-cached-response" + ) { + let httpChannel = aSubject.QueryInterface(Ci.nsIHttpChannel); + let loadInfo = httpChannel.loadInfo; + + if (httpChannel.URI.spec !== aFaviconURL) { + return; + } + + let result = { + topic: aTopic, + privateBrowsingId: loadInfo.originAttributes.privateBrowsingId, + }; + + resolve(result); + Services.obs.removeObserver(observer, "http-on-examine-response"); + Services.obs.removeObserver( + observer, + "http-on-examine-cached-response" + ); + } + }, + }; + + Services.obs.addObserver(observer, "http-on-examine-response"); + Services.obs.addObserver(observer, "http-on-examine-cached-response"); + }); +} + +function waitOnFaviconLoaded(aFaviconURL) { + return PlacesTestUtils.waitForNotification("favicon-changed", events => + events.some(e => e.faviconUrl == aFaviconURL) + ); +} + +async function assignCookies(aBrowser, aURL, aCookieValue) { + let tabInfo = await openTab(aBrowser, aURL); + + await SpecialPowers.spawn( + tabInfo.browser, + [aCookieValue], + async function (value) { + content.document.cookie = value; + } + ); + + BrowserTestUtils.removeTab(tabInfo.tab); +} + +async function openTab(aBrowser, aURL) { + let tab = BrowserTestUtils.addTab(aBrowser, aURL); + + // Select tab and make sure its browser is focused. + aBrowser.selectedTab = tab; + tab.ownerGlobal.focus(); + + let browser = aBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + return { tab, browser }; +} + +registerCleanupFunction(async () => { + Services.cookies.removeAll(); + clearAllImageCaches(); + Services.cache2.clear(); + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(async function test_favicon_privateBrowsing() { + // Clear all image caches before running the test. + clearAllImageCaches(); + // Clear all favicons in Places. + await clearAllPlacesFavicons(); + + // Create a private browsing window. + let privateWindow = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + let pageURI = Services.io.newURI(TEST_PAGE); + + // Generate two random cookies for non-private window and private window + // respectively. + let cookies = []; + cookies.push(Math.random().toString()); + cookies.push(Math.random().toString()); + + // Open a tab in private window and add a cookie into it. + await assignCookies(privateWindow.gBrowser, TEST_SITE, cookies[0]); + + // Open a tab in non-private window and add a cookie into it. + await assignCookies(gBrowser, TEST_SITE, cookies[1]); + + // Add the observer earlier in case we don't capture events in time. + let promiseObserveFavicon = observeFavicon(true, cookies[0], pageURI); + + // The page must be bookmarked for favicon requests to go through in PB mode. + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: TEST_PAGE, + }); + + // Open a tab for the private window. + let tabInfo = await openTab(privateWindow.gBrowser, TEST_PAGE); + + info("Waiting until favicon requests are all made in private window."); + await promiseObserveFavicon; + + // Close the tab. + BrowserTestUtils.removeTab(tabInfo.tab); + // FIXME: We need to wait for the next event tick here to avoid observing + // the previous tab info in the next step (bug 1446725). + await new Promise(executeSoon); + + // Add the observer earlier in case we don't capture events in time. + promiseObserveFavicon = observeFavicon(false, cookies[1], pageURI); + + // Open a tab for the non-private window. + tabInfo = await openTab(gBrowser, TEST_PAGE); + + info("Waiting until favicon requests are all made in non-private window."); + await promiseObserveFavicon; + + // Close the tab. + BrowserTestUtils.removeTab(tabInfo.tab); + await BrowserTestUtils.closeWindow(privateWindow); +}); + +add_task(async function test_favicon_cache_privateBrowsing() { + // Clear all image caches and network cache before running the test. + clearAllImageCaches(); + + Services.cache2.clear(); + + // Clear all favicons in Places. + await clearAllPlacesFavicons(); + + // Add an observer for making sure the favicon has been loaded and cached. + let promiseFaviconLoaded = waitOnFaviconLoaded(FAVICON_CACHE_URI); + let promiseFaviconResponse = waitOnFaviconResponse(FAVICON_CACHE_URI); + + // Open a tab for the non-private window. + let tabInfoNonPrivate = await openTab(gBrowser, TEST_CACHE_PAGE); + + let response = await promiseFaviconResponse; + + await promiseFaviconLoaded; + + // Check that the favicon response has come from the network and it has the + // correct privateBrowsingId. + is( + response.topic, + "http-on-examine-response", + "The favicon image should be loaded through network." + ); + is( + response.privateBrowsingId, + 0, + "We should observe the network response for the non-private tab." + ); + + // Create a private browsing window. + let privateWindow = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + + // The page must be bookmarked for favicon requests to go through in PB mode. + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: TEST_CACHE_PAGE, + }); + + promiseFaviconResponse = waitOnFaviconResponse(FAVICON_CACHE_URI); + + // Open a tab for the private window. + let tabInfoPrivate = await openTab(privateWindow.gBrowser, TEST_CACHE_PAGE); + + // Wait for the favicon response of the private tab. + response = await promiseFaviconResponse; + + // Make sure the favicon is loaded through the network and its privateBrowsingId is correct. + is( + response.topic, + "http-on-examine-response", + "The favicon image should be loaded through the network again." + ); + is( + response.privateBrowsingId, + 1, + "We should observe the network response for the private tab." + ); + + BrowserTestUtils.removeTab(tabInfoPrivate.tab); + BrowserTestUtils.removeTab(tabInfoNonPrivate.tab); + await BrowserTestUtils.closeWindow(privateWindow); +}); diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_geoprompt_page.html b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_geoprompt_page.html new file mode 100644 index 0000000000..01ed3f3d2c --- /dev/null +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_geoprompt_page.html @@ -0,0 +1,13 @@ + + + + Geolocation invoker + + + + + diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_history_shift_click.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_history_shift_click.js new file mode 100644 index 0000000000..793bcd1a5d --- /dev/null +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_history_shift_click.js @@ -0,0 +1,69 @@ +/* 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/. */ + +add_task(async function () { + await testShiftClickOpensNewWindow("back-button"); +}); + +add_task(async function () { + await testShiftClickOpensNewWindow("forward-button"); +}); + +// Create new private browser, open new tab and set history state, then return the window +async function createPrivateWindow() { + const privateWindow = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + await BrowserTestUtils.openNewForegroundTab( + privateWindow.gBrowser, + "http://example.com" + ); + await SpecialPowers.spawn( + privateWindow.gBrowser.selectedBrowser, + [], + async function () { + content.history.pushState({}, "first item", "first-item.html"); + content.history.pushState({}, "second item", "second-item.html"); + content.history.pushState({}, "third item", "third-item.html"); + content.history.back(); + } + ); + await TestUtils.topicObserved("sessionstore-state-write-complete"); + + // Wait for the session data to be flushed before continuing the test + await new Promise(resolve => + SessionStore.getSessionHistory(privateWindow.gBrowser.selectedTab, resolve) + ); + + info("Private window created"); + + return privateWindow; +} + +async function testShiftClickOpensNewWindow(buttonId) { + const privateWindow = await createPrivateWindow(); + + const button = privateWindow.document.getElementById(buttonId); + // Wait for the new private window to be created after click + const newPrivateWindowPromise = BrowserTestUtils.waitForNewWindow(); + + EventUtils.synthesizeMouseAtCenter(button, { shiftKey: true }, privateWindow); + + info("Waiting for new private browser to open"); + + const newPrivateWindow = await newPrivateWindowPromise; + + ok( + PrivateBrowsingUtils.isBrowserPrivate(newPrivateWindow.gBrowser), + "New window is private" + ); + + // Cleanup + await Promise.all([ + BrowserTestUtils.closeWindow(privateWindow), + BrowserTestUtils.closeWindow(newPrivateWindow), + ]); + + info("Closed all windows"); +} diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_last_private_browsing_context_exited.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_last_private_browsing_context_exited.js new file mode 100644 index 0000000000..1fd28d4ca6 --- /dev/null +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_last_private_browsing_context_exited.js @@ -0,0 +1,66 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function test_no_notification_when_pb_autostart() { + let observedLastPBContext = false; + let observerExited = { + observe(aSubject, aTopic, aData) { + observedLastPBContext = true; + }, + }; + Services.obs.addObserver(observerExited, "last-pb-context-exited"); + + await SpecialPowers.pushPrefEnv({ + set: [["browser.privatebrowsing.autostart", true]], + }); + + let win = await BrowserTestUtils.openNewBrowserWindow(); + + let browser = win.gBrowser.selectedTab.linkedBrowser; + ok(browser.browsingContext.usePrivateBrowsing, "should use private browsing"); + + await BrowserTestUtils.closeWindow(win); + + await SpecialPowers.popPrefEnv(); + Services.obs.removeObserver(observerExited, "last-pb-context-exited"); + ok(!observedLastPBContext, "No last-pb-context-exited notification seen"); +}); + +add_task(async function test_notification_when_about_preferences() { + let observedLastPBContext = false; + let observerExited = { + observe(aSubject, aTopic, aData) { + observedLastPBContext = true; + }, + }; + Services.obs.addObserver(observerExited, "last-pb-context-exited"); + + let win = await BrowserTestUtils.openNewBrowserWindow({ private: true }); + + let browser = win.gBrowser.selectedTab.linkedBrowser; + ok(browser.browsingContext.usePrivateBrowsing, "should use private browsing"); + ok(browser.browsingContext.isContent, "should be content browsing context"); + + let tab = await BrowserTestUtils.addTab(win.gBrowser, "about:preferences"); + ok( + tab.linkedBrowser.browsingContext.usePrivateBrowsing, + "should use private browsing" + ); + ok( + tab.linkedBrowser.browsingContext.isContent, + "should be content browsing context" + ); + + let tabClose = BrowserTestUtils.waitForTabClosing(win.gBrowser.selectedTab); + BrowserTestUtils.removeTab(win.gBrowser.selectedTab); + await tabClose; + + ok(!observedLastPBContext, "No last-pb-context-exited notification seen"); + + await BrowserTestUtils.closeWindow(win); + + Services.obs.removeObserver(observerExited, "last-pb-context-exited"); + ok(observedLastPBContext, "No last-pb-context-exited notification seen"); +}); diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_lastpbcontextexited.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_lastpbcontextexited.js new file mode 100644 index 0000000000..c46417933a --- /dev/null +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_lastpbcontextexited.js @@ -0,0 +1,63 @@ +/* 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/. */ + +function test() { + // We need to open a new window for this so that its docshell would get destroyed + // when clearing the PB mode flag. + function runTest(aCloseWindow, aCallback) { + let newWin = OpenBrowserWindow({ private: true }); + SimpleTest.waitForFocus(function () { + let expectedExiting = true; + let expectedExited = false; + let observerExiting = { + observe(aSubject, aTopic, aData) { + is( + aTopic, + "last-pb-context-exiting", + "Correct topic should be dispatched (exiting)" + ); + is(expectedExiting, true, "notification not expected yet (exiting)"); + expectedExited = true; + Services.obs.removeObserver( + observerExiting, + "last-pb-context-exiting" + ); + }, + }; + let observerExited = { + observe(aSubject, aTopic, aData) { + is( + aTopic, + "last-pb-context-exited", + "Correct topic should be dispatched (exited)" + ); + is(expectedExited, true, "notification not expected yet (exited)"); + Services.obs.removeObserver(observerExited, "last-pb-context-exited"); + aCallback(); + }, + }; + Services.obs.addObserver(observerExiting, "last-pb-context-exiting"); + Services.obs.addObserver(observerExited, "last-pb-context-exited"); + expectedExiting = true; + aCloseWindow(newWin); + newWin = null; + SpecialPowers.forceGC(); + }, newWin); + } + + waitForExplicitFinish(); + + runTest( + function (newWin) { + // Simulate pressing the window close button + newWin.document.getElementById("cmd_closeWindow").doCommand(); + }, + function () { + runTest(function (newWin) { + // Simulate closing the last tab + newWin.document.getElementById("cmd_close").doCommand(); + }, finish); + } + ); +} diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_localStorage.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_localStorage.js new file mode 100644 index 0000000000..7cf07fd601 --- /dev/null +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_localStorage.js @@ -0,0 +1,28 @@ +/* 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/. */ + +add_task(async function test() { + requestLongerTimeout(2); + const page1 = + "http://mochi.test:8888/browser/browser/components/privatebrowsing/test/browser/" + + "browser_privatebrowsing_localStorage_page1.html"; + + let win = await BrowserTestUtils.openNewBrowserWindow({ private: true }); + + win.gBrowser.selectedTab = BrowserTestUtils.addTab(win.gBrowser, page1); + let browser = win.gBrowser.selectedBrowser; + await BrowserTestUtils.browserLoaded(browser); + + BrowserTestUtils.startLoadingURIString( + browser, + "http://mochi.test:8888/browser/browser/components/privatebrowsing/test/browser/" + + "browser_privatebrowsing_localStorage_page2.html" + ); + await BrowserTestUtils.browserLoaded(browser); + + is(browser.contentTitle, "2", "localStorage should contain 2 items"); + + // Cleanup + await BrowserTestUtils.closeWindow(win); +}); diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_localStorage_before_after.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_localStorage_before_after.js new file mode 100644 index 0000000000..3537236068 --- /dev/null +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_localStorage_before_after.js @@ -0,0 +1,46 @@ +/* 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/. */ + +// Ensure that a storage instance used by both private and public sessions at different times does not +// allow any data to leak due to cached values. + +// Step 1: Load browser_privatebrowsing_localStorage_before_after_page.html in a private tab, causing a storage +// item to exist. Close the tab. +// Step 2: Load the same page in a non-private tab, ensuring that the storage instance reports only one item +// existing. + +add_task(async function test() { + let prefix = + "http://mochi.test:8888/browser/browser/components/privatebrowsing/test/browser/"; + + // Step 1. + let privateWin = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + let testURL = + prefix + "browser_privatebrowsing_localStorage_before_after_page.html"; + await BrowserTestUtils.openNewForegroundTab(privateWin.gBrowser, testURL); + + is( + privateWin.gBrowser.selectedBrowser.contentTitle, + "1", + "localStorage should contain 1 item" + ); + + // Step 2. + let win = await BrowserTestUtils.openNewBrowserWindow(); + testURL = + prefix + "browser_privatebrowsing_localStorage_before_after_page2.html"; + await BrowserTestUtils.openNewForegroundTab(win.gBrowser, testURL); + + is( + win.gBrowser.selectedBrowser.contentTitle, + "null|0", + "localStorage should contain 0 items" + ); + + // Cleanup + await BrowserTestUtils.closeWindow(privateWin); + await BrowserTestUtils.closeWindow(win); +}); diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_localStorage_before_after_page.html b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_localStorage_before_after_page.html new file mode 100644 index 0000000000..0fcb3f89be --- /dev/null +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_localStorage_before_after_page.html @@ -0,0 +1,11 @@ + + + + + + + diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_localStorage_before_after_page2.html b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_localStorage_before_after_page2.html new file mode 100644 index 0000000000..4eccebdf48 --- /dev/null +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_localStorage_before_after_page2.html @@ -0,0 +1,10 @@ + + + + + + + diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_localStorage_page1.html b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_localStorage_page1.html new file mode 100644 index 0000000000..ecf5507e0a --- /dev/null +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_localStorage_page1.html @@ -0,0 +1,10 @@ + + + + + + + diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_localStorage_page2.html b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_localStorage_page2.html new file mode 100644 index 0000000000..d49c7fea29 --- /dev/null +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_localStorage_page2.html @@ -0,0 +1,10 @@ + + + + + + + diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_newtab_from_popup.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_newtab_from_popup.js new file mode 100644 index 0000000000..a46c44b0b8 --- /dev/null +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_newtab_from_popup.js @@ -0,0 +1,71 @@ +/** + * Tests that a popup window in private browsing window opens + * new tab links in the original private browsing window as + * new tabs. + * + * This is a regression test for bug 1202634. + */ + +// We're able to sidestep some quote-escaping issues when +// nesting data URI's by encoding the second data URI in +// base64. +const POPUP_BODY_BASE64 = btoa(` + Now click this + `); +const POPUP_LINK = `data:text/html;charset=utf-8;base64,${POPUP_BODY_BASE64}`; +const WINDOW_BODY = `data:text/html, + + First click this. + `; + +add_task(async function test_private_popup_window_opens_private_tabs() { + // allow top level data: URI navigations, otherwise clicking a data: link fails + await SpecialPowers.pushPrefEnv({ + set: [["security.data_uri.block_toplevel_data_uri_navigations", false]], + }); + let privWin = await BrowserTestUtils.openNewBrowserWindow({ private: true }); + + // Sanity check - this browser better be private. + ok( + PrivateBrowsingUtils.isWindowPrivate(privWin), + "Opened a private browsing window." + ); + + // First, open a private browsing window, and load our + // testing page. + let privBrowser = privWin.gBrowser.selectedBrowser; + BrowserTestUtils.startLoadingURIString(privBrowser, WINDOW_BODY); + await BrowserTestUtils.browserLoaded(privBrowser); + + // Next, click on the link in the testing page, and ensure + // that a private popup window is opened. + let openedPromise = BrowserTestUtils.waitForNewWindow({ url: POPUP_LINK }); + + await BrowserTestUtils.synthesizeMouseAtCenter("#first", {}, privBrowser); + let popupWin = await openedPromise; + ok( + PrivateBrowsingUtils.isWindowPrivate(popupWin), + "Popup window was private." + ); + + // Now click on the link in the popup, and ensure that a new + // tab is opened in the original private browsing window. + let newTabPromise = BrowserTestUtils.waitForNewTab(privWin.gBrowser); + let popupBrowser = popupWin.gBrowser.selectedBrowser; + await BrowserTestUtils.synthesizeMouseAtCenter("#second", {}, popupBrowser); + let newPrivTab = await newTabPromise; + + // Ensure that the newly created tab's browser is private. + ok( + PrivateBrowsingUtils.isBrowserPrivate(newPrivTab.linkedBrowser), + "Newly opened tab should be private." + ); + + // Clean up + BrowserTestUtils.removeTab(newPrivTab); + await BrowserTestUtils.closeWindow(popupWin); + await BrowserTestUtils.closeWindow(privWin); +}); diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_noSessionRestoreMenuOption.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_noSessionRestoreMenuOption.js new file mode 100644 index 0000000000..17ea34d1aa --- /dev/null +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_noSessionRestoreMenuOption.js @@ -0,0 +1,29 @@ +"use strict"; + +/** + * Tests that if we open a tab within a private browsing window, and then + * close that private browsing window, that subsequent private browsing + * windows do not allow the command for restoring the last session. + */ +add_task(async function test_no_session_restore_menu_option() { + let win = await BrowserTestUtils.openNewBrowserWindow({ private: true }); + ok(true, "The first private window got loaded"); + BrowserTestUtils.addTab(win.gBrowser, "about:mozilla"); + await BrowserTestUtils.closeWindow(win); + + win = await BrowserTestUtils.openNewBrowserWindow({ private: true }); + let srCommand = win.document.getElementById("Browser:RestoreLastSession"); + ok(srCommand, "The Session Restore command should exist"); + is( + PrivateBrowsingUtils.isWindowPrivate(win), + true, + "PrivateBrowsingUtils should report the correct per-window private browsing status" + ); + is( + srCommand.hasAttribute("disabled"), + true, + "The Session Restore command should be disabled in private browsing mode" + ); + + await BrowserTestUtils.closeWindow(win); +}); diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_nonbrowser.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_nonbrowser.js new file mode 100644 index 0000000000..24586d7464 --- /dev/null +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_nonbrowser.js @@ -0,0 +1,21 @@ +"use strict"; + +/** + * Tests that we fire the last-pb-context-exited observer notification + * when the last private browsing window closes, even if a chrome window + * was opened from that private browsing window. + */ +add_task(async function () { + let win = await BrowserTestUtils.openNewBrowserWindow({ private: true }); + let chromeWin = win.open( + "chrome://browser/content/places/places.xhtml", + "_blank", + "chrome,extrachrome,menubar,resizable,scrollbars,status,toolbar" + ); + await BrowserTestUtils.waitForEvent(chromeWin, "load"); + let obsPromise = TestUtils.topicObserved("last-pb-context-exited"); + await BrowserTestUtils.closeWindow(win); + await obsPromise; + Assert.ok(true, "Got the last-pb-context-exited notification"); + chromeWin.close(); +}); diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_opendir.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_opendir.js new file mode 100644 index 0000000000..146ab82628 --- /dev/null +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_opendir.js @@ -0,0 +1,175 @@ +/* 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/. */ + +// This test makes sure that the last open directory used inside the private +// browsing mode is not remembered after leaving that mode. + +var windowsToClose = []; +function testOnWindow(options, callback) { + var win = OpenBrowserWindow(options); + win.addEventListener( + "load", + function () { + windowsToClose.push(win); + callback(win); + }, + { once: true } + ); +} + +registerCleanupFunction(function () { + windowsToClose.forEach(function (win) { + win.close(); + }); +}); + +function test() { + // initialization + waitForExplicitFinish(); + let dir1 = Services.dirsvc.get("ProfD", Ci.nsIFile); + let dir2 = Services.dirsvc.get("TmpD", Ci.nsIFile); + let file = dir2.clone(); + file.append("pbtest.file"); + file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600); + + const kPrefName = "browser.open.lastDir"; + + function setupCleanSlate(win) { + win.gLastOpenDirectory.reset(); + Services.prefs.clearUserPref(kPrefName); + } + + setupCleanSlate(window); + + // open one regular and one private window + testOnWindow(undefined, function (nonPrivateWindow) { + setupCleanSlate(nonPrivateWindow); + testOnWindow({ private: true }, function (privateWindow) { + setupCleanSlate(privateWindow); + + // Test 1: general workflow test + + // initial checks + ok( + !nonPrivateWindow.gLastOpenDirectory.path, + "Last open directory path should be initially empty" + ); + nonPrivateWindow.gLastOpenDirectory.path = dir2; + is( + nonPrivateWindow.gLastOpenDirectory.path.path, + dir2.path, + "The path should be successfully set" + ); + nonPrivateWindow.gLastOpenDirectory.path = null; + is( + nonPrivateWindow.gLastOpenDirectory.path.path, + dir2.path, + "The path should be not change when assigning it to null" + ); + nonPrivateWindow.gLastOpenDirectory.path = dir1; + is( + nonPrivateWindow.gLastOpenDirectory.path.path, + dir1.path, + "The path should be successfully outside of the private browsing mode" + ); + + // test the private window + is( + privateWindow.gLastOpenDirectory.path.path, + dir1.path, + "The path should not change when entering the private browsing mode" + ); + privateWindow.gLastOpenDirectory.path = dir2; + is( + privateWindow.gLastOpenDirectory.path.path, + dir2.path, + "The path should successfully change inside the private browsing mode" + ); + + // test the non-private window + is( + nonPrivateWindow.gLastOpenDirectory.path.path, + dir1.path, + "The path should be reset to the same path as before entering the private browsing mode" + ); + + setupCleanSlate(nonPrivateWindow); + setupCleanSlate(privateWindow); + + // Test 2: the user first tries to open a file inside the private browsing mode + + // test the private window + ok( + !privateWindow.gLastOpenDirectory.path, + "No original path should exist inside the private browsing mode" + ); + privateWindow.gLastOpenDirectory.path = dir1; + is( + privateWindow.gLastOpenDirectory.path.path, + dir1.path, + "The path should be successfully set inside the private browsing mode" + ); + // test the non-private window + ok( + !nonPrivateWindow.gLastOpenDirectory.path, + "The path set inside the private browsing mode should not leak when leaving that mode" + ); + + setupCleanSlate(nonPrivateWindow); + setupCleanSlate(privateWindow); + + // Test 3: the last open directory is set from a previous session, it should be used + // in normal mode + + Services.prefs.setComplexValue(kPrefName, Ci.nsIFile, dir1); + is( + nonPrivateWindow.gLastOpenDirectory.path.path, + dir1.path, + "The pref set from last session should take effect outside the private browsing mode" + ); + + setupCleanSlate(nonPrivateWindow); + setupCleanSlate(privateWindow); + + // Test 4: the last open directory is set from a previous session, it should be used + // in private browsing mode mode + + Services.prefs.setComplexValue(kPrefName, Ci.nsIFile, dir1); + // test the private window + is( + privateWindow.gLastOpenDirectory.path.path, + dir1.path, + "The pref set from last session should take effect inside the private browsing mode" + ); + // test the non-private window + is( + nonPrivateWindow.gLastOpenDirectory.path.path, + dir1.path, + "The pref set from last session should remain in effect after leaving the private browsing mode" + ); + + setupCleanSlate(nonPrivateWindow); + setupCleanSlate(privateWindow); + + // Test 5: setting the path to a file shouldn't work + + nonPrivateWindow.gLastOpenDirectory.path = file; + ok( + !nonPrivateWindow.gLastOpenDirectory.path, + "Setting the path to a file shouldn't work when it's originally null" + ); + nonPrivateWindow.gLastOpenDirectory.path = dir1; + nonPrivateWindow.gLastOpenDirectory.path = file; + is( + nonPrivateWindow.gLastOpenDirectory.path.path, + dir1.path, + "Setting the path to a file shouldn't work when it's not originally null" + ); + + // cleanup + file.remove(false); + finish(); + }); + }); +} diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_placesTitleNoUpdate.html b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_placesTitleNoUpdate.html new file mode 100644 index 0000000000..f5bb3212f8 --- /dev/null +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_placesTitleNoUpdate.html @@ -0,0 +1,8 @@ + + + + Title 1 + + + + diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_placesTitleNoUpdate.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_placesTitleNoUpdate.js new file mode 100644 index 0000000000..855bfe4c41 --- /dev/null +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_placesTitleNoUpdate.js @@ -0,0 +1,59 @@ +/* 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/. */ +/* eslint-disable mozilla/no-arbitrary-setTimeout */ + +// Test to make sure that the visited page titles do not get updated inside the +// private browsing mode. +"use strict"; + +add_task(async function test() { + const TEST_URL = + "http://mochi.test:8888/browser/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_placesTitleNoUpdate.html"; + const TITLE_1 = "Title 1"; + const TITLE_2 = "Title 2"; + + await PlacesUtils.history.clear(); + + let promiseTitleChanged = PlacesTestUtils.waitForNotification( + "page-title-changed", + events => events[0].url == TEST_URL + ); + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + registerCleanupFunction(async () => { + BrowserTestUtils.removeTab(tab); + }); + info("Wait for a title change notification."); + await promiseTitleChanged; + await BrowserTestUtils.waitForCondition(async function () { + let entry = await PlacesUtils.history.fetch(TEST_URL); + return entry && entry.title == TITLE_1; + }, "The title matches the original title after first visit"); + + promiseTitleChanged = PlacesTestUtils.waitForNotification( + "page-title-changed", + events => events[0].url == TEST_URL + ); + await PlacesTestUtils.addVisits({ uri: TEST_URL, title: TITLE_2 }); + info("Wait for a title change notification."); + await promiseTitleChanged; + await BrowserTestUtils.waitForCondition(async function () { + let entry = await PlacesUtils.history.fetch(TEST_URL); + return entry && entry.title == TITLE_2; + }, "The title matches the original title after updating visit"); + + let privateWin = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + registerCleanupFunction(async () => { + await BrowserTestUtils.closeWindow(privateWin); + }); + await BrowserTestUtils.openNewForegroundTab(privateWin.gBrowser, TEST_URL); + // Wait long enough to be sure history didn't set a title. + await new Promise(resolve => setTimeout(resolve, 1000)); + is( + (await PlacesUtils.history.fetch(TEST_URL)).title, + TITLE_2, + "The title remains the same after visiting in private window" + ); +}); diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_placestitle.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_placestitle.js new file mode 100644 index 0000000000..af2c6aeb65 --- /dev/null +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_placestitle.js @@ -0,0 +1,82 @@ +/* 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/. */ +/* eslint-disable mozilla/no-arbitrary-setTimeout */ + +// This test makes sure that the title of existing history entries does not +// change inside a private window. + +add_task(async function test() { + const TEST_URL = + "http://mochi.test:8888/browser/browser/components/" + + "privatebrowsing/test/browser/title.sjs"; + let cm = Services.cookies; + + function cleanup() { + // delete all cookies + cm.removeAll(); + // delete all history items + return PlacesUtils.history.clear(); + } + + await cleanup(); + registerCleanupFunction(cleanup); + + let win = await BrowserTestUtils.openNewBrowserWindow(); + registerCleanupFunction(async () => { + await BrowserTestUtils.closeWindow(win); + }); + + let promiseTitleChanged = PlacesTestUtils.waitForNotification( + "page-title-changed", + events => events[0].url == TEST_URL + ); + await BrowserTestUtils.openNewForegroundTab(win.gBrowser, TEST_URL); + await promiseTitleChanged; + await BrowserTestUtils.waitForCondition(async function () { + let entry = await PlacesUtils.history.fetch(TEST_URL); + return entry && entry.title == "No Cookie"; + }, "The page should be loaded without any cookie for the first time"); + + promiseTitleChanged = PlacesTestUtils.waitForNotification( + "page-title-changed", + events => events[0].url == TEST_URL + ); + await BrowserTestUtils.openNewForegroundTab(win.gBrowser, TEST_URL); + await promiseTitleChanged; + await BrowserTestUtils.waitForCondition(async function () { + let entry = await PlacesUtils.history.fetch(TEST_URL); + return entry && entry.title == "Cookie"; + }, "The page should be loaded with a cookie for the second time"); + + await cleanup(); + + promiseTitleChanged = PlacesTestUtils.waitForNotification( + "page-title-changed", + events => events[0].url == TEST_URL + ); + await BrowserTestUtils.openNewForegroundTab(win.gBrowser, TEST_URL); + await promiseTitleChanged; + await BrowserTestUtils.waitForCondition(async function () { + let entry = await PlacesUtils.history.fetch(TEST_URL); + return entry && entry.title == "No Cookie"; + }, "The page should be loaded without any cookie again"); + + // Reopen the page in a private browser window, it should not notify a title + // change. + let win2 = await BrowserTestUtils.openNewBrowserWindow({ private: true }); + registerCleanupFunction(async () => { + let promisePBExit = TestUtils.topicObserved("last-pb-context-exited"); + await BrowserTestUtils.closeWindow(win2); + await promisePBExit; + }); + + await BrowserTestUtils.openNewForegroundTab(win2.gBrowser, TEST_URL); + // Wait long enough to be sure history didn't set a title. + await new Promise(resolve => setTimeout(resolve, 1000)); + is( + (await PlacesUtils.history.fetch(TEST_URL)).title, + "No Cookie", + "The title remains the same after visiting in private window" + ); +}); diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_protocolhandler.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_protocolhandler.js new file mode 100644 index 0000000000..4ab90e2079 --- /dev/null +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_protocolhandler.js @@ -0,0 +1,71 @@ +/* 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/. */ +/* eslint-disable mozilla/no-arbitrary-setTimeout */ + +// This test makes sure that the web pages can't register protocol handlers +// inside the private browsing mode. + +add_task(async function test() { + let notificationValue = "Protocol Registration: web+testprotocol"; + let testURI = + "https://example.com/browser/" + + "browser/components/privatebrowsing/test/browser/browser_privatebrowsing_protocolhandler_page.html"; + + let doTest = async function (aIsPrivateMode, aWindow) { + let tab = (aWindow.gBrowser.selectedTab = BrowserTestUtils.addTab( + aWindow.gBrowser, + testURI + )); + let notificationBox = aWindow.gBrowser.getNotificationBox(); + let notificationShownPromise; + if (!aIsPrivateMode) { + notificationShownPromise = BrowserTestUtils.waitForEvent( + notificationBox.stack, + "AlertActive", + false, + event => event.target.getAttribute("value") == notificationValue + ); + } + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await notificationShownPromise; + + let promiseFinished = Promise.withResolvers(); + setTimeout(function () { + let notification = + notificationBox.getNotificationWithValue(notificationValue); + + if (aIsPrivateMode) { + // Make sure the notification is correctly displayed without a remember control + ok( + !notification, + "Notification box should not be displayed inside of private browsing mode" + ); + } else { + // Make sure the notification is correctly displayed with a remember control + ok( + notification, + "Notification box should be displaying outside of private browsing mode" + ); + } + + promiseFinished.resolve(); + }, 100); // remember control is added in a setTimeout(0) call + + await promiseFinished.promise; + }; + + // test first when not on private mode + let win = await BrowserTestUtils.openNewBrowserWindow(); + await doTest(false, win); + + // then test when on private mode + let privateWin = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + await doTest(true, privateWin); + + // Cleanup + await BrowserTestUtils.closeWindow(win); + await BrowserTestUtils.closeWindow(privateWin); +}); diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_protocolhandler_page.html b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_protocolhandler_page.html new file mode 100644 index 0000000000..200fda0d42 --- /dev/null +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_protocolhandler_page.html @@ -0,0 +1,13 @@ + + + + Protocol registrar page + + + + + diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_rememberprompt.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_rememberprompt.js new file mode 100644 index 0000000000..2090742c91 --- /dev/null +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_rememberprompt.js @@ -0,0 +1,90 @@ +/* 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/. */ + +// This test makes sure that the geolocation prompt does not show a remember +// control inside the private browsing mode. + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["dom.vr.always_support_vr", true]], + }); +}); + +add_task(async function test() { + function checkPrompt(aURL, aName, aPrivateMode, aWindow) { + return (async function () { + aWindow.gBrowser.selectedTab = BrowserTestUtils.addTab( + aWindow.gBrowser, + aURL + ); + await BrowserTestUtils.browserLoaded(aWindow.gBrowser.selectedBrowser); + + let notification = aWindow.PopupNotifications.getNotification(aName); + + // Wait until the notification is available. + while (!notification) { + await new Promise(resolve => { + executeSoon(resolve); + }); + notification = aWindow.PopupNotifications.getNotification(aName); + } + + if (aPrivateMode) { + // Make sure the notification is correctly displayed without a remember control + ok( + !notification.options.checkbox.show, + "Secondary actions should not exist (always/never remember)" + ); + } else { + ok( + notification.options.checkbox.show, + "Secondary actions should exist (always/never remember)" + ); + } + notification.remove(); + + aWindow.gBrowser.removeCurrentTab(); + })(); + } + + function checkPrivateBrowsingRememberPrompt(aURL, aName) { + return (async function () { + let win = await BrowserTestUtils.openNewBrowserWindow(); + let browser = win.gBrowser.selectedBrowser; + BrowserTestUtils.startLoadingURIString(browser, aURL); + await BrowserTestUtils.browserLoaded(browser); + + await checkPrompt(aURL, aName, false, win); + + let privateWin = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + let privateBrowser = privateWin.gBrowser.selectedBrowser; + BrowserTestUtils.startLoadingURIString(privateBrowser, aURL); + await BrowserTestUtils.browserLoaded(privateBrowser); + + await checkPrompt(aURL, aName, true, privateWin); + + // Cleanup + await BrowserTestUtils.closeWindow(win); + await BrowserTestUtils.closeWindow(privateWin); + })(); + } + + const geoTestPageURL = + "https://example.com/browser/" + + "browser/components/privatebrowsing/test/browser/browser_privatebrowsing_geoprompt_page.html"; + + await checkPrivateBrowsingRememberPrompt(geoTestPageURL, "geolocation"); + + const vrEnabled = Services.prefs.getBoolPref("dom.vr.enabled"); + + if (vrEnabled) { + const xrTestPageURL = + "https://example.com/browser/" + + "browser/components/privatebrowsing/test/browser/browser_privatebrowsing_xrprompt_page.html"; + + await checkPrivateBrowsingRememberPrompt(xrTestPageURL, "xr"); + } +}); diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_resetPBM.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_resetPBM.js new file mode 100644 index 0000000000..a80d818c87 --- /dev/null +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_resetPBM.js @@ -0,0 +1,824 @@ +/* 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); +}); diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_sidebar.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_sidebar.js new file mode 100644 index 0000000000..58a333bfdb --- /dev/null +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_sidebar.js @@ -0,0 +1,88 @@ +/* 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/. */ + +// This test makes sure that Sidebars do not migrate across windows with +// different privacy states + +// See Bug 885054: https://bugzilla.mozilla.org/show_bug.cgi?id=885054 + +function test() { + waitForExplicitFinish(); + + // opens a sidebar + function openSidebar(win) { + return win.SidebarUI.show("viewBookmarksSidebar").then(() => win); + } + + let windowCache = []; + function cacheWindow(w) { + windowCache.push(w); + return w; + } + function closeCachedWindows() { + windowCache.forEach(w => w.close()); + } + + // Part 1: NON PRIVATE WINDOW -> PRIVATE WINDOW + openWindow(window, {}, 1) + .then(cacheWindow) + .then(openSidebar) + .then(win => openWindow(win, { private: true })) + .then(cacheWindow) + .then(function ({ document }) { + let sidebarBox = document.getElementById("sidebar-box"); + is( + sidebarBox.hidden, + true, + "Opening a private window from reg window does not open the sidebar" + ); + }) + .then(closeCachedWindows) + // Part 2: NON PRIVATE WINDOW -> NON PRIVATE WINDOW + .then(() => openWindow(window)) + .then(cacheWindow) + .then(openSidebar) + .then(win => openWindow(win)) + .then(cacheWindow) + .then(function ({ document }) { + let sidebarBox = document.getElementById("sidebar-box"); + is( + sidebarBox.hidden, + false, + "Opening a reg window from reg window does open the sidebar" + ); + }) + .then(closeCachedWindows) + // Part 3: PRIVATE WINDOW -> NON PRIVATE WINDOW + .then(() => openWindow(window, { private: true })) + .then(cacheWindow) + .then(openSidebar) + .then(win => openWindow(win)) + .then(cacheWindow) + .then(function ({ document }) { + let sidebarBox = document.getElementById("sidebar-box"); + is( + sidebarBox.hidden, + true, + "Opening a reg window from a private window does not open the sidebar" + ); + }) + .then(closeCachedWindows) + // Part 4: PRIVATE WINDOW -> PRIVATE WINDOW + .then(() => openWindow(window, { private: true })) + .then(cacheWindow) + .then(openSidebar) + .then(win => openWindow(win, { private: true })) + .then(cacheWindow) + .then(function ({ document }) { + let sidebarBox = document.getElementById("sidebar-box"); + is( + sidebarBox.hidden, + false, + "Opening a private window from private window does open the sidebar" + ); + }) + .then(closeCachedWindows) + .then(finish); +} diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_theming.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_theming.js new file mode 100644 index 0000000000..18b398e961 --- /dev/null +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_theming.js @@ -0,0 +1,46 @@ +/* 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/. */ + +// This test makes sure that privatebrowsingmode attribute of the window is correctly +// adjusted based on whether the window is a private window. + +var windowsToClose = []; +function testOnWindow(options, callback) { + var win = OpenBrowserWindow(options); + win.addEventListener( + "load", + function () { + windowsToClose.push(win); + executeSoon(() => callback(win)); + }, + { once: true } + ); +} + +registerCleanupFunction(function () { + windowsToClose.forEach(function (win) { + win.close(); + }); +}); + +function test() { + // initialization + waitForExplicitFinish(); + + ok( + !document.documentElement.hasAttribute("privatebrowsingmode"), + "privatebrowsingmode should not be present in normal mode" + ); + + // open a private window + testOnWindow({ private: true }, function (win) { + is( + win.document.documentElement.getAttribute("privatebrowsingmode"), + "temporary", + 'privatebrowsingmode should be "temporary" inside the private browsing mode' + ); + + finish(); + }); +} diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_ui.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_ui.js new file mode 100644 index 0000000000..ab74caeb5e --- /dev/null +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_ui.js @@ -0,0 +1,102 @@ +/* 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/. */ + +// This test makes sure that the gPrivateBrowsingUI object, the Private Browsing +// menu item and its XUL element work correctly. + +function test() { + // initialization + waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({ + set: [["security.allow_eval_with_system_principal", true]], + }); + let windowsToClose = []; + let testURI = "about:blank"; + let pbMenuItem; + let cmd; + + function doTest(aIsPrivateMode, aWindow, aCallback) { + BrowserTestUtils.browserLoaded(aWindow.gBrowser.selectedBrowser).then( + function () { + ok(aWindow.gPrivateBrowsingUI, "The gPrivateBrowsingUI object exists"); + + pbMenuItem = aWindow.document.getElementById("menu_newPrivateWindow"); + ok(pbMenuItem, "The Private Browsing menu item exists"); + + cmd = aWindow.document.getElementById("Tools:PrivateBrowsing"); + isnot( + cmd, + null, + "XUL command object for the private browsing service exists" + ); + + is( + pbMenuItem.getAttribute("label"), + "New Private Window", + 'The Private Browsing menu item should read "New Private Window"' + ); + is( + PrivateBrowsingUtils.isWindowPrivate(aWindow), + aIsPrivateMode, + "PrivateBrowsingUtils should report the correct per-window private browsing status (privateBrowsing should be " + + aIsPrivateMode + + ")" + ); + + aCallback(); + } + ); + + BrowserTestUtils.startLoadingURIString( + aWindow.gBrowser.selectedBrowser, + testURI + ); + } + + function openPrivateBrowsingModeByUI(aWindow, aCallback) { + Services.obs.addObserver(function observer(aSubject, aTopic, aData) { + aSubject.addEventListener( + "load", + function () { + Services.obs.removeObserver(observer, "domwindowopened"); + windowsToClose.push(aSubject); + aCallback(aSubject); + }, + { once: true } + ); + }, "domwindowopened"); + + cmd = aWindow.document.getElementById("Tools:PrivateBrowsing"); + var func = new Function("", cmd.getAttribute("oncommand")); + func.call(cmd); + } + + function testOnWindow(aOptions, aCallback) { + whenNewWindowLoaded(aOptions, function (aWin) { + windowsToClose.push(aWin); + // execute should only be called when need, like when you are opening + // web pages on the test. If calling executeSoon() is not necesary, then + // call whenNewWindowLoaded() instead of testOnWindow() on your test. + executeSoon(() => aCallback(aWin)); + }); + } + + // this function is called after calling finish() on the test. + registerCleanupFunction(function () { + windowsToClose.forEach(function (aWin) { + aWin.close(); + }); + }); + + // test first when not on private mode + testOnWindow({}, function (aWin) { + doTest(false, aWin, function () { + // then test when on private mode, opening a new private window from the + // user interface. + openPrivateBrowsingModeByUI(aWin, function (aPrivateWin) { + doTest(true, aPrivateWin, finish); + }); + }); + }); +} diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_urlbarfocus.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_urlbarfocus.js new file mode 100644 index 0000000000..cc4c0585de --- /dev/null +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_urlbarfocus.js @@ -0,0 +1,44 @@ +/* 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/. */ + +// This test makes sure that the URL bar is focused when entering the private window. + +"use strict"; + +function checkUrlbarFocus(win) { + let urlbar = win.gURLBar; + is( + win.document.activeElement, + urlbar.inputField, + "URL Bar should be focused" + ); + is(urlbar.value, "", "URL Bar should be empty"); +} + +function openNewPrivateWindow() { + return new Promise(resolve => { + whenNewWindowLoaded({ private: true }, win => { + executeSoon(() => resolve(win)); + }); + }); +} + +add_task(async function () { + let win = await openNewPrivateWindow(); + checkUrlbarFocus(win); + win.close(); +}); + +add_task(async function () { + AboutNewTab.newTabURL = "about:blank"; + registerCleanupFunction(() => { + AboutNewTab.resetNewTabURL(); + }); + + let win = await openNewPrivateWindow(); + checkUrlbarFocus(win); + win.close(); + + AboutNewTab.resetNewTabURL(); +}); diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_windowtitle.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_windowtitle.js new file mode 100644 index 0000000000..de99b19a44 --- /dev/null +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_windowtitle.js @@ -0,0 +1,140 @@ +/* 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/. */ + +// This test makes sure that the window title changes correctly while switching +// from and to private browsing mode. + +"use strict"; + +add_task(async function test() { + const testPageURL = + "http://mochi.test:8888/browser/" + + "browser/components/privatebrowsing/test/browser/browser_privatebrowsing_windowtitle_page.html"; + requestLongerTimeout(2); + + // initialization of expected titles + let test_title = "Test title"; + let app_name = document.title; + + // XXX: Bug 1597849 - Dehardcode titles by fetching them from Fluent + // to compare with the actual values. + const isMacOS = AppConstants.platform == "macosx"; + + let pb_postfix = isMacOS ? ` — Private Browsing` : ` Private Browsing`; + let page_with_title = isMacOS ? test_title : `${test_title} — ${app_name}`; + let page_without_title = app_name; + let about_pb_title = app_name; + let pb_page_with_title = isMacOS + ? `${test_title}${pb_postfix}` + : `${test_title} — ${app_name}${pb_postfix}`; + let pb_page_without_title = `${app_name}${pb_postfix}`; + let pb_about_pb_title = `${app_name}${pb_postfix}`; + + async function testTabTitle(aWindow, url, insidePB, expected_title) { + let tab = await BrowserTestUtils.openNewForegroundTab(aWindow.gBrowser); + BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, url); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + + await BrowserTestUtils.waitForCondition(() => { + return aWindow.document.title === expected_title; + }, `Window title should be ${expected_title}, got ${aWindow.document.title}`); + + is( + aWindow.document.title, + expected_title, + "The window title for " + + url + + " is correct (" + + (insidePB ? "inside" : "outside") + + " private browsing mode)" + ); + + let win = aWindow.gBrowser.replaceTabWithWindow(tab); + await BrowserTestUtils.waitForEvent(win, "load", false); + + await BrowserTestUtils.waitForCondition(() => { + return win.document.title === expected_title; + }, `Window title should be ${expected_title}, got ${win.document.title}`); + + is( + win.document.title, + expected_title, + "The window title for " + + url + + " detached tab is correct (" + + (insidePB ? "inside" : "outside") + + " private browsing mode)" + ); + + await Promise.all([ + BrowserTestUtils.closeWindow(win), + BrowserTestUtils.closeWindow(aWindow), + ]); + } + + function openWin(isPrivate) { + return BrowserTestUtils.openNewBrowserWindow({ private: isPrivate }); + } + await testTabTitle( + await openWin(false), + "about:blank", + false, + page_without_title + ); + await testTabTitle(await openWin(false), testPageURL, false, page_with_title); + await testTabTitle( + await openWin(false), + "about:privatebrowsing", + false, + about_pb_title + ); + await testTabTitle( + await openWin(true), + "about:blank", + true, + pb_page_without_title + ); + await testTabTitle( + await openWin(true), + testPageURL, + true, + pb_page_with_title + ); + await testTabTitle( + await openWin(true), + "about:privatebrowsing", + true, + pb_about_pb_title + ); + + await SpecialPowers.pushPrefEnv({ + set: [["privacy.exposeContentTitleInWindow.pbm", false]], + }); + await testTabTitle(await openWin(false), testPageURL, false, page_with_title); + await testTabTitle( + await openWin(true), + testPageURL, + true, + pb_page_without_title + ); + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.exposeContentTitleInWindow", false], + ["privacy.exposeContentTitleInWindow.pbm", true], + ], + }); + await testTabTitle( + await openWin(false), + testPageURL, + false, + page_without_title + ); + // The generic preference set to false is intended to override the PBM one + await testTabTitle( + await openWin(true), + testPageURL, + true, + pb_page_without_title + ); +}); diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_windowtitle_page.html b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_windowtitle_page.html new file mode 100644 index 0000000000..760bde7d14 --- /dev/null +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_windowtitle_page.html @@ -0,0 +1,9 @@ + + + + Test title + + + Test page for the window title test + + diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_xrprompt_page.html b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_xrprompt_page.html new file mode 100644 index 0000000000..4330785df2 --- /dev/null +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_xrprompt_page.html @@ -0,0 +1,11 @@ + + + + XR invoker + + + + + diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_zoom.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_zoom.js new file mode 100644 index 0000000000..048796e7d2 --- /dev/null +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_zoom.js @@ -0,0 +1,46 @@ +/* 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/. */ + +// This test makes sure that private browsing turns off doesn't cause zoom +// settings to be reset on tab switch (bug 464962) + +add_task(async function test() { + let win = await BrowserTestUtils.openNewBrowserWindow({ private: true }); + let tabAbout = await BrowserTestUtils.openNewForegroundTab( + win.gBrowser, + "about:mozilla" + ); + let tabMozilla = await BrowserTestUtils.openNewForegroundTab( + win.gBrowser, + "about:mozilla" + ); + + let mozillaZoom = win.ZoomManager.zoom; + + // change the zoom on the mozilla page + win.FullZoom.enlarge(); + // make sure the zoom level has been changed + isnot(win.ZoomManager.zoom, mozillaZoom, "Zoom level can be changed"); + mozillaZoom = win.ZoomManager.zoom; + + // switch to about: tab + await BrowserTestUtils.switchTab(win.gBrowser, tabAbout); + + // switch back to mozilla tab + await BrowserTestUtils.switchTab(win.gBrowser, tabMozilla); + + // make sure the zoom level has not changed + is( + win.ZoomManager.zoom, + mozillaZoom, + "Entering private browsing should not reset the zoom on a tab" + ); + + // cleanup + win.FullZoom.reset(); + BrowserTestUtils.removeTab(tabMozilla); + BrowserTestUtils.removeTab(tabAbout); + + await BrowserTestUtils.closeWindow(win); +}); diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_zoomrestore.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_zoomrestore.js new file mode 100644 index 0000000000..dd0e2e1b64 --- /dev/null +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_zoomrestore.js @@ -0,0 +1,80 @@ +/* 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/. */ + +// This test makes sure that about:privatebrowsing does not appear zoomed in +// if there is already a zoom site pref for about:blank (bug 487656). + +add_task(async function test() { + // initialization + let windowsToClose = []; + let windowsToReset = []; + + function promiseLocationChange() { + return new Promise(resolve => { + Services.obs.addObserver(function onLocationChange(subj, topic, data) { + Services.obs.removeObserver(onLocationChange, topic); + resolve(); + }, "browser-fullZoom:location-change"); + }); + } + + async function promiseTestReady(aIsZoomedWindow, aWindow) { + // Need to wait on two things, the ordering of which is not guaranteed: + // (1) the page load, and (2) FullZoom's update to the new page's zoom + // level. FullZoom broadcasts "browser-fullZoom:location-change" when its + // update is done. (See bug 856366 for details.) + + let browser = aWindow.gBrowser.selectedBrowser; + BrowserTestUtils.startLoadingURIString(browser, "about:blank"); + await Promise.all([ + BrowserTestUtils.browserLoaded(browser), + promiseLocationChange(), + ]); + doTest(aIsZoomedWindow, aWindow); + } + + function doTest(aIsZoomedWindow, aWindow) { + if (aIsZoomedWindow) { + is( + aWindow.ZoomManager.zoom, + 1, + "Zoom level for freshly loaded about:blank should be 1" + ); + // change the zoom on the blank page + aWindow.FullZoom.enlarge(); + isnot( + aWindow.ZoomManager.zoom, + 1, + "Zoom level for about:blank should be changed" + ); + return; + } + + // make sure the zoom level is set to 1 + is( + aWindow.ZoomManager.zoom, + 1, + "Zoom level for about:privatebrowsing should be reset" + ); + } + + function testOnWindow(options, callback) { + return BrowserTestUtils.openNewBrowserWindow(options).then(win => { + windowsToClose.push(win); + windowsToReset.push(win); + return win; + }); + } + + await testOnWindow({}).then(win => promiseTestReady(true, win)); + await testOnWindow({ private: true }).then(win => + promiseTestReady(false, win) + ); + + // cleanup + windowsToReset.forEach(win => win.FullZoom.reset()); + await Promise.all( + windowsToClose.map(win => BrowserTestUtils.closeWindow(win)) + ); +}); diff --git a/browser/components/privatebrowsing/test/browser/empty_file.html b/browser/components/privatebrowsing/test/browser/empty_file.html new file mode 100644 index 0000000000..0dc101b533 --- /dev/null +++ b/browser/components/privatebrowsing/test/browser/empty_file.html @@ -0,0 +1 @@ + diff --git a/browser/components/privatebrowsing/test/browser/file_favicon.html b/browser/components/privatebrowsing/test/browser/file_favicon.html new file mode 100644 index 0000000000..f294b47758 --- /dev/null +++ b/browser/components/privatebrowsing/test/browser/file_favicon.html @@ -0,0 +1,11 @@ + + + + + Favicon Test for originAttributes + + + + Favicon!! + + diff --git a/browser/components/privatebrowsing/test/browser/file_favicon.png b/browser/components/privatebrowsing/test/browser/file_favicon.png new file mode 100644 index 0000000000..5535363c94 Binary files /dev/null and b/browser/components/privatebrowsing/test/browser/file_favicon.png differ diff --git a/browser/components/privatebrowsing/test/browser/file_favicon.png^headers^ b/browser/components/privatebrowsing/test/browser/file_favicon.png^headers^ new file mode 100644 index 0000000000..9e23c73b7f --- /dev/null +++ b/browser/components/privatebrowsing/test/browser/file_favicon.png^headers^ @@ -0,0 +1 @@ +Cache-Control: no-cache diff --git a/browser/components/privatebrowsing/test/browser/file_triggeringprincipal_oa.html b/browser/components/privatebrowsing/test/browser/file_triggeringprincipal_oa.html new file mode 100644 index 0000000000..cd05e833f3 --- /dev/null +++ b/browser/components/privatebrowsing/test/browser/file_triggeringprincipal_oa.html @@ -0,0 +1,10 @@ + + + + + Test for Bug 1348801 + + + checkPrincipalOA + + diff --git a/browser/components/privatebrowsing/test/browser/head.js b/browser/components/privatebrowsing/test/browser/head.js new file mode 100644 index 0000000000..f880c10a91 --- /dev/null +++ b/browser/components/privatebrowsing/test/browser/head.js @@ -0,0 +1,163 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +ChromeUtils.defineESModuleGetters(this, { + ASRouter: "resource:///modules/asrouter/ASRouter.sys.mjs", + ExperimentAPI: "resource://nimbus/ExperimentAPI.sys.mjs", + ExperimentFakes: "resource://testing-common/NimbusTestUtils.sys.mjs", + FileUtils: "resource://gre/modules/FileUtils.sys.mjs", + PanelTestProvider: "resource:///modules/asrouter/PanelTestProvider.sys.mjs", + PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs", + PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", + TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs", + sinon: "resource://testing-common/Sinon.sys.mjs", +}); + +function whenNewWindowLoaded(aOptions, aCallback) { + let win = OpenBrowserWindow(aOptions); + let focused = SimpleTest.promiseFocus(win); + let startupFinished = TestUtils.topicObserved( + "browser-delayed-startup-finished", + subject => subject == win + ).then(() => win); + Promise.all([focused, startupFinished]).then(results => + executeSoon(() => aCallback(results[1])) + ); + + return win; +} + +function openWindow(aParent, aOptions) { + return BrowserWindowTracker.promiseOpenWindow({ + openerWindow: aParent, + ...aOptions, + }); +} + +/** + * Opens a new private window and loads "about:privatebrowsing" there. + */ +async function openAboutPrivateBrowsing() { + let win = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + waitForTabURL: "about:privatebrowsing", + }); + let tab = win.gBrowser.selectedBrowser; + return { win, tab }; +} + +/** + * Wrapper for openAboutPrivateBrowsing that returns after render is complete + */ +async function openTabAndWaitForRender() { + let { win, tab } = await openAboutPrivateBrowsing(); + await SpecialPowers.spawn(tab, [], async function () { + // Wait for render to complete + await ContentTaskUtils.waitForCondition(() => + content.document.documentElement.hasAttribute( + "PrivateBrowsingRenderComplete" + ) + ); + }); + return { win, tab }; +} + +function newDirectory() { + let dir = FileUtils.getDir("TmpD", ["testdir"]); + dir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); + return dir; +} + +function newFileInDirectory(aDir) { + let file = aDir.clone(); + file.append("testfile"); + file.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_FILE); + return file; +} + +function clearHistory() { + // simulate clearing the private data + Services.obs.notifyObservers(null, "browser:purge-session-history"); +} + +function _initTest() { + // Don't use about:home as the homepage for new windows + Services.prefs.setIntPref("browser.startup.page", 0); + registerCleanupFunction(() => + Services.prefs.clearUserPref("browser.startup.page") + ); +} + +function waitForTelemetryEvent(category, value) { + info("waiting for telemetry event"); + return TestUtils.waitForCondition(() => { + let events = Services.telemetry.snapshotEvents( + Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS, + false + ).content; + + if (!events) { + return null; + } + events = events.filter(e => e[1] == category); + info(JSON.stringify(events)); + + // Check for experimentId passed as value + // if exists return events only for specific experimentId + if (value) { + events = events.filter(e => e[4].includes(value)); + } + if (events.length) { + return events[0]; + } + return null; + }, "wait and retrieve telemetry event"); +} + +async function setupMSExperimentWithMessage(message) { + Services.telemetry.clearEvents(); + Services.telemetry.snapshotEvents( + Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS, + true + ); + let doExperimentCleanup = await ExperimentFakes.enrollWithFeatureConfig({ + featureId: "pbNewtab", + value: message, + }); + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "browser.newtabpage.activity-stream.asrouter.providers.messaging-experiments", + '{"id":"messaging-experiments","enabled":true,"type":"remote-experiments","updateCycleInMs":0}', + ], + ], + }); + // Reload the provider + await ASRouter._updateMessageProviders(); + // Wait to load the messages from the messaging-experiments provider + await ASRouter.loadMessagesFromAllProviders(); + + // XXX this only runs at the end of the file, so some of this stuff (eg unblockAll) should be run + // at the bottom of various test functions too. Quite possibly other stuff beside unblockAll too. + registerCleanupFunction(async () => { + // Clear telemetry side effects + Services.telemetry.clearEvents(); + // Make sure the side-effects from dismisses are cleared. + ASRouter.unblockAll(); + // put the disabled providers back + SpecialPowers.popPrefEnv(); + // Reload the provider again at cleanup to remove the experiment message + await ASRouter._updateMessageProviders(); + // Wait to load the messages from the messaging-experiments provider + await ASRouter.loadMessagesFromAllProviders(); + }); + + Assert.ok( + ASRouter.state.messages.find(m => m.id.includes(message.id)), + "Experiment message found in ASRouter state" + ); + + return doExperimentCleanup; +} + +_initTest(); diff --git a/browser/components/privatebrowsing/test/browser/title.sjs b/browser/components/privatebrowsing/test/browser/title.sjs new file mode 100644 index 0000000000..817f124888 --- /dev/null +++ b/browser/components/privatebrowsing/test/browser/title.sjs @@ -0,0 +1,23 @@ +/* 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/. */ + +// This provides the tests with a page with different titles based on whether +// a cookie is present or not. + +function handleRequest(request, response) { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html", false); + + var cookie = "name=value"; + var title = "No Cookie"; + if (request.hasHeader("Cookie") && request.getHeader("Cookie") == cookie) { + title = "Cookie"; + } else { + response.setHeader("Set-Cookie", cookie, false); + } + + response.write(""); + response.write(title); + response.write("test page"); +} -- cgit v1.2.3