diff options
Diffstat (limited to 'browser/components/protections/test')
8 files changed, 2917 insertions, 0 deletions
diff --git a/browser/components/protections/test/browser/browser.ini b/browser/components/protections/test/browser/browser.ini new file mode 100644 index 0000000000..6a1dba1f67 --- /dev/null +++ b/browser/components/protections/test/browser/browser.ini @@ -0,0 +1,13 @@ +[DEFAULT] +prefs = + toolkit.telemetry.ipcBatchTimeout=0 +support-files = + head.js + !/browser/base/content/test/protectionsUI/trackingPage.html + +[browser_protections_lockwise.js] +[browser_protections_monitor.js] +[browser_protections_proxy.js] +[browser_protections_report_ui.js] +[browser_protections_telemetry.js] +[browser_protections_vpn.js] diff --git a/browser/components/protections/test/browser/browser_protections_lockwise.js b/browser/components/protections/test/browser/browser_protections_lockwise.js new file mode 100644 index 0000000000..93d10eb4b5 --- /dev/null +++ b/browser/components/protections/test/browser/browser_protections_lockwise.js @@ -0,0 +1,290 @@ +/* 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"; + +requestLongerTimeout(2); + +const { AboutProtectionsParent } = ChromeUtils.importESModule( + "resource:///actors/AboutProtectionsParent.sys.mjs" +); +const ABOUT_LOGINS_URL = "about:logins"; + +add_task(async function testNoLoginsLockwiseCardUI() { + const tab = await BrowserTestUtils.openNewForegroundTab({ + url: "about:protections", + gBrowser, + }); + const aboutLoginsPromise = BrowserTestUtils.waitForNewTab( + gBrowser, + ABOUT_LOGINS_URL + ); + + info( + "Check that the correct lockwise card content is displayed for non-logged in users." + ); + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + await ContentTaskUtils.waitForCondition(() => { + const lockwiseCard = content.document.querySelector(".lockwise-card"); + return ContentTaskUtils.is_visible(lockwiseCard); + }, "Lockwise card for user with no logins is visible."); + + const lockwiseHowItWorks = content.document.querySelector( + "#lockwise-how-it-works" + ); + ok( + ContentTaskUtils.is_hidden(lockwiseHowItWorks), + "How it works link is hidden" + ); + + const lockwiseHeaderContent = content.document.querySelector( + "#lockwise-header-content span" + ); + await content.document.l10n.translateElements([lockwiseHeaderContent]); + is( + lockwiseHeaderContent.dataset.l10nId, + "passwords-header-content", + "lockwiseHeaderContent contents should match l10n-id attribute set on the element" + ); + + const lockwiseScannedWrapper = content.document.querySelector( + ".lockwise-scanned-wrapper" + ); + ok( + ContentTaskUtils.is_hidden(lockwiseScannedWrapper), + "Lockwise scanned wrapper is hidden" + ); + + const managePasswordsButton = content.document.querySelector( + "#manage-passwords-button" + ); + ok( + ContentTaskUtils.is_hidden(managePasswordsButton), + "Manage passwords button is hidden" + ); + + const savePasswordsButton = content.document.querySelector( + "#save-passwords-button" + ); + ok( + ContentTaskUtils.is_visible(savePasswordsButton), + "Save passwords button is visible in the header" + ); + info( + "Click on the save passwords button and check that it opens about:logins in a new tab" + ); + savePasswordsButton.click(); + }); + const loginsTab = await aboutLoginsPromise; + info("about:logins was successfully opened in a new tab"); + gBrowser.removeTab(loginsTab); + gBrowser.removeTab(tab); +}); + +add_task(async function testLockwiseCardUIWithLogins() { + const tab = await BrowserTestUtils.openNewForegroundTab({ + url: "about:protections", + gBrowser, + }); + const aboutLoginsPromise = BrowserTestUtils.waitForNewTab( + gBrowser, + ABOUT_LOGINS_URL + ); + + info( + "Add a login and check that lockwise card content for a logged in user is displayed correctly" + ); + await Services.logins.addLoginAsync(TEST_LOGIN1); + await BrowserTestUtils.reloadTab(tab); + + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + await ContentTaskUtils.waitForCondition(() => { + const hasLogins = content.document.querySelector(".lockwise-card"); + return ContentTaskUtils.is_visible(hasLogins); + }, "Lockwise card for user with logins is visible"); + + const lockwiseTitle = content.document.querySelector("#lockwise-title"); + await content.document.l10n.translateElements([lockwiseTitle]); + await ContentTaskUtils.waitForCondition( + () => lockwiseTitle.textContent == "Manage your passwords", + "Waiting for Fluent to provide the title translation" + ); + is( + lockwiseTitle.textContent, + "Manage your passwords", + "Correct passwords title is shown" + ); + + const lockwiseHowItWorks = content.document.querySelector( + "#lockwise-how-it-works" + ); + ok( + ContentTaskUtils.is_visible(lockwiseHowItWorks), + "How it works link is visible" + ); + + const lockwiseHeaderContent = content.document.querySelector( + "#lockwise-header-content span" + ); + await content.document.l10n.translateElements([lockwiseHeaderContent]); + is( + lockwiseHeaderContent.dataset.l10nId, + "lockwise-header-content-logged-in", + "lockwiseHeaderContent contents should match l10n-id attribute set on the element" + ); + + const lockwiseScannedWrapper = content.document.querySelector( + ".lockwise-scanned-wrapper" + ); + ok( + ContentTaskUtils.is_visible(lockwiseScannedWrapper), + "Lockwise scanned wrapper is visible" + ); + + const lockwiseScannedText = content.document.querySelector( + "#lockwise-scanned-text" + ); + await content.document.l10n.translateElements([lockwiseScannedText]); + is( + lockwiseScannedText.textContent, + "1 password stored securely.", + "Correct lockwise scanned text is shown" + ); + + const savePasswordsButton = content.document.querySelector( + "#save-passwords-button" + ); + ok( + ContentTaskUtils.is_hidden(savePasswordsButton), + "Save passwords button is hidden" + ); + + const managePasswordsButton = content.document.querySelector( + "#manage-passwords-button" + ); + ok( + ContentTaskUtils.is_visible(managePasswordsButton), + "Manage passwords button is visible" + ); + info( + "Click on the manage passwords button and check that it opens about:logins in a new tab" + ); + managePasswordsButton.click(); + }); + const loginsTab = await aboutLoginsPromise; + info("about:logins was successfully opened in a new tab"); + gBrowser.removeTab(loginsTab); + + info( + "Add another login and check that the scanned text about stored logins is updated after reload." + ); + await Services.logins.addLoginAsync(TEST_LOGIN2); + await BrowserTestUtils.reloadTab(tab); + + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + const lockwiseScannedText = content.document.querySelector( + "#lockwise-scanned-text" + ).textContent; + ContentTaskUtils.waitForCondition( + () => + lockwiseScannedText.textContent == + "Your passwords are being stored securely.", + "Correct lockwise scanned text is shown" + ); + }); + + Services.logins.removeLogin(TEST_LOGIN1); + Services.logins.removeLogin(TEST_LOGIN2); + + gBrowser.removeTab(tab); +}); + +add_task(async function testLockwiseCardUIWithBreachedLogins() { + info( + "Add a breached login and test that the lockwise scanned text is displayed correctly" + ); + const tab = await BrowserTestUtils.openNewForegroundTab({ + url: "about:protections", + gBrowser, + }); + await Services.logins.addLoginAsync(TEST_LOGIN1); + + info("Mock monitor data with a breached login to test the Lockwise UI"); + AboutProtectionsParent.setTestOverride( + mockGetLoginDataWithSyncedDevices(false, 1) + ); + await BrowserTestUtils.reloadTab(tab); + + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + const lockwiseScannedText = content.document.querySelector( + "#lockwise-scanned-text" + ); + ok( + ContentTaskUtils.is_visible(lockwiseScannedText), + "Lockwise scanned text is visible" + ); + await ContentTaskUtils.waitForCondition( + () => + lockwiseScannedText.textContent == + "1 password may have been exposed in a data breach." + ); + info("Correct lockwise scanned text is shown"); + }); + + info( + "Mock monitor data with more than one breached logins to test the Lockwise UI" + ); + AboutProtectionsParent.setTestOverride( + mockGetLoginDataWithSyncedDevices(false, 2) + ); + await BrowserTestUtils.reloadTab(tab); + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + const lockwiseScannedText = content.document.querySelector( + "#lockwise-scanned-text" + ); + ok( + ContentTaskUtils.is_visible(lockwiseScannedText), + "Lockwise scanned text is visible" + ); + await ContentTaskUtils.waitForCondition( + () => + lockwiseScannedText.textContent == + "2 passwords may have been exposed in a data breach." + ); + info("Correct lockwise scanned text is shown"); + }); + + AboutProtectionsParent.setTestOverride(null); + Services.logins.removeLogin(TEST_LOGIN1); + gBrowser.removeTab(tab); +}); + +add_task(async function testLockwiseCardPref() { + const tab = await BrowserTestUtils.openNewForegroundTab({ + url: "about:protections", + gBrowser, + }); + + info("Disable showing the Lockwise card."); + Services.prefs.setBoolPref( + "browser.contentblocking.report.lockwise.enabled", + false + ); + await BrowserTestUtils.reloadTab(tab); + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + const lockwiseCard = content.document.querySelector(".lockwise-card"); + await ContentTaskUtils.waitForCondition(() => { + return !lockwiseCard["data-enabled"]; + }, "Lockwise card is not enabled."); + + ok(ContentTaskUtils.is_hidden(lockwiseCard), "Lockwise card is hidden."); + }); + + // Set the pref back to displaying the card. + Services.prefs.setBoolPref( + "browser.contentblocking.report.lockwise.enabled", + true + ); + gBrowser.removeTab(tab); +}); diff --git a/browser/components/protections/test/browser/browser_protections_monitor.js b/browser/components/protections/test/browser/browser_protections_monitor.js new file mode 100644 index 0000000000..572f6c8b07 --- /dev/null +++ b/browser/components/protections/test/browser/browser_protections_monitor.js @@ -0,0 +1,161 @@ +/* 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 { AboutProtectionsParent } = ChromeUtils.importESModule( + "resource:///actors/AboutProtectionsParent.sys.mjs" +); + +const monitorErrorData = { + error: true, +}; + +const mockMonitorData = { + numBreaches: 11, + numBreachesResolved: 0, +}; + +add_task(async function () { + const tab = await BrowserTestUtils.openNewForegroundTab({ + url: "about:protections", + gBrowser, + }); + + await BrowserTestUtils.reloadTab(tab); + + const monitorCardEnabled = Services.prefs.getBoolPref( + "browser.contentblocking.report.monitor.enabled" + ); + + // Only run monitor card tests if it's enabled. + if (monitorCardEnabled) { + info( + "Check that the correct content is displayed for users with no logins." + ); + await checkNoLoginsContentIsDisplayed(tab, "monitor-sign-up"); + + info( + "Check that the correct content is displayed for users with monitor data." + ); + await Services.logins.addLoginAsync(TEST_LOGIN1); + AboutProtectionsParent.setTestOverride(mockGetMonitorData(mockMonitorData)); + await BrowserTestUtils.reloadTab(tab); + + Assert.ok( + true, + "Error was not thrown for trying to reach the Monitor endpoint, the cache has worked." + ); + + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + await ContentTaskUtils.waitForCondition(() => { + const hasLogins = content.document.querySelector( + ".monitor-card.has-logins" + ); + return hasLogins && ContentTaskUtils.is_visible(hasLogins); + }, "Monitor card for user with stored logins is shown."); + + const hasLoginsHeaderContent = content.document.querySelector( + "#monitor-header-content span" + ); + const cardBody = content.document.querySelector( + ".monitor-card .card-body" + ); + + ok( + ContentTaskUtils.is_visible(cardBody), + "Card body is shown for users monitor data." + ); + await ContentTaskUtils.waitForCondition(() => { + return ( + hasLoginsHeaderContent.textContent == + "Firefox Monitor warns you if your info has appeared in a known data breach." + ); + }, "Header content for user with monitor data is correct."); + + info("Make sure correct numbers for monitor stats are displayed."); + const emails = content.document.querySelector( + ".monitor-stat span[data-type='stored-emails']" + ); + const passwords = content.document.querySelector( + ".monitor-stat span[data-type='exposed-passwords']" + ); + const breaches = content.document.querySelector( + ".monitor-stat span[data-type='known-breaches']" + ); + + is(emails.textContent, 1, "1 monitored email is displayed"); + is(passwords.textContent, 8, "8 exposed passwords are displayed"); + is(breaches.textContent, 11, "11 known data breaches are displayed."); + }); + + info( + "Check that correct content is displayed when monitor data contains an error message." + ); + AboutProtectionsParent.setTestOverride( + mockGetMonitorData(monitorErrorData) + ); + await BrowserTestUtils.reloadTab(tab); + await checkNoLoginsContentIsDisplayed(tab); + + info("Disable showing the Monitor card."); + Services.prefs.setBoolPref( + "browser.contentblocking.report.monitor.enabled", + false + ); + await BrowserTestUtils.reloadTab(tab); + } + + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + await ContentTaskUtils.waitForCondition(() => { + const monitorCard = content.document.querySelector(".monitor-card"); + return !monitorCard["data-enabled"]; + }, "Monitor card is not enabled."); + + const monitorCard = content.document.querySelector(".monitor-card"); + ok(ContentTaskUtils.is_hidden(monitorCard), "Monitor card is hidden."); + }); + + if (monitorCardEnabled) { + // set the pref back to displaying the card. + Services.prefs.setBoolPref( + "browser.contentblocking.report.monitor.enabled", + true + ); + + // remove logins + Services.logins.removeLogin(TEST_LOGIN1); + + // restore original test functions + AboutProtectionsParent.setTestOverride(null); + } + + await BrowserTestUtils.removeTab(tab); +}); + +async function checkNoLoginsContentIsDisplayed(tab, expectedLinkContent) { + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + await ContentTaskUtils.waitForCondition(() => { + const noLogins = content.document.querySelector( + ".monitor-card.no-logins" + ); + return noLogins && ContentTaskUtils.is_visible(noLogins); + }, "Monitor card for user with no logins is shown."); + + const noLoginsHeaderContent = content.document.querySelector( + "#monitor-header-content span" + ); + const cardBody = content.document.querySelector(".monitor-card .card-body"); + + ok( + ContentTaskUtils.is_hidden(cardBody), + "Card body is hidden for users with no logins." + ); + is( + noLoginsHeaderContent.getAttribute("data-l10n-id"), + "monitor-header-content-no-account", + "Header content for user with no logins is correct" + ); + }); +} diff --git a/browser/components/protections/test/browser/browser_protections_proxy.js b/browser/components/protections/test/browser/browser_protections_proxy.js new file mode 100644 index 0000000000..840abcab3f --- /dev/null +++ b/browser/components/protections/test/browser/browser_protections_proxy.js @@ -0,0 +1,107 @@ +/* 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"; + +ChromeUtils.defineESModuleGetters(this, { + Region: "resource://gre/modules/Region.sys.mjs", +}); + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.contentblocking.report.monitor.enabled", false], + ["browser.contentblocking.report.lockwise.enabled", false], + ["browser.vpn_promo.enabled", false], + ], + }); +}); + +add_task(async function () { + let tab = await BrowserTestUtils.openNewForegroundTab({ + url: "about:protections", + gBrowser, + }); + + info("Secure Proxy card should be hidden by default"); + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + await ContentTaskUtils.waitForCondition(() => { + const proxyCard = content.document.querySelector(".proxy-card"); + return !proxyCard["data-enabled"]; + }, "Proxy card is not enabled."); + + const proxyCard = content.document.querySelector(".proxy-card"); + ok(ContentTaskUtils.is_hidden(proxyCard), "Proxy card is hidden."); + }); + + info("Enable showing the Secure Proxy card"); + Services.prefs.setBoolPref( + "browser.contentblocking.report.proxy.enabled", + true + ); + + info( + "Check that secure proxy card is hidden if user's language is not en-US" + ); + Services.prefs.setCharPref("intl.accept_languages", "en-CA"); + await BrowserTestUtils.reloadTab(tab); + await checkProxyCardVisibility(tab, true); + + info( + "Check that secure proxy card is shown if user's location is in the US." + ); + // Set language back to en-US + Services.prefs.setCharPref("intl.accept_languages", "en-US"); + Region._setHomeRegion("US", false); + await BrowserTestUtils.reloadTab(tab); + await checkProxyCardVisibility(tab, false); + + info( + "Check that secure proxy card is hidden if user's location is not in the US." + ); + Region._setHomeRegion("CA", false); + await BrowserTestUtils.reloadTab(tab); + await checkProxyCardVisibility(tab, true); + + info( + "Check that secure proxy card is hidden if the extension is already installed." + ); + // Make sure we set the region back to "US" + Region._setHomeRegion("US", false); + const id = "secure-proxy@mozilla.com"; + const extension = ExtensionTestUtils.loadExtension({ + manifest: { + browser_specific_settings: { gecko: { id } }, + name: "Firefox Proxy", + }, + useAddonManager: "temporary", + }); + await extension.startup(); + await BrowserTestUtils.reloadTab(tab); + await checkProxyCardVisibility(tab, true); + await extension.unload(); + + Services.prefs.setBoolPref( + "browser.contentblocking.report.proxy.enabled", + false + ); + + await BrowserTestUtils.removeTab(tab); +}); + +async function checkProxyCardVisibility(tab, shouldBeHidden) { + await SpecialPowers.spawn( + tab.linkedBrowser, + [{ _shouldBeHidden: shouldBeHidden }], + async function ({ _shouldBeHidden }) { + await ContentTaskUtils.waitForCondition(() => { + const proxyCard = content.document.querySelector(".proxy-card"); + return ContentTaskUtils.is_hidden(proxyCard) === _shouldBeHidden; + }); + + const visibilityState = _shouldBeHidden ? "hidden" : "shown"; + ok(true, `Proxy card is ${visibilityState}.`); + } + ); +} diff --git a/browser/components/protections/test/browser/browser_protections_report_ui.js b/browser/components/protections/test/browser/browser_protections_report_ui.js new file mode 100644 index 0000000000..6f8d48d6dd --- /dev/null +++ b/browser/components/protections/test/browser/browser_protections_report_ui.js @@ -0,0 +1,845 @@ +/* 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/. */ + +// Note: This test may cause intermittents if run at exactly midnight. + +const { Sqlite } = ChromeUtils.importESModule( + "resource://gre/modules/Sqlite.sys.mjs" +); +const { AboutProtectionsParent } = ChromeUtils.importESModule( + "resource:///actors/AboutProtectionsParent.sys.mjs" +); + +XPCOMUtils.defineLazyServiceGetter( + this, + "TrackingDBService", + "@mozilla.org/tracking-db-service;1", + "nsITrackingDBService" +); + +XPCOMUtils.defineLazyGetter(this, "DB_PATH", function () { + return PathUtils.join(PathUtils.profileDir, "protections.sqlite"); +}); + +const SQL = { + insertCustomTimeEvent: + "INSERT INTO events (type, count, timestamp)" + + "VALUES (:type, :count, date(:timestamp));", + + selectAll: "SELECT * FROM events", +}; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.contentblocking.database.enabled", true], + ["browser.vpn_promo.enabled", false], + ], + }); +}); + +add_task(async function test_graph_display() { + // This creates the schema. + await TrackingDBService.saveEvents(JSON.stringify({})); + let db = await Sqlite.openConnection({ path: DB_PATH }); + + let date = new Date().toISOString(); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.TRACKERS_ID, + count: 1, + timestamp: date, + }); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.CRYPTOMINERS_ID, + count: 2, + timestamp: date, + }); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.FINGERPRINTERS_ID, + count: 2, + timestamp: date, + }); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.TRACKING_COOKIES_ID, + count: 4, + timestamp: date, + }); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.SOCIAL_ID, + count: 1, + timestamp: date, + }); + + date = new Date(Date.now() - 1 * 24 * 60 * 60 * 1000).toISOString(); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.TRACKERS_ID, + count: 4, + timestamp: date, + }); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.CRYPTOMINERS_ID, + count: 3, + timestamp: date, + }); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.FINGERPRINTERS_ID, + count: 2, + timestamp: date, + }); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.SOCIAL_ID, + count: 1, + timestamp: date, + }); + + date = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.TRACKERS_ID, + count: 4, + timestamp: date, + }); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.CRYPTOMINERS_ID, + count: 3, + timestamp: date, + }); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.TRACKING_COOKIES_ID, + count: 1, + timestamp: date, + }); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.SOCIAL_ID, + count: 1, + timestamp: date, + }); + + date = new Date(Date.now() - 3 * 24 * 60 * 60 * 1000).toISOString(); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.TRACKERS_ID, + count: 3, + timestamp: date, + }); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.FINGERPRINTERS_ID, + count: 2, + timestamp: date, + }); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.TRACKING_COOKIES_ID, + count: 1, + timestamp: date, + }); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.SOCIAL_ID, + count: 1, + timestamp: date, + }); + + date = new Date(Date.now() - 4 * 24 * 60 * 60 * 1000).toISOString(); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.CRYPTOMINERS_ID, + count: 2, + timestamp: date, + }); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.FINGERPRINTERS_ID, + count: 2, + timestamp: date, + }); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.TRACKING_COOKIES_ID, + count: 1, + timestamp: date, + }); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.SOCIAL_ID, + count: 1, + timestamp: date, + }); + + date = new Date(Date.now() - 5 * 24 * 60 * 60 * 1000).toISOString(); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.TRACKERS_ID, + count: 3, + timestamp: date, + }); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.CRYPTOMINERS_ID, + count: 3, + timestamp: date, + }); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.FINGERPRINTERS_ID, + count: 2, + timestamp: date, + }); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.TRACKING_COOKIES_ID, + count: 8, + timestamp: date, + }); + + let tab = await BrowserTestUtils.openNewForegroundTab({ + url: "about:protections", + gBrowser, + }); + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + const DATA_TYPES = [ + "cryptominer", + "fingerprinter", + "tracker", + "cookie", + "social", + ]; + let allBars = null; + await ContentTaskUtils.waitForCondition(() => { + allBars = content.document.querySelectorAll(".graph-bar"); + return allBars.length; + }, "The graph has been built"); + + Assert.equal(allBars.length, 7, "7 bars have been found on the graph"); + + // For accessibility, test if the graph is a table + // and has a correct column count (number of data types + total + day) + Assert.equal( + content.document.getElementById("graph").getAttribute("role"), + "table", + "Graph is an accessible table" + ); + Assert.equal( + content.document.getElementById("graph").getAttribute("aria-colcount"), + DATA_TYPES.length + 2, + "Table has the right number of columns" + ); + Assert.equal( + content.document.getElementById("graph").getAttribute("aria-labelledby"), + "graphLegendDescription", + "Table has an accessible label" + ); + + // today has each type + // yesterday will have no tracking cookies + // 2 days ago will have no fingerprinters + // 3 days ago will have no cryptominers + // 4 days ago will have no trackers + // 5 days ago will have no social (when we add social) + // 6 days ago will be empty + Assert.equal( + allBars[6].querySelectorAll(".inner-bar").length, + DATA_TYPES.length, + "today has all of the data types shown" + ); + Assert.equal( + allBars[6].getAttribute("role"), + "row", + "Today has the correct role" + ); + Assert.equal( + allBars[6].getAttribute("aria-owns"), + "day0 count0 cryptominer0 fingerprinter0 tracker0 cookie0 social0", + "Row has the columns in the right order" + ); + Assert.equal( + allBars[6].querySelector(".tracker-bar").style.height, + "10%", + "trackers take 10%" + ); + Assert.equal( + allBars[6].querySelector(".tracker-bar").parentNode.getAttribute("role"), + "cell", + "Trackers have the correct role" + ); + Assert.equal( + allBars[6].querySelector(".tracker-bar").getAttribute("role"), + "img", + "Tracker bar has the correct image role" + ); + Assert.equal( + allBars[6].querySelector(".tracker-bar").getAttribute("aria-label"), + "1 tracking content (10%)", + "Trackers have the correct accessible text" + ); + Assert.equal( + allBars[6].querySelector(".cryptominer-bar").style.height, + "20%", + "cryptominers take 20%" + ); + Assert.equal( + allBars[6] + .querySelector(".cryptominer-bar") + .parentNode.getAttribute("role"), + "cell", + "Cryptominers have the correct role" + ); + Assert.equal( + allBars[6].querySelector(".cryptominer-bar").getAttribute("role"), + "img", + "Cryptominer bar has the correct image role" + ); + Assert.equal( + allBars[6].querySelector(".cryptominer-bar").getAttribute("aria-label"), + "2 cryptominers (20%)", + "Cryptominers have the correct accessible label" + ); + Assert.equal( + allBars[6].querySelector(".fingerprinter-bar").style.height, + "20%", + "fingerprinters take 20%" + ); + Assert.equal( + allBars[6] + .querySelector(".fingerprinter-bar") + .parentNode.getAttribute("role"), + "cell", + "Fingerprinters have the correct role" + ); + Assert.equal( + allBars[6].querySelector(".fingerprinter-bar").getAttribute("role"), + "img", + "Fingerprinter bar has the correct image role" + ); + Assert.equal( + allBars[6].querySelector(".fingerprinter-bar").getAttribute("aria-label"), + "2 fingerprinters (20%)", + "Fingerprinters have the correct accessible label" + ); + Assert.equal( + allBars[6].querySelector(".cookie-bar").style.height, + "40%", + "cross site tracking cookies take 40%" + ); + Assert.equal( + allBars[6].querySelector(".cookie-bar").parentNode.getAttribute("role"), + "cell", + "cross site tracking cookies have the correct role" + ); + Assert.equal( + allBars[6].querySelector(".cookie-bar").getAttribute("role"), + "img", + "Cross site tracking cookies bar has the correct image role" + ); + Assert.equal( + allBars[6].querySelector(".cookie-bar").getAttribute("aria-label"), + "4 cross-site tracking cookies (40%)", + "cross site tracking cookies have the correct accessible label" + ); + Assert.equal( + allBars[6].querySelector(".social-bar").style.height, + "10%", + "social trackers take 10%" + ); + Assert.equal( + allBars[6].querySelector(".social-bar").parentNode.getAttribute("role"), + "cell", + "social trackers have the correct role" + ); + Assert.equal( + allBars[6].querySelector(".social-bar").getAttribute("role"), + "img", + "social tracker bar has the correct image role" + ); + Assert.equal( + allBars[6].querySelector(".social-bar").getAttribute("aria-label"), + "1 social media tracker (10%)", + "social trackers have the correct accessible text" + ); + + Assert.equal( + allBars[5].querySelectorAll(".inner-bar").length, + DATA_TYPES.length - 1, + "1 day ago is missing one type" + ); + Assert.ok( + !allBars[5].querySelector(".cookie-bar"), + "there is no cross site tracking cookie section 1 day ago." + ); + Assert.equal( + allBars[5].getAttribute("aria-owns"), + "day1 count1 cryptominer1 fingerprinter1 tracker1 social1", + "Row has the columns in the right order" + ); + + Assert.equal( + allBars[4].querySelectorAll(".inner-bar").length, + DATA_TYPES.length - 1, + "2 days ago is missing one type" + ); + Assert.ok( + !allBars[4].querySelector(".fingerprinter-bar"), + "there is no fingerprinter section 1 day ago." + ); + Assert.equal( + allBars[4].getAttribute("aria-owns"), + "day2 count2 cryptominer2 tracker2 cookie2 social2", + "Row has the columns in the right order" + ); + + Assert.equal( + allBars[3].querySelectorAll(".inner-bar").length, + DATA_TYPES.length - 1, + "3 days ago is missing one type" + ); + Assert.ok( + !allBars[3].querySelector(".cryptominer-bar"), + "there is no cryptominer section 1 day ago." + ); + Assert.equal( + allBars[3].getAttribute("aria-owns"), + "day3 count3 fingerprinter3 tracker3 cookie3 social3", + "Row has the columns in the right order" + ); + + Assert.equal( + allBars[2].querySelectorAll(".inner-bar").length, + DATA_TYPES.length - 1, + "4 days ago is missing one type" + ); + Assert.ok( + !allBars[2].querySelector(".tracker-bar"), + "there is no tracker section 1 day ago." + ); + Assert.equal( + allBars[2].getAttribute("aria-owns"), + "day4 count4 cryptominer4 fingerprinter4 cookie4 social4", + "Row has the columns in the right order" + ); + + Assert.equal( + allBars[1].querySelectorAll(".inner-bar").length, + DATA_TYPES.length - 1, + "5 days ago is missing one type" + ); + Assert.ok( + !allBars[1].querySelector(".social-bar"), + "there is no social section 1 day ago." + ); + Assert.equal( + allBars[1].getAttribute("aria-owns"), + "day5 count5 cryptominer5 fingerprinter5 tracker5 cookie5", + "Row has the columns in the right order" + ); + + Assert.equal( + allBars[0].querySelectorAll(".inner-bar").length, + 0, + "6 days ago has no content" + ); + Assert.ok( + allBars[0].classList.contains("empty"), + "6 days ago is an empty bar" + ); + Assert.equal( + allBars[0].getAttribute("aria-owns"), + "day6 ", + "Row has the columns in the right order" + ); + + // Check that each tab has the correct aria-labelledby and aria-describedby + // values. This helps screen readers know what type of tracker the reported + // tab number is referencing. + const socialTab = content.document.getElementById("tab-social"); + Assert.equal( + socialTab.getAttribute("aria-labelledby"), + "socialLabel socialTitle", + "aria-labelledby attribute is socialLabel socialTitle" + ); + Assert.equal( + socialTab.getAttribute("aria-describedby"), + "socialContent", + "aria-describedby attribute is socialContent" + ); + + const cookieTab = content.document.getElementById("tab-cookie"); + Assert.equal( + cookieTab.getAttribute("aria-labelledby"), + "cookieLabel cookieTitle", + "aria-labelledby attribute is cookieLabel cookieTitle" + ); + Assert.equal( + cookieTab.getAttribute("aria-describedby"), + "cookieContent", + "aria-describedby attribute is cookieContent" + ); + + const trackerTab = content.document.getElementById("tab-tracker"); + Assert.equal( + trackerTab.getAttribute("aria-labelledby"), + "trackerLabel trackerTitle", + "aria-labelledby attribute is trackerLabel trackerTitle" + ); + Assert.equal( + trackerTab.getAttribute("aria-describedby"), + "trackerContent", + "aria-describedby attribute is trackerContent" + ); + + const fingerprinterTab = + content.document.getElementById("tab-fingerprinter"); + Assert.equal( + fingerprinterTab.getAttribute("aria-labelledby"), + "fingerprinterLabel fingerprinterTitle", + "aria-labelledby attribute is fingerprinterLabel fingerprinterTitle" + ); + Assert.equal( + fingerprinterTab.getAttribute("aria-describedby"), + "fingerprinterContent", + "aria-describedby attribute is fingerprinterContent" + ); + + const cryptominerTab = content.document.getElementById("tab-cryptominer"); + Assert.equal( + cryptominerTab.getAttribute("aria-labelledby"), + "cryptominerLabel cryptominerTitle", + "aria-labelledby attribute is cryptominerLabel cryptominerTitle" + ); + Assert.equal( + cryptominerTab.getAttribute("aria-describedby"), + "cryptominerContent", + "aria-describedby attribute is cryptominerContent" + ); + }); + + // Use the TrackingDBService API to delete the data. + await TrackingDBService.clearAll(); + // Make sure the data was deleted. + let rows = await db.execute(SQL.selectAll); + is(rows.length, 0, "length is 0"); + await db.close(); + BrowserTestUtils.removeTab(tab); +}); + +// Ensure that each type of tracker is hidden from the graph if there are no recorded +// trackers of that type and the user has chosen to not block that type. +add_task(async function test_etp_custom_settings() { + Services.prefs.setStringPref("browser.contentblocking.category", "strict"); + Services.prefs.setBoolPref( + "privacy.socialtracking.block_cookies.enabled", + true + ); + // hide cookies from the graph + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + let tab = await BrowserTestUtils.openNewForegroundTab({ + url: "about:protections", + gBrowser, + }); + + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + await ContentTaskUtils.waitForCondition(() => { + let legend = content.document.getElementById("legend"); + return ContentTaskUtils.is_visible(legend); + }, "The legend is visible"); + + let label = content.document.getElementById("cookieLabel"); + Assert.ok(ContentTaskUtils.is_hidden(label), "Cookie Label is hidden"); + + label = content.document.getElementById("trackerLabel"); + Assert.ok(ContentTaskUtils.is_visible(label), "Tracker Label is visible"); + label = content.document.getElementById("socialLabel"); + Assert.ok(ContentTaskUtils.is_visible(label), "Social Label is visible"); + label = content.document.getElementById("cryptominerLabel"); + Assert.ok( + ContentTaskUtils.is_visible(label), + "Cryptominer Label is visible" + ); + label = content.document.getElementById("fingerprinterLabel"); + Assert.ok( + ContentTaskUtils.is_visible(label), + "Fingerprinter Label is visible" + ); + }); + BrowserTestUtils.removeTab(tab); + + // hide ad trackers from the graph + Services.prefs.setBoolPref("privacy.trackingprotection.enabled", false); + tab = await BrowserTestUtils.openNewForegroundTab({ + url: "about:protections", + gBrowser, + }); + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + await ContentTaskUtils.waitForCondition(() => { + let legend = content.document.getElementById("legend"); + return ContentTaskUtils.is_visible(legend); + }, "The legend is visible"); + + let label = content.document.querySelector("#trackerLabel"); + Assert.ok(ContentTaskUtils.is_hidden(label), "Tracker Label is hidden"); + + label = content.document.querySelector("#socialLabel"); + Assert.ok(ContentTaskUtils.is_hidden(label), "Social Label is hidden"); + }); + BrowserTestUtils.removeTab(tab); + + // hide social from the graph + Services.prefs.setBoolPref( + "privacy.trackingprotection.socialtracking.enabled", + false + ); + Services.prefs.setBoolPref( + "privacy.socialtracking.block_cookies.enabled", + false + ); + tab = await BrowserTestUtils.openNewForegroundTab({ + url: "about:protections", + gBrowser, + }); + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + await ContentTaskUtils.waitForCondition(() => { + let legend = content.document.getElementById("legend"); + return ContentTaskUtils.is_visible(legend); + }, "The legend is visible"); + + let label = content.document.querySelector("#socialLabel"); + Assert.ok(ContentTaskUtils.is_hidden(label), "Social Label is hidden"); + }); + BrowserTestUtils.removeTab(tab); + + // hide fingerprinting from the graph + Services.prefs.setBoolPref( + "privacy.trackingprotection.fingerprinting.enabled", + false + ); + tab = await BrowserTestUtils.openNewForegroundTab({ + url: "about:protections", + gBrowser, + }); + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + await ContentTaskUtils.waitForCondition(() => { + let legend = content.document.getElementById("legend"); + return ContentTaskUtils.is_visible(legend); + }, "The legend is visible"); + + let label = content.document.querySelector("#fingerprinterLabel"); + Assert.ok( + ContentTaskUtils.is_hidden(label), + "Fingerprinter Label is hidden" + ); + }); + BrowserTestUtils.removeTab(tab); + + // hide cryptomining from the graph + Services.prefs.setBoolPref( + "privacy.trackingprotection.cryptomining.enabled", + false + ); + // Turn fingerprinting on so that all protectionsare not turned off, otherwise we will get a special card. + Services.prefs.setBoolPref( + "privacy.trackingprotection.fingerprinting.enabled", + true + ); + tab = await BrowserTestUtils.openNewForegroundTab({ + url: "about:protections", + gBrowser, + }); + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + await ContentTaskUtils.waitForCondition(() => { + let legend = content.document.getElementById("legend"); + return ContentTaskUtils.is_visible(legend); + }, "The legend is visible"); + + let label = content.document.querySelector("#cryptominerLabel"); + Assert.ok(ContentTaskUtils.is_hidden(label), "Cryptominer Label is hidden"); + }); + Services.prefs.clearUserPref("browser.contentblocking.category"); + Services.prefs.clearUserPref( + "privacy.trackingprotection.fingerprinting.enabled" + ); + Services.prefs.clearUserPref( + "privacy.trackingprotection.cryptomining.enabled" + ); + Services.prefs.clearUserPref("privacy.trackingprotection.enabled"); + Services.prefs.clearUserPref("network.cookie.cookieBehavior"); + Services.prefs.clearUserPref("privacy.socialtracking.block_cookies.enabled"); + + BrowserTestUtils.removeTab(tab); +}); + +// Ensure that the Custom manage Protections card is shown if the user has all protections turned off. +add_task(async function test_etp_custom_protections_off() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.contentblocking.category", "custom"], + ["network.cookie.cookieBehavior", 0], // not blocking + ["privacy.trackingprotection.cryptomining.enabled", false], // not blocking + ["privacy.trackingprotection.fingerprinting.enabled", false], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.socialtracking.enabled", false], + ["privacy.socialtracking.block_cookies.enabled", false], + ], + }); + + let tab = await BrowserTestUtils.openNewForegroundTab({ + url: "about:protections", + gBrowser, + }); + + let aboutPreferencesPromise = BrowserTestUtils.waitForNewTab( + gBrowser, + "about:preferences#privacy" + ); + + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + await ContentTaskUtils.waitForCondition(() => { + let etpCard = content.document.querySelector(".etp-card"); + return etpCard.classList.contains("custom-not-blocking"); + }, "The custom protections warning card is showing"); + + let manageProtectionsButton = + content.document.getElementById("manage-protections"); + Assert.ok( + ContentTaskUtils.is_visible(manageProtectionsButton), + "Button to manage protections is displayed" + ); + }); + + // Custom protection card should show, even if there would otherwise be data on the graph. + let db = await Sqlite.openConnection({ path: DB_PATH }); + let date = new Date().toISOString(); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.TRACKERS_ID, + count: 1, + timestamp: date, + }); + await BrowserTestUtils.reloadTab(tab); + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + await ContentTaskUtils.waitForCondition(() => { + let etpCard = content.document.querySelector(".etp-card"); + return etpCard.classList.contains("custom-not-blocking"); + }, "The custom protections warning card is showing"); + + let manageProtectionsButton = + content.document.getElementById("manage-protections"); + Assert.ok( + ContentTaskUtils.is_visible(manageProtectionsButton), + "Button to manage protections is displayed" + ); + + manageProtectionsButton.click(); + }); + let aboutPreferencesTab = await aboutPreferencesPromise; + info("about:preferences#privacy was successfully opened in a new tab"); + gBrowser.removeTab(aboutPreferencesTab); + + Services.prefs.setStringPref("browser.contentblocking.category", "standard"); + // Use the TrackingDBService API to delete the data. + await TrackingDBService.clearAll(); + // Make sure the data was deleted. + let rows = await db.execute(SQL.selectAll); + is(rows.length, 0, "length is 0"); + await db.close(); + BrowserTestUtils.removeTab(tab); +}); + +// Ensure that the ETP mobile promotion card is shown when the pref is on and +// there are no mobile devices connected. +add_task(async function test_etp_mobile_promotion_pref_on() { + AboutProtectionsParent.setTestOverride(mockGetLoginDataWithSyncedDevices()); + await SpecialPowers.pushPrefEnv({ + set: [["browser.contentblocking.report.show_mobile_app", true]], + }); + + let tab = await BrowserTestUtils.openNewForegroundTab({ + url: "about:protections", + gBrowser, + }); + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + let mobilePromotion = content.document.getElementById("mobile-hanger"); + Assert.ok( + ContentTaskUtils.is_visible(mobilePromotion), + "Mobile promotions card is displayed when pref is on and there are no synced mobile devices" + ); + + // Card should hide after the X is clicked. + mobilePromotion.querySelector(".exit-icon").click(); + Assert.ok( + ContentTaskUtils.is_hidden(mobilePromotion), + "Mobile promotions card is no longer displayed after clicking the X button" + ); + }); + BrowserTestUtils.removeTab(tab); + + // Add a mock mobile device. The promotion should now be hidden. + AboutProtectionsParent.setTestOverride( + mockGetLoginDataWithSyncedDevices(true) + ); + tab = await BrowserTestUtils.openNewForegroundTab({ + url: "about:protections", + gBrowser, + }); + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + let mobilePromotion = content.document.getElementById("mobile-hanger"); + Assert.ok( + ContentTaskUtils.is_hidden(mobilePromotion), + "Mobile promotions card is hidden when pref is on if there are synced mobile devices" + ); + }); + + BrowserTestUtils.removeTab(tab); + AboutProtectionsParent.setTestOverride(null); +}); + +// Test that ETP mobile promotion is not shown when the pref is off, +// even if no mobile devices are synced. +add_task(async function test_etp_mobile_promotion_pref_on() { + AboutProtectionsParent.setTestOverride(mockGetLoginDataWithSyncedDevices()); + await SpecialPowers.pushPrefEnv({ + set: [["browser.contentblocking.report.show_mobile_app", false]], + }); + + let tab = await BrowserTestUtils.openNewForegroundTab({ + url: "about:protections", + gBrowser, + }); + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + let mobilePromotion = content.document.getElementById("mobile-hanger"); + Assert.ok( + ContentTaskUtils.is_hidden(mobilePromotion), + "Mobile promotions card is not displayed when pref is off and there are no synced mobile devices" + ); + }); + + BrowserTestUtils.removeTab(tab); + + AboutProtectionsParent.setTestOverride( + mockGetLoginDataWithSyncedDevices(true) + ); + tab = await BrowserTestUtils.openNewForegroundTab({ + url: "about:protections", + gBrowser, + }); + + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + let mobilePromotion = content.document.getElementById("mobile-hanger"); + Assert.ok( + ContentTaskUtils.is_hidden(mobilePromotion), + "Mobile promotions card is not displayed when pref is off even if there are synced mobile devices" + ); + }); + BrowserTestUtils.removeTab(tab); + AboutProtectionsParent.setTestOverride(null); +}); + +// Test that clicking on the link to settings in the header properly opens the settings page. +add_task(async function test_settings_links() { + let tab = await BrowserTestUtils.openNewForegroundTab({ + url: "about:protections", + gBrowser, + }); + let aboutPreferencesPromise = BrowserTestUtils.waitForNewTab( + gBrowser, + "about:preferences#privacy" + ); + + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + const protectionSettings = await ContentTaskUtils.waitForCondition(() => { + return content.document.getElementById("protection-settings"); + }, "protection-settings link exists"); + + protectionSettings.click(); + }); + let aboutPreferencesTab = await aboutPreferencesPromise; + info("about:preferences#privacy was successfully opened in a new tab"); + gBrowser.removeTab(aboutPreferencesTab); + gBrowser.removeTab(tab); +}); diff --git a/browser/components/protections/test/browser/browser_protections_telemetry.js b/browser/components/protections/test/browser/browser_protections_telemetry.js new file mode 100644 index 0000000000..dfcc274b6c --- /dev/null +++ b/browser/components/protections/test/browser/browser_protections_telemetry.js @@ -0,0 +1,1123 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +XPCOMUtils.defineLazyServiceGetter( + this, + "TrackingDBService", + "@mozilla.org/tracking-db-service;1", + "nsITrackingDBService" +); + +const { AboutProtectionsParent } = ChromeUtils.importESModule( + "resource:///actors/AboutProtectionsParent.sys.mjs" +); + +const LOG = { + "https://1.example.com": [ + [Ci.nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT, true, 1], + ], + "https://2.example.com": [ + [Ci.nsIWebProgressListener.STATE_BLOCKED_FINGERPRINTING_CONTENT, true, 1], + ], + "https://3.example.com": [ + [Ci.nsIWebProgressListener.STATE_BLOCKED_CRYPTOMINING_CONTENT, true, 2], + ], + "https://4.example.com": [ + [Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, true, 3], + ], + "https://5.example.com": [ + [Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, true, 1], + ], + // Cookie blocked for other reason, then identified as a tracker + "https://6.example.com": [ + [ + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_ALL | + Ci.nsIWebProgressListener.STATE_LOADED_LEVEL_1_TRACKING_CONTENT, + true, + 4, + ], + ], +}; + +requestLongerTimeout(2); + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.vpn_promo.enabled", true], + ["browser.contentblocking.report.vpn_regions", "us,ca,nz,sg,my,gb"], + [ + "browser.vpn_promo.disallowed_regions", + "ae,by,cn,cu,iq,ir,kp,om,ru,sd,sy,tm,tr,ua", + ], + + // Change the endpoints to prevent non-local network connections when landing on the page. + ["browser.contentblocking.report.monitor.url", ""], + ["browser.contentblocking.report.monitor.sign_in_url", ""], + ["browser.contentblocking.report.social.url", ""], + ["browser.contentblocking.report.cookie.url", ""], + ["browser.contentblocking.report.tracker.url", ""], + ["browser.contentblocking.report.fingerprinter.url", ""], + ["browser.contentblocking.report.cryptominer.url", ""], + ["browser.contentblocking.report.mobile-ios.url", ""], + ["browser.contentblocking.report.mobile-android.url", ""], + ["browser.contentblocking.report.monitor.home_page_url", ""], + ["browser.contentblocking.report.monitor.preferences_url", ""], + ["browser.contentblocking.report.vpn.url", ""], + ["browser.contentblocking.report.vpn-promo.url", ""], + ["browser.contentblocking.report.vpn-android.url", ""], + ["browser.contentblocking.report.vpn-ios.url", ""], + ], + }); + + let oldCanRecord = Services.telemetry.canRecordExtended; + Services.telemetry.canRecordExtended = true; + registerCleanupFunction(() => { + Services.telemetry.canRecordExtended = oldCanRecord; + // AboutProtectionsParent.setTestOverride(null); + }); +}); + +add_task(async function checkTelemetryLoadEvents() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.contentblocking.database.enabled", false], + ["browser.contentblocking.report.monitor.enabled", false], + ["browser.contentblocking.report.lockwise.enabled", false], + ["browser.contentblocking.report.proxy.enabled", false], + ["browser.vpn_promo.enabled", false], + ], + }); + await addArbitraryTimeout(); + + // Clear everything. + Services.telemetry.clearEvents(); + await TestUtils.waitForCondition(() => { + let events = Services.telemetry.snapshotEvents( + Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS, + true + ).content; + return !events || !events.length; + }); + + Services.telemetry.setEventRecordingEnabled("security.ui.protections", true); + + let tab = await BrowserTestUtils.openNewForegroundTab({ + url: "about:protections", + gBrowser, + }); + + let loadEvents = await TestUtils.waitForCondition(() => { + let events = Services.telemetry.snapshotEvents( + Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS, + true + ).content; + if (events && events.length) { + events = events.filter( + e => e[1] == "security.ui.protections" && e[2] == "show" + ); + if (events.length == 1) { + return events; + } + } + return null; + }, "recorded telemetry for showing the report"); + + is(loadEvents.length, 1, `recorded telemetry for showing the report`); + await BrowserTestUtils.reloadTab(tab); + loadEvents = await TestUtils.waitForCondition(() => { + let events = Services.telemetry.snapshotEvents( + Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS, + true + ).content; + if (events && events.length) { + events = events.filter( + e => e[1] == "security.ui.protections" && e[2] == "close" + ); + if (events.length == 1) { + return events; + } + } + return null; + }, "recorded telemetry for closing the report"); + + is(loadEvents.length, 1, `recorded telemetry for closing the report`); + + await BrowserTestUtils.removeTab(tab); +}); + +function waitForTelemetryEventCount(count) { + info("waiting for telemetry event count of " + count); + return TestUtils.waitForCondition(() => { + let events = Services.telemetry.snapshotEvents( + Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS, + false + ).content; + if (!events) { + return null; + } + // Ignore irrelevant events from other parts of the browser. + events = events.filter(e => e[1] == "security.ui.protections"); + info("got " + (events && events.length) + " events"); + if (events.length == count) { + return events; + } + return null; + }, "waiting for telemetry event count of: " + count); +} + +let addArbitraryTimeout = async () => { + // There's an arbitrary interval of 2 seconds in which the content + // processes sync their event data with the parent process, we wait + // this out to ensure that we clear everything that is left over from + // previous tests and don't receive random events in the middle of our tests. + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(c => setTimeout(c, 2000)); +}; + +add_task(async function checkTelemetryClickEvents() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.contentblocking.database.enabled", true], + ["browser.contentblocking.report.monitor.enabled", true], + ["browser.contentblocking.report.lockwise.enabled", true], + ["browser.contentblocking.report.proxy.enabled", true], + ["browser.vpn_promo.enabled", false], + ], + }); + await addArbitraryTimeout(); + + // Clear everything. + Services.telemetry.clearEvents(); + await TestUtils.waitForCondition(() => { + let events = Services.telemetry.snapshotEvents( + Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS, + true + ).content; + return !events || !events.length; + }); + + Services.telemetry.setEventRecordingEnabled("security.ui.protections", true); + + let tab = await BrowserTestUtils.openNewForegroundTab({ + url: "about:protections", + gBrowser, + }); + + // Add user logins. + await Services.logins.addLoginAsync(TEST_LOGIN1); + await BrowserTestUtils.reloadTab(tab); + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + const managePasswordsButton = await ContentTaskUtils.waitForCondition( + () => { + return content.document.getElementById("manage-passwords-button"); + }, + "Manage passwords button exists" + ); + await ContentTaskUtils.waitForCondition( + () => ContentTaskUtils.is_visible(managePasswordsButton), + "manage passwords button is visible" + ); + managePasswordsButton.click(); + }); + + let events = await waitForTelemetryEventCount(4); + events = events.filter( + e => + e[1] == "security.ui.protections" && + e[2] == "click" && + e[3] == "lw_open_button" && + e[4] == "manage_passwords" + ); + is( + events.length, + 1, + `recorded telemetry for lw_open_button when there are no breached passwords` + ); + await BrowserTestUtils.removeTab(gBrowser.selectedTab); + + // Add breached logins. + AboutProtectionsParent.setTestOverride( + mockGetLoginDataWithSyncedDevices(false, 4) + ); + await BrowserTestUtils.reloadTab(tab); + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + const managePasswordsButton = await ContentTaskUtils.waitForCondition( + () => { + return content.document.getElementById("manage-passwords-button"); + }, + "Manage passwords button exists" + ); + await ContentTaskUtils.waitForCondition( + () => ContentTaskUtils.is_visible(managePasswordsButton), + "manage passwords button is visible" + ); + managePasswordsButton.click(); + }); + + events = await waitForTelemetryEventCount(7); + events = events.filter( + e => + e[1] == "security.ui.protections" && + e[2] == "click" && + e[3] == "lw_open_button" && + e[4] == "manage_breached_passwords" + ); + is( + events.length, + 1, + `recorded telemetry for lw_open_button when there are breached passwords` + ); + AboutProtectionsParent.setTestOverride(null); + Services.logins.removeLogin(TEST_LOGIN1); + await BrowserTestUtils.removeTab(gBrowser.selectedTab); + await BrowserTestUtils.reloadTab(tab); + + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + // Show all elements, so we can click on them, even though our user is not logged in. + let hidden_elements = content.document.querySelectorAll(".hidden"); + for (let el of hidden_elements) { + el.style.display = "block "; + } + + const savePasswordsButton = await ContentTaskUtils.waitForCondition(() => { + // Opens an extra tab + return content.document.getElementById("save-passwords-button"); + }, "Save Passwords button exists"); + + savePasswordsButton.click(); + }); + + events = await waitForTelemetryEventCount(10); + events = events.filter( + e => + e[1] == "security.ui.protections" && + e[2] == "click" && + e[3] == "lw_open_button" && + e[4] == "save_passwords" + ); + is( + events.length, + 1, + `recorded telemetry for lw_open_button when there are no stored passwords` + ); + await BrowserTestUtils.removeTab(gBrowser.selectedTab); + + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + const lockwiseAboutLink = await ContentTaskUtils.waitForCondition(() => { + return content.document.getElementById("lockwise-how-it-works"); + }, "lockwiseReportLink exists"); + + lockwiseAboutLink.click(); + }); + + events = await waitForTelemetryEventCount(11); + + events = events.filter( + e => + e[1] == "security.ui.protections" && + e[2] == "click" && + e[3] == "lw_about_link" + ); + is(events.length, 1, `recorded telemetry for lw_about_link`); + + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + let monitorAboutLink = await ContentTaskUtils.waitForCondition(() => { + return content.document.getElementById("monitor-link"); + }, "monitorAboutLink exists"); + + monitorAboutLink.click(); + }); + + events = await waitForTelemetryEventCount(12); + + events = events.filter( + e => + e[1] == "security.ui.protections" && + e[2] == "click" && + e[3] == "mtr_about_link" + ); + is(events.length, 1, `recorded telemetry for mtr_about_link`); + + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + const signUpForMonitorLink = await ContentTaskUtils.waitForCondition(() => { + return content.document.getElementById("sign-up-for-monitor-link"); + }, "signUpForMonitorLink exists"); + + signUpForMonitorLink.click(); + }); + + events = await waitForTelemetryEventCount(13); + + events = events.filter( + e => + e[1] == "security.ui.protections" && + e[2] == "click" && + e[3] == "mtr_signup_button" + ); + is(events.length, 1, `recorded telemetry for mtr_signup_button`); + + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + const socialLearnMoreLink = await ContentTaskUtils.waitForCondition(() => { + return content.document.getElementById("social-link"); + }, "Learn more link for social tab exists"); + + socialLearnMoreLink.click(); + }); + + events = await waitForTelemetryEventCount(14); + + events = events.filter( + e => + e[1] == "security.ui.protections" && + e[2] == "click" && + e[3] == "trackers_about_link" && + e[4] == "social" + ); + is(events.length, 1, `recorded telemetry for social trackers_about_link`); + + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + const cookieLearnMoreLink = await ContentTaskUtils.waitForCondition(() => { + return content.document.getElementById("cookie-link"); + }, "Learn more link for cookie tab exists"); + + cookieLearnMoreLink.click(); + }); + + events = await waitForTelemetryEventCount(15); + + events = events.filter( + e => + e[1] == "security.ui.protections" && + e[2] == "click" && + e[3] == "trackers_about_link" && + e[4] == "cookie" + ); + is(events.length, 1, `recorded telemetry for cookie trackers_about_link`); + + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + const trackerLearnMoreLink = await ContentTaskUtils.waitForCondition(() => { + return content.document.getElementById("tracker-link"); + }, "Learn more link for tracker tab exists"); + + trackerLearnMoreLink.click(); + }); + + events = await waitForTelemetryEventCount(16); + + events = events.filter( + e => + e[1] == "security.ui.protections" && + e[2] == "click" && + e[3] == "trackers_about_link" && + e[4] == "tracker" + ); + is( + events.length, + 1, + `recorded telemetry for content tracker trackers_about_link` + ); + + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + const fingerprinterLearnMoreLink = await ContentTaskUtils.waitForCondition( + () => { + return content.document.getElementById("fingerprinter-link"); + }, + "Learn more link for fingerprinter tab exists" + ); + + fingerprinterLearnMoreLink.click(); + }); + + events = await waitForTelemetryEventCount(17); + + events = events.filter( + e => + e[1] == "security.ui.protections" && + e[2] == "click" && + e[3] == "trackers_about_link" && + e[4] == "fingerprinter" + ); + is( + events.length, + 1, + `recorded telemetry for fingerprinter trackers_about_link` + ); + + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + const cryptominerLearnMoreLink = await ContentTaskUtils.waitForCondition( + () => { + return content.document.getElementById("cryptominer-link"); + }, + "Learn more link for cryptominer tab exists" + ); + + cryptominerLearnMoreLink.click(); + }); + + events = await waitForTelemetryEventCount(18); + + events = events.filter( + e => + e[1] == "security.ui.protections" && + e[2] == "click" && + e[3] == "trackers_about_link" && + e[4] == "cryptominer" + ); + is( + events.length, + 1, + `recorded telemetry for cryptominer trackers_about_link` + ); + + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + const protectionSettings = await ContentTaskUtils.waitForCondition(() => { + return content.document.getElementById("protection-settings"); + }, "protection-settings link exists"); + + protectionSettings.click(); + }); + + events = await waitForTelemetryEventCount(19); + events = events.filter( + e => + e[1] == "security.ui.protections" && + e[2] == "click" && + e[3] == "settings_link" && + e[4] == "header-settings" + ); + is(events.length, 1, `recorded telemetry for settings_link header-settings`); + await BrowserTestUtils.removeTab(gBrowser.selectedTab); + + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + const customProtectionSettings = await ContentTaskUtils.waitForCondition( + () => { + return content.document.getElementById("manage-protections"); + }, + "manage-protections link exists" + ); + // Show element so we can click on it + customProtectionSettings.style.display = "block"; + + customProtectionSettings.click(); + }); + + events = await waitForTelemetryEventCount(20); + events = events.filter( + e => + e[1] == "security.ui.protections" && + e[2] == "click" && + e[3] == "settings_link" && + e[4] == "custom-card-settings" + ); + is( + events.length, + 1, + `recorded telemetry for settings_link custom-card-settings` + ); + await BrowserTestUtils.removeTab(gBrowser.selectedTab); + + // Add breached logins and some resolved breaches. + AboutProtectionsParent.setTestOverride( + mockGetMonitorData({ + potentiallyBreachedLogins: 4, + numBreaches: 3, + numBreachesResolved: 1, + }) + ); + await BrowserTestUtils.reloadTab(tab); + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + const resolveBreachesButton = await ContentTaskUtils.waitForCondition( + () => { + return content.document.getElementById("monitor-partial-breaches-link"); + }, + "Monitor resolve breaches button exists" + ); + + await ContentTaskUtils.waitForCondition( + () => ContentTaskUtils.is_visible(resolveBreachesButton), + "Resolve breaches button is visible" + ); + + resolveBreachesButton.click(); + }); + + events = await waitForTelemetryEventCount(23); + events = events.filter( + e => + e[1] == "security.ui.protections" && + e[2] == "click" && + e[3] == "mtr_report_link" && + e[4] == "resolve_breaches" + ); + is(events.length, 1, `recorded telemetry for resolve breaches button`); + + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + const monitorKnownBreachesBlock = await ContentTaskUtils.waitForCondition( + () => { + return content.document.getElementById("monitor-known-breaches-link"); + }, + "Monitor card known breaches block exists" + ); + + monitorKnownBreachesBlock.click(); + }); + + events = await waitForTelemetryEventCount(24); + events = events.filter( + e => + e[1] == "security.ui.protections" && + e[2] == "click" && + e[3] == "mtr_report_link" && + e[4] == "known_resolved_breaches" + ); + is(events.length, 1, `recorded telemetry for monitor known breaches block`); + + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + const monitorExposedPasswordsBlock = + await ContentTaskUtils.waitForCondition(() => { + return content.document.getElementById( + "monitor-exposed-passwords-link" + ); + }, "Monitor card exposed passwords block exists"); + + monitorExposedPasswordsBlock.click(); + }); + + events = await waitForTelemetryEventCount(25); + events = events.filter( + e => + e[1] == "security.ui.protections" && + e[2] == "click" && + e[3] == "mtr_report_link" && + e[4] == "exposed_passwords_unresolved_breaches" + ); + is( + events.length, + 1, + `recorded telemetry for monitor exposed passwords block` + ); + + // Add breached logins and no resolved breaches. + AboutProtectionsParent.setTestOverride( + mockGetMonitorData({ + potentiallyBreachedLogins: 4, + numBreaches: 3, + numBreachesResolved: 0, + }) + ); + await BrowserTestUtils.reloadTab(tab); + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + const manageBreachesButton = await ContentTaskUtils.waitForCondition(() => { + return content.document.getElementById("monitor-breaches-link"); + }, "Monitor manage breaches button exists"); + + await ContentTaskUtils.waitForCondition( + () => ContentTaskUtils.is_visible(manageBreachesButton), + "Manage breaches button is visible" + ); + + manageBreachesButton.click(); + }); + + events = await waitForTelemetryEventCount(28); + events = events.filter( + e => + e[1] == "security.ui.protections" && + e[2] == "click" && + e[3] == "mtr_report_link" && + e[4] == "manage_breaches" + ); + is(events.length, 1, `recorded telemetry for manage breaches button`); + + // All breaches are resolved. + AboutProtectionsParent.setTestOverride( + mockGetMonitorData({ + potentiallyBreachedLogins: 4, + numBreaches: 3, + numBreachesResolved: 3, + }) + ); + await BrowserTestUtils.reloadTab(tab); + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + const viewReportButton = await ContentTaskUtils.waitForCondition(() => { + return content.document.getElementById("monitor-breaches-link"); + }, "Monitor view report button exists"); + await ContentTaskUtils.waitForCondition( + () => ContentTaskUtils.is_visible(viewReportButton), + "View report button is visible" + ); + + viewReportButton.click(); + }); + + events = await waitForTelemetryEventCount(31); + events = events.filter( + e => + e[1] == "security.ui.protections" && + e[2] == "click" && + e[3] == "mtr_report_link" && + e[4] == "view_report" + ); + is(events.length, 1, `recorded telemetry for view report button`); + + // No breaches are present. + AboutProtectionsParent.setTestOverride( + mockGetMonitorData({ + potentiallyBreachedLogins: 4, + numBreaches: 0, + numBreachesResolved: 0, + }) + ); + await BrowserTestUtils.reloadTab(tab); + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + const viewReportButton = await ContentTaskUtils.waitForCondition(() => { + return content.document.getElementById("monitor-breaches-link"); + }, "Monitor view report button exists"); + await ContentTaskUtils.waitForCondition( + () => ContentTaskUtils.is_visible(viewReportButton), + "View report button is visible" + ); + + viewReportButton.click(); + }); + + events = await waitForTelemetryEventCount(34); + events = events.filter( + e => + e[1] == "security.ui.protections" && + e[2] == "click" && + e[3] == "mtr_report_link" && + e[4] == "view_report" + ); + is(events.length, 2, `recorded telemetry for view report button`); + + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + const monitorEmailBlock = await ContentTaskUtils.waitForCondition(() => { + return content.document.getElementById("monitor-stored-emails-link"); + }, "Monitor card email block exists"); + + monitorEmailBlock.click(); + }); + + events = await waitForTelemetryEventCount(35); + events = events.filter( + e => + e[1] == "security.ui.protections" && + e[2] == "click" && + e[3] == "mtr_report_link" && + e[4] == "stored_emails" + ); + is(events.length, 1, `recorded telemetry for monitor email block`); + + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + const monitorKnownBreachesBlock = await ContentTaskUtils.waitForCondition( + () => { + return content.document.getElementById("monitor-known-breaches-link"); + }, + "Monitor card known breaches block exists" + ); + + monitorKnownBreachesBlock.click(); + }); + + events = await waitForTelemetryEventCount(36); + events = events.filter( + e => + e[1] == "security.ui.protections" && + e[2] == "click" && + e[3] == "mtr_report_link" && + e[4] == "known_unresolved_breaches" + ); + is(events.length, 1, `recorded telemetry for monitor known breaches block`); + + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + const monitorExposedPasswordsBlock = + await ContentTaskUtils.waitForCondition(() => { + return content.document.getElementById( + "monitor-exposed-passwords-link" + ); + }, "Monitor card exposed passwords block exists"); + + monitorExposedPasswordsBlock.click(); + }); + + events = await waitForTelemetryEventCount(37); + events = events.filter( + e => + e[1] == "security.ui.protections" && + e[2] == "click" && + e[3] == "mtr_report_link" && + e[4] == "exposed_passwords_all_breaches" + ); + is( + events.length, + 1, + `recorded telemetry for monitor exposed passwords block` + ); + + // Clean up. + AboutProtectionsParent.setTestOverride(null); + await BrowserTestUtils.removeTab(tab); +}); + +// This tests that telemetry is sent when saveEvents is called. +add_task(async function test_save_telemetry() { + // Clear all scalar telemetry. + Services.telemetry.clearScalars(); + + await TrackingDBService.saveEvents(JSON.stringify(LOG)); + + const scalars = Services.telemetry.getSnapshotForScalars( + "main", + false + ).parent; + is(scalars["contentblocking.trackers_blocked_count"], 6); + + // Use the TrackingDBService API to delete the data. + await TrackingDBService.clearAll(); +}); + +// Test that telemetry is sent if entrypoint param is included, +// and test that it is recorded as default if entrypoint param is not properly included +add_task(async function checkTelemetryLoadEventForEntrypoint() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.contentblocking.database.enabled", false], + ["browser.contentblocking.report.monitor.enabled", false], + ["browser.contentblocking.report.lockwise.enabled", false], + ["browser.contentblocking.report.proxy.enabled", false], + ["browser.vpn_promo.enabled", false], + ], + }); + await addArbitraryTimeout(); + + // Clear everything. + Services.telemetry.clearEvents(); + await TestUtils.waitForCondition(() => { + let events = Services.telemetry.snapshotEvents( + Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS, + true + ).content; + return !events || !events.length; + }); + + Services.telemetry.setEventRecordingEnabled("security.ui.protections", true); + + info("Typo in 'entrypoint' should not be recorded"); + let tab = await BrowserTestUtils.openNewForegroundTab({ + url: "about:protections?entryPoint=newPage", + gBrowser, + }); + + let loadEvents = await TestUtils.waitForCondition(() => { + let events = Services.telemetry.snapshotEvents( + Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS, + true + ).content; + if (events && events.length) { + events = events.filter( + e => + e[1] == "security.ui.protections" && + e[2] == "show" && + e[4] == "direct" + ); + if (events.length == 1) { + return events; + } + } + return null; + }, "recorded telemetry for showing the report contains default 'direct' entrypoint"); + + is( + loadEvents.length, + 1, + `recorded telemetry for showing the report contains default 'direct' entrypoint` + ); + + await BrowserTestUtils.removeTab(tab); + + tab = await BrowserTestUtils.openNewForegroundTab({ + url: "about:protections?entrypoint=page", + gBrowser, + }); + + loadEvents = await TestUtils.waitForCondition(() => { + let events = Services.telemetry.snapshotEvents( + Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS, + true + ).content; + if (events && events.length) { + events = events.filter( + e => + e[1] == "security.ui.protections" && e[2] == "show" && e[4] == "page" + ); + if (events.length == 1) { + return events; + } + } + return null; + }, "recorded telemetry for showing the report contains correct entrypoint"); + + is( + loadEvents.length, + 1, + "recorded telemetry for showing the report contains correct entrypoint" + ); + + // Clean up. + await BrowserTestUtils.removeTab(tab); +}); + +// This test is skipping due to failures on try, it passes locally. +// Test that telemetry is sent from the vpn card +add_task(async function checkTelemetryClickEventsVPN() { + if (Services.sysinfo.getProperty("name") != "Windows_NT") { + ok(true, "User is on an unsupported platform, the vpn card will not show"); + return; + } + await addArbitraryTimeout(); + // Clear everything. + Services.telemetry.clearEvents(); + await TestUtils.waitForCondition(() => { + let events = Services.telemetry.snapshotEvents( + Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS, + true + ).content; + return !events || !events.length; + }); + Services.telemetry.setEventRecordingEnabled("security.ui.protections", true); + + // user is not subscribed to VPN, and is in the us + AboutProtectionsParent.setTestOverride(getVPNOverrides(false, "us")); + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.vpn_promo.enabled", true], + [ + "browser.vpn_promo.disallowed_regions", + "ae,by,cn,cu,iq,ir,kp,om,ru,sd,sy,tm,tr,ua", + ], + ["browser.contentblocking.report.vpn_regions", "us,ca,nz,sg,my,gb"], + ["browser.contentblocking.database.enabled", false], + ["browser.contentblocking.report.monitor.enabled", false], + ["browser.contentblocking.report.lockwise.enabled", false], + ["browser.contentblocking.report.proxy.enabled", false], + ["browser.contentblocking.report.hide_vpn_banner", true], + ["browser.contentblocking.report.vpn-android.url", ""], + ["browser.contentblocking.report.vpn-ios.url", ""], + ["browser.contentblocking.report.vpn.url", ""], + ], + }); + Services.locale.availableLocales = ["en-US"]; + Services.locale.requestedLocales = ["en-US"]; + await promiseSetHomeRegion("US"); + + let tab = await BrowserTestUtils.openNewForegroundTab({ + url: "about:protections", + gBrowser, + }); + + info("checking for vpn link"); + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + const getVPNLink = await ContentTaskUtils.waitForCondition(() => { + return content.document.getElementById("get-vpn-link"); + }, "get vpn link exists"); + await ContentTaskUtils.waitForCondition( + () => ContentTaskUtils.is_visible(getVPNLink), + "get vpn link is visible" + ); + EventUtils.sendMouseEvent( + { type: "click", button: 1 }, + getVPNLink, + content + ); + }); + + let events = await waitForTelemetryEventCount(2); + events = events.filter( + e => + e[1] == "security.ui.protections" && + e[2] == "click" && + e[3] == "vpn_card_link" + ); + is( + events.length, + 1, + `recorded telemetry for vpn_card_link when user is not subscribed` + ); + + // User is subscribed to VPN + AboutProtectionsParent.setTestOverride(getVPNOverrides(true, "us")); + await BrowserTestUtils.reloadTab(tab); + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + const androidVPNLink = await ContentTaskUtils.waitForCondition(() => { + return content.document.getElementById("vpn-google-playstore-link"); + }, "android vpn link exists"); + await ContentTaskUtils.waitForCondition( + () => ContentTaskUtils.is_visible(androidVPNLink), + "android vpn link is visible" + ); + await ContentTaskUtils.waitForCondition(() => { + return content.document + .querySelector(".vpn-card") + .classList.contains("subscribed"); + }, "subscribed class is added to the vpn card"); + + EventUtils.sendMouseEvent( + { type: "click", button: 1 }, + androidVPNLink, + content + ); + }); + + events = await waitForTelemetryEventCount(5); + events = events.filter( + e => + e[1] == "security.ui.protections" && + e[2] == "click" && + e[3] == "vpn_app_link_android" + ); + is(events.length, 1, `recorded telemetry for vpn_app_link_android link`); + + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + const iosVPNLink = await ContentTaskUtils.waitForCondition(() => { + return content.document.getElementById("vpn-app-store-link"); + }, "ios vpn link exists"); + await ContentTaskUtils.waitForCondition( + () => ContentTaskUtils.is_visible(iosVPNLink), + "ios vpn link is visible" + ); + await ContentTaskUtils.waitForCondition(() => { + return content.document + .querySelector(".vpn-card") + .classList.contains("subscribed"); + }, "subscribed class is added to the vpn card"); + + EventUtils.sendMouseEvent( + { type: "click", button: 1 }, + iosVPNLink, + content + ); + }); + + events = await waitForTelemetryEventCount(6); + events = events.filter( + e => + e[1] == "security.ui.protections" && + e[2] == "click" && + e[3] == "vpn_app_link_ios" + ); + is(events.length, 1, `recorded telemetry for vpn_app_link_ios link`); + + // Clean up. + await BrowserTestUtils.removeTab(tab); +}).skip(); + +// This test is skipping due to failures on try, it passes locally. +// Test that telemetry is sent from the vpn banner +add_task(async function checkTelemetryEventsVPNBanner() { + if (Services.sysinfo.getProperty("name") != "Windows_NT") { + ok(true, "User is on an unsupported platform, the vpn card will not show"); + return; + } + AboutProtectionsParent.setTestOverride(getVPNOverrides(false, "us")); + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.vpn_promo.enabled", true], + ["browser.contentblocking.report.vpn_regions", "us,ca,nz,sg,my,gb"], + [ + "browser.vpn_promo.disallowed_regions", + "ae,by,cn,cu,iq,ir,kp,om,ru,sd,sy,tm,tr,ua", + ], + ["browser.contentblocking.database.enabled", false], + ["browser.contentblocking.report.monitor.enabled", false], + ["browser.contentblocking.report.lockwise.enabled", false], + ["browser.contentblocking.report.proxy.enabled", false], + ["browser.contentblocking.report.hide_vpn_banner", false], + ["browser.contentblocking.report.vpn-promo.url", ""], + ], + }); + await addArbitraryTimeout(); + + // The VPN banner only shows if the user is in en* + Services.locale.availableLocales = ["en-US"]; + Services.locale.requestedLocales = ["en-US"]; + + // Clear everything. + Services.telemetry.clearEvents(); + await TestUtils.waitForCondition(() => { + let events = Services.telemetry.snapshotEvents( + Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS, + true + ).content; + return !events || !events.length; + }); + + Services.telemetry.setEventRecordingEnabled("security.ui.protections", true); + // User is not subscribed to VPN + AboutProtectionsParent.setTestOverride(getVPNOverrides(false, "us")); + + let tab = await BrowserTestUtils.openNewForegroundTab({ + url: "about:protections", + gBrowser, + }); + + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + const bannerVPNLink = await ContentTaskUtils.waitForCondition(() => { + return content.document.getElementById("vpn-banner-link"); + }, "vpn banner link exists"); + await ContentTaskUtils.waitForCondition( + () => ContentTaskUtils.is_visible(bannerVPNLink), + "vpn banner link is visible" + ); + EventUtils.sendMouseEvent( + { type: "click", button: 1 }, + bannerVPNLink, + content + ); + }); + + let events = await waitForTelemetryEventCount(3); + events = events.filter( + e => + e[1] == "security.ui.protections" && + e[2] == "click" && + e[3] == "vpn_banner_link" + ); + is(events.length, 1, `recorded telemetry for vpn_banner_link`); + + // VPN Banner flips this pref each time it shows, flip back between each instruction. + await SpecialPowers.pushPrefEnv({ + set: [["browser.contentblocking.report.hide_vpn_banner", false]], + }); + + await BrowserTestUtils.reloadTab(tab); + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + const bannerExitLink = await ContentTaskUtils.waitForCondition(() => { + return content.document.querySelector(".vpn-banner .exit-icon"); + }, "vpn banner exit link exists"); + await ContentTaskUtils.waitForCondition( + () => ContentTaskUtils.is_visible(bannerExitLink), + "vpn banner exit link is visible" + ); + EventUtils.sendMouseEvent( + { type: "click", button: 1 }, + bannerExitLink, + content + ); + }); + + events = await waitForTelemetryEventCount(7); + events = events.filter( + e => + e[1] == "security.ui.protections" && + e[2] == "click" && + e[3] == "vpn_banner_close" + ); + is(events.length, 1, `recorded telemetry for vpn_banner_close`); + + // Clean up. + await BrowserTestUtils.removeTab(tab); +}).skip(); diff --git a/browser/components/protections/test/browser/browser_protections_vpn.js b/browser/components/protections/test/browser/browser_protections_vpn.js new file mode 100644 index 0000000000..9a174c07bf --- /dev/null +++ b/browser/components/protections/test/browser/browser_protections_vpn.js @@ -0,0 +1,282 @@ +/* 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 { AboutProtectionsParent } = ChromeUtils.importESModule( + "resource:///actors/AboutProtectionsParent.sys.mjs" +); + +let { Region } = ChromeUtils.importESModule( + "resource://gre/modules/Region.sys.mjs" +); + +const initialHomeRegion = Region._home; +const initialCurrentRegion = Region._current; + +async function checkVPNCardVisibility(tab, shouldBeHidden, subscribed = false) { + await SpecialPowers.spawn( + tab.linkedBrowser, + [{ _shouldBeHidden: shouldBeHidden, _subscribed: subscribed }], + async function ({ _shouldBeHidden, _subscribed }) { + await ContentTaskUtils.waitForCondition(() => { + const vpnCard = content.document.querySelector(".vpn-card"); + const subscribedStateCorrect = + vpnCard.classList.contains("subscribed") == _subscribed; + return ( + ContentTaskUtils.is_hidden(vpnCard) === _shouldBeHidden && + subscribedStateCorrect + ); + }); + + const visibilityState = _shouldBeHidden ? "hidden" : "shown"; + ok(true, `VPN card is ${visibilityState}.`); + } + ); +} + +async function checkVPNPromoBannerVisibility(tab, shouldBeHidden) { + await SpecialPowers.spawn( + tab.linkedBrowser, + [{ _shouldBeHidden: shouldBeHidden }], + async function ({ _shouldBeHidden }) { + await ContentTaskUtils.waitForCondition(() => { + const vpnBanner = content.document.querySelector(".vpn-banner"); + return ContentTaskUtils.is_hidden(vpnBanner) === _shouldBeHidden; + }); + + const visibilityState = _shouldBeHidden ? "hidden" : "shown"; + ok(true, `VPN banner is ${visibilityState}.`); + } + ); +} + +async function setCurrentRegion(region) { + Region._setCurrentRegion(region); +} + +async function setHomeRegion(region) { + // _setHomeRegion sets a char pref to the value of region. A non-string value will result in an error, so default to an empty string when region is falsey. + Region._setHomeRegion(region || ""); +} + +async function revertRegions() { + setCurrentRegion(initialCurrentRegion); + setHomeRegion(initialHomeRegion); +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.contentblocking.report.monitor.enabled", false], + ["browser.contentblocking.report.lockwise.enabled", false], + ["browser.vpn_promo.enabled", true], + ], + }); + AboutProtectionsParent.setTestOverride(getVPNOverrides(false)); + setCurrentRegion("us"); + const avLocales = Services.locale.availableLocales; + + registerCleanupFunction(() => { + Services.locale.availableLocales = avLocales; + }); +}); + +add_task(async function testVPNCardVisibility() { + AboutProtectionsParent.setTestOverride(getVPNOverrides(false)); + await promiseSetHomeRegion("us"); + setCurrentRegion("us"); + + let tab = await BrowserTestUtils.openNewForegroundTab({ + url: "about:protections", + gBrowser, + }); + + info("Enable showing the VPN card"); + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.vpn_promo.enabled", true], + ["browser.contentblocking.report.vpn_regions", "us,ca,nz,sg,my,gb"], + [ + "browser.vpn_promo.disallowed_regions", + "ae,by,cn,cu,iq,ir,kp,om,ru,sd,sy,tm,tr,ua", + ], + ], + }); + + info( + "Check that vpn card is hidden if neither the user's home nor current location is on the regions list." + ); + AboutProtectionsParent.setTestOverride(getVPNOverrides(false)); + setCurrentRegion("ls"); + await promiseSetHomeRegion("ls"); + await BrowserTestUtils.reloadTab(tab); + await checkVPNCardVisibility(tab, true); + + info( + "Check that vpn card is hidden if user's location is in the list of disallowed regions." + ); + AboutProtectionsParent.setTestOverride(getVPNOverrides(false)); + setCurrentRegion("sy"); + await BrowserTestUtils.reloadTab(tab); + await checkVPNCardVisibility(tab, true); + + info( + "Check that vpn card shows a different version if user has subscribed to Mozilla vpn." + ); + AboutProtectionsParent.setTestOverride(getVPNOverrides(true)); + setCurrentRegion("us"); + await BrowserTestUtils.reloadTab(tab); + await checkVPNCardVisibility(tab, false, true); + + info( + "VPN card should be hidden when vpn not enabled, though all other conditions are true" + ); + await SpecialPowers.pushPrefEnv({ + set: [["browser.vpn_promo.enabled", false]], + }); + await BrowserTestUtils.reloadTab(tab); + await checkVPNCardVisibility(tab, true); + + await BrowserTestUtils.removeTab(tab); + revertRegions(); +}); + +add_task(async function testVPNPromoBanner() { + AboutProtectionsParent.setTestOverride(getVPNOverrides(false)); + + let tab = await BrowserTestUtils.openNewForegroundTab({ + url: "about:protections", + gBrowser, + }); + + info("Enable showing the VPN card and banner"); + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.vpn_promo.enabled", true], + ["browser.contentblocking.report.vpn_regions", "us,ca,nz,sg,my,gb"], + [ + "browser.vpn_promo.disallowed_regions", + "ae,by,cn,cu,iq,ir,kp,om,ru,sd,sy,tm,tr,ua", + ], + ["browser.contentblocking.report.hide_vpn_banner", false], + ], + }); + + info("Check that vpn banner is shown if user's region is supported"); + setCurrentRegion("us"); + await BrowserTestUtils.reloadTab(tab); + await checkVPNPromoBannerVisibility(tab, false); + + is( + Services.prefs.getBoolPref( + "browser.contentblocking.report.hide_vpn_banner", + false + ), + true, + "After showing the banner once, the pref to hide the VPN banner is flipped" + ); + info("The banner does not show when the pref to hide it is flipped"); + await BrowserTestUtils.reloadTab(tab); + await checkVPNPromoBannerVisibility(tab, true); + + // VPN Banner flips this pref each time it shows, flip back between each instruction. + await SpecialPowers.pushPrefEnv({ + set: [["browser.contentblocking.report.hide_vpn_banner", false]], + }); + + info( + "Check that VPN banner is hidden if user's location is not on the regions list." + ); + AboutProtectionsParent.setTestOverride(getVPNOverrides(false)); + setCurrentRegion("ls"); + await setHomeRegion("ls'"); + await BrowserTestUtils.reloadTab(tab); + await checkVPNPromoBannerVisibility(tab, true); + + info( + "Check that VPN banner is hidden if user's location is in the disallowed regions list." + ); + AboutProtectionsParent.setTestOverride(getVPNOverrides(false)); + setCurrentRegion("sy"); + await BrowserTestUtils.reloadTab(tab); + await checkVPNPromoBannerVisibility(tab, true); + + info( + "VPN banner should be hidden when vpn not enabled, though all other conditions are true" + ); + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.vpn_promo.enabled", false], + ["browser.contentblocking.report.hide_vpn_banner", false], + ], + }); + await BrowserTestUtils.reloadTab(tab); + await checkVPNPromoBannerVisibility(tab, true); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.vpn_promo.enabled", true], + ["browser.contentblocking.report.hide_vpn_banner", false], + ], + }); + + info("If user is subscribed to VPN already the promo banner should not show"); + AboutProtectionsParent.setTestOverride(getVPNOverrides(true)); + setCurrentRegion("us"); + await BrowserTestUtils.reloadTab(tab); + await checkVPNPromoBannerVisibility(tab, true); + + await BrowserTestUtils.removeTab(tab); + revertRegions(); +}); + +// Expect the vpn card and banner to not show as we are expressly excluding China. Even when cn is in the supported region pref. +add_task(async function testVPNDoesNotShowChina() { + AboutProtectionsParent.setTestOverride(getVPNOverrides(false)); + setCurrentRegion("us"); + let tab = await BrowserTestUtils.openNewForegroundTab({ + url: "about:protections", + gBrowser, + }); + + info("Enable showing the VPN card and banners"); + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.vpn_promo.enabled", true], + ["browser.contentblocking.report.vpn_regions", "us,ca,nz,sg,my,gb,cn"], + [ + "browser.vpn_promo.disallowed_regions", + "ae,by,cn,cu,iq,ir,kp,om,ru,sd,sy,tm,tr,ua", + ], + ["browser.contentblocking.report.hide_vpn_banner", false], + ], + }); + + info( + "set home location to China, even though user is currently in the US, expect vpn card to be hidden" + ); + await promiseSetHomeRegion("CN"); + await BrowserTestUtils.reloadTab(tab); + await checkVPNPromoBannerVisibility(tab, true); + await BrowserTestUtils.reloadTab(tab); + await checkVPNCardVisibility(tab, true); + + // VPN Banner flips this pref each time it shows, flip back between each instruction. + await SpecialPowers.pushPrefEnv({ + set: [["browser.contentblocking.report.hide_vpn_banner", false]], + }); + + info("home region is US, but current location is China"); + AboutProtectionsParent.setTestOverride(getVPNOverrides(false)); + await promiseSetHomeRegion("US"); + setCurrentRegion("CN"); + await BrowserTestUtils.reloadTab(tab); + await checkVPNPromoBannerVisibility(tab, true); + await BrowserTestUtils.reloadTab(tab); + await checkVPNCardVisibility(tab, true); + + await BrowserTestUtils.removeTab(tab); + revertRegions(); +}); diff --git a/browser/components/protections/test/browser/head.js b/browser/components/protections/test/browser/head.js new file mode 100644 index 0000000000..9815869ee5 --- /dev/null +++ b/browser/components/protections/test/browser/head.js @@ -0,0 +1,96 @@ +/* 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 no-unused-vars */ + +"use strict"; + +const nsLoginInfo = new Components.Constructor( + "@mozilla.org/login-manager/loginInfo;1", + Ci.nsILoginInfo, + "init" +); + +ChromeUtils.defineESModuleGetters(this, { + Region: "resource://gre/modules/Region.sys.mjs", +}); + +const { SearchTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/SearchTestUtils.sys.mjs" +); + +const TEST_LOGIN1 = new nsLoginInfo( + "https://example.com/", + "https://example.com/", + null, + "user1", + "pass1", + "username", + "password" +); + +const TEST_LOGIN2 = new nsLoginInfo( + "https://2.example.com/", + "https://2.example.com/", + null, + "user2", + "pass2", + "username", + "password" +); + +// Used to replace AboutProtectionsHandler.getLoginData in front-end tests. +const mockGetLoginDataWithSyncedDevices = ( + mobileDeviceConnected = false, + potentiallyBreachedLogins = 0 +) => { + return { + getLoginData: () => { + return { + numLogins: Services.logins.countLogins("", "", ""), + potentiallyBreachedLogins, + mobileDeviceConnected, + }; + }, + }; +}; + +// Used to replace AboutProtectionsHandler.getMonitorData in front-end tests. +const mockGetMonitorData = data => { + return { + getMonitorData: () => { + if (data.error) { + return data; + } + + return { + monitoredEmails: 1, + numBreaches: data.numBreaches, + passwords: 8, + numBreachesResolved: data.numBreachesResolved, + passwordsResolved: 1, + error: false, + }; + }, + }; +}; + +registerCleanupFunction(function head_cleanup() { + Services.logins.removeAllUserFacingLogins(); +}); + +// Used to replace AboutProtectionsParent.VPNSubStatus +const getVPNOverrides = (hasSubscription = false) => { + return { + vpnOverrides: () => { + return hasSubscription; + }, + }; +}; + +const promiseSetHomeRegion = async region => { + let promise = SearchTestUtils.promiseSearchNotification("engines-reloaded"); + Region._setHomeRegion(region); + await promise; +}; |