From 53504de08df00e8a0fd32562f2ee03abedd36e91 Mon Sep 17 00:00:00 2001 From: Sam Macbeth Date: Wed, 5 Feb 2025 11:54:33 +0100 Subject: [PATCH 01/11] Fix breakage report race condition. Config processing can how take one tick after the config ready promise resolves. Waiting for allLoadingFinished should ensure that everything is ready. --- shared/js/background/broken-site-report.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/shared/js/background/broken-site-report.js b/shared/js/background/broken-site-report.js index 69532f952..51ea6304b 100644 --- a/shared/js/background/broken-site-report.js +++ b/shared/js/background/broken-site-report.js @@ -321,6 +321,8 @@ export async function breakageReportForTab({ export async function sendBreakageReportForCurrentTab({ pixelName, currentTab, category, description, reportFlow }) { await settings.ready(); await tdsStorage.ready('config'); + // wait for onload callbacks (to ensure that config has been correctly processed) + await tdsStorage.config.allLoadingFinished const tab = currentTab || (await tabManager.getOrRestoreCurrentTab()); if (!tab) { From fbd3cb3dbc6f5dbffb3030ddfc7e41816705335d Mon Sep 17 00:00:00 2001 From: Sam Macbeth Date: Thu, 6 Feb 2025 12:33:49 +0100 Subject: [PATCH 02/11] Create a dashboard messaging component to handle dashboard data and broken site report messages --- shared/js/background/background.js | 7 +- shared/js/background/broken-site-report.js | 44 --------- .../components/dashboard-messaging.js | 92 +++++++++++++++++++ .../background/components/toggle-reports.js | 20 ++-- shared/js/background/message-handlers.js | 47 ---------- shared/js/background/storage/tds.js | 3 +- 6 files changed, 114 insertions(+), 99 deletions(-) create mode 100644 shared/js/background/components/dashboard-messaging.js diff --git a/shared/js/background/background.js b/shared/js/background/background.js index eda6b256d..5f0664ca6 100644 --- a/shared/js/background/background.js +++ b/shared/js/background/background.js @@ -29,6 +29,7 @@ import DebuggerConnection from './components/debugger-connection'; import Devtools from './components/devtools'; import DNRListeners from './components/dnr-listeners'; import RemoteConfig from './components/remote-config'; +import DashboardMessaging from './components/dashboard-messaging'; import initDebugBuild from './devbuild'; import initReloader from './devbuild-reloader'; import tabManager from './tab-manager'; @@ -51,14 +52,17 @@ const remoteConfig = new RemoteConfig({ settings }); const abnMetrics = BUILD_TARGET !== 'firefox' ? new AbnExperimentMetrics({ remoteConfig }) : null; const tds = new TDSStorage({ settings, remoteConfig, abnMetrics }); const devtools = new Devtools({ tds }); +const dashboardMessaging = new DashboardMessaging({ settings, tds, tabManager }) /** * @type {{ * autofill: EmailAutofill; + * dashboardMessaging: DashboardMessaging * omnibox: OmniboxSearch; * fireButton?: FireButton; * internalUser: InternalUserDetector; * tds: TDSStorage; * tabTracking: TabTracker; + * toggleReports: ToggleReports; * trackers: TrackersGlobal; * remoteConfig: RemoteConfig; * abnMetrics: AbnExperimentMetrics?; @@ -67,11 +71,12 @@ const devtools = new Devtools({ tds }); */ const components = { autofill: new EmailAutofill({ settings }), + dashboardMessaging, omnibox: new OmniboxSearch(), internalUser: new InternalUserDetector({ settings }), tabTracking: new TabTracker({ tabManager, devtools }), tds, - toggleReports: new ToggleReports(), + toggleReports: new ToggleReports({ dashboardMessaging }), trackers: new TrackersGlobal({ tds }), debugger: new DebuggerConnection({ tds, devtools }), devtools, diff --git a/shared/js/background/broken-site-report.js b/shared/js/background/broken-site-report.js index 51ea6304b..270626ce0 100644 --- a/shared/js/background/broken-site-report.js +++ b/shared/js/background/broken-site-report.js @@ -10,15 +10,12 @@ * @typedef {import('@duckduckgo/privacy-dashboard/schema/__generated__/schema.types').DataItemId} DisclosureParamId */ -const browser = require('webextension-polyfill'); const load = require('./load'); const browserWrapper = require('./wrapper'); const settings = require('./settings'); const parseUserAgentString = require('../shared-utils/parse-user-agent-string'); const { getCurrentTab, getURLWithoutQueryString } = require('./utils'); const { getURL } = require('./pixels'); -const tdsStorage = require('./storage/tds').default; -const tabManager = require('./tab-manager'); const maxPixelLength = 7000; /** @@ -307,47 +304,6 @@ export async function breakageReportForTab({ return fire(pixelName, brokenSiteParams.toString()); } -/** - * Attempt to send a breakage report for the currently focused tab. - * - * @param {Object} arg - * @prop {string} pixelName - * @prop {import("./classes/tab") | undefined} arg.currentTab - * @prop {string | undefined} arg.category - * @prop {string | undefined} arg.description - * @prop {string | undefined} arg.reportFlow - * String detailing the UI flow that this breakage report came from. - */ -export async function sendBreakageReportForCurrentTab({ pixelName, currentTab, category, description, reportFlow }) { - await settings.ready(); - await tdsStorage.ready('config'); - // wait for onload callbacks (to ensure that config has been correctly processed) - await tdsStorage.config.allLoadingFinished - - const tab = currentTab || (await tabManager.getOrRestoreCurrentTab()); - if (!tab) { - return; - } - - const pageParams = (await browser.tabs.sendMessage(tab.id, { getBreakagePageParams: true })) || {}; - - const tds = settings.getSetting('tds-etag'); - const remoteConfigEtag = settings.getSetting('config-etag'); - const remoteConfigVersion = tdsStorage.config.version; - - return await breakageReportForTab({ - pixelName, - tab, - tds, - remoteConfigEtag, - remoteConfigVersion, - category, - description, - pageParams, - reportFlow, - }); -} - /** * Returns the breakage report details as expected by the * "getBreakageFormOptions" and "getToggleReportOptions" messages. diff --git a/shared/js/background/components/dashboard-messaging.js b/shared/js/background/components/dashboard-messaging.js new file mode 100644 index 000000000..9624eb992 --- /dev/null +++ b/shared/js/background/components/dashboard-messaging.js @@ -0,0 +1,92 @@ +import browser from 'webextension-polyfill'; +import { breakageReportForTab, getDisclosureDetails } from '../broken-site-report'; +import { dashboardDataFromTab } from '../classes/privacy-dashboard-data'; +import { registerMessageHandler } from '../message-handlers'; +import { getCurrentTab } from '../utils'; +import { isFireButtonEnabled } from './fire-button'; + +/** + * Message handlers for communication from the dashboard to the extension background. + * + * Note, additional message handles for toggle reports is in the separate ToggleReports component. + */ +export default class DashboardMessaging { + /** + * @param {{ + * settings: import('../settings.js'); + * tds: import('./tds').default; + * tabManager: import('../tab-manager.js'); + * }} args + */ + constructor({ settings, tds, tabManager }) { + this.settings = settings; + this.tds = tds; + this.tabManager = tabManager; + + registerMessageHandler('submitBrokenSiteReport', (report) => this.submitBrokenSiteReport(report)); + registerMessageHandler('getPrivacyDashboardData', this.getPrivacyDashboardData.bind(this)); + registerMessageHandler('getBreakageFormOptions', getDisclosureDetails); + } + + /** + * Only the dashboard sends this message, so we import the types from there. + * @param {import('@duckduckgo/privacy-dashboard/schema/__generated__/schema.types').BreakageReportRequest} breakageReport + * @param {string} [pixelName] + * @param {string} [reportFlow] + * @returns {Promise} + */ + async submitBrokenSiteReport(breakageReport, pixelName = 'epbf', reportFlow = undefined) { + // wait for config and TDS so we can get etags and config version + await Promise.all([this.tds.remoteConfig.allLoadingFinished, this.tds.tds.ready]); + const { category, description } = breakageReport; + const tab = await this.tabManager.getOrRestoreCurrentTab(); + if (!tab) { + return; + } + const pageParams = (await browser.tabs.sendMessage(tab.id, { getBreakagePageParams: true })) || {}; + const tds = this.tds.tds.etag; + const remoteConfigEtag = this.tds.remoteConfig.etag; + const remoteConfigVersion = this.tds.remoteConfig.config?.version; + return breakageReportForTab({ + pixelName, + tab, + tds, + remoteConfigEtag, + remoteConfigVersion, + category, + description, + pageParams, + reportFlow, + }); + } + + /** + * This message is here to ensure the privacy dashboard can render + * from a single call to the extension. + * + * Currently, it will collect data for the current tab and email protection + * user data. + */ + async getPrivacyDashboardData(options) { + let { tabId } = options; + if (tabId === null) { + const currentTab = await getCurrentTab(); + if (!currentTab?.id) { + throw new Error('could not get the current tab...'); + } + tabId = currentTab?.id; + } + + // Await for storage to be ready; this happens on service worker closing mostly. + await this.settings.ready(); + await this.tds.config.ready; + + const tab = await this.tabManager.getOrRestoreTab(tabId); + if (!tab) throw new Error('unreachable - cannot access current tab with ID ' + tabId); + const userData = this.settings.getSetting('userData'); + const fireButtonData = { + enabled: isFireButtonEnabled, + }; + return dashboardDataFromTab(tab, userData, fireButtonData); + } +} diff --git a/shared/js/background/components/toggle-reports.js b/shared/js/background/components/toggle-reports.js index 908a49810..490bfd71e 100644 --- a/shared/js/background/components/toggle-reports.js +++ b/shared/js/background/components/toggle-reports.js @@ -3,7 +3,7 @@ import { registerMessageHandler } from '../message-handlers'; import { postPopupMessage } from '../popup-messaging'; import settings from '../settings'; import { getFeatureSettings, reloadCurrentTab, resolveAfterDelay } from '../utils'; -import { getDisclosureDetails, sendBreakageReportForCurrentTab } from '../broken-site-report'; +import { getDisclosureDetails } from '../broken-site-report'; import { createAlarm } from '../wrapper'; import tabManager from '../tab-manager'; @@ -23,7 +23,14 @@ import tabManager from '../tab-manager'; export default class ToggleReports { static ALARM_NAME = 'toggleReportsClearExpired'; - constructor() { + /** + * + * @param {{ + * dashboardMessaging: import('./dashboard-messaging').default + * }} args + */ + constructor({ dashboardMessaging }) { + this.dashboardMessaging = dashboardMessaging; this.onDisconnect = this.toggleReportFinished.bind(this, false); registerMessageHandler('getToggleReportOptions', (_, sender) => this.toggleReportStarted(sender)); @@ -81,10 +88,11 @@ export default class ToggleReports { try { // Send the breakage report before reloading the page, to ensure // the correct page details are sent with the report. - await sendBreakageReportForCurrentTab({ - pixelName: 'protection-toggled-off-breakage-report', - reportFlow: 'on_protections_off_dashboard_main', - }); + await this.dashboardMessaging.submitBrokenSiteReport( + {}, + 'protection-toggled-off-breakage-report', + 'on_protections_off_dashboard_main', + ); } catch (e) { // Catch this, mostly to ensure the page is still reloaded if // sending the breakage report fails. diff --git a/shared/js/background/message-handlers.js b/shared/js/background/message-handlers.js index c4eb272f9..91d369096 100644 --- a/shared/js/background/message-handlers.js +++ b/shared/js/background/message-handlers.js @@ -1,13 +1,10 @@ import browser from 'webextension-polyfill'; -import { dashboardDataFromTab } from './classes/privacy-dashboard-data'; -import { getDisclosureDetails, sendBreakageReportForCurrentTab } from './broken-site-report'; import parseUserAgentString from '../shared-utils/parse-user-agent-string'; import { getExtensionURL } from './wrapper'; import { isFeatureEnabled, reloadCurrentTab } from './utils'; import { ensureClickToLoadRuleActionDisabled } from './dnr-click-to-load'; import tdsStorage from './storage/tds'; import { getArgumentsObject } from './helpers/arguments-object'; -import { isFireButtonEnabled } from './components/fire-button'; import { postPopupMessage } from './popup-messaging'; import ToggleReports from './components/toggle-reports'; const utils = require('./utils'); @@ -99,47 +96,6 @@ export function openOptions() { } } -/** - * Only the dashboard sends this message, so we import the types from there. - * @param {import('@duckduckgo/privacy-dashboard/schema/__generated__/schema.types').BreakageReportRequest} breakageReport - * @returns {Promise} - */ -export function submitBrokenSiteReport(breakageReport) { - const pixelName = 'epbf'; - const { category, description } = breakageReport; - return sendBreakageReportForCurrentTab({ pixelName, category, description }); -} - -/** - * This message is here to ensure the privacy dashboard can render - * from a single call to the extension. - * - * Currently, it will collect data for the current tab and email protection - * user data. - */ -export async function getPrivacyDashboardData(options) { - let { tabId } = options; - if (tabId === null) { - const currentTab = await utils.getCurrentTab(); - if (!currentTab?.id) { - throw new Error('could not get the current tab...'); - } - tabId = currentTab?.id; - } - - // Await for storage to be ready; this happens on service worker closing mostly. - await settings.ready(); - await tdsStorage.ready('config'); - - const tab = await tabManager.getOrRestoreTab(tabId); - if (!tab) throw new Error('unreachable - cannot access current tab with ID ' + tabId); - const userData = settings.getSetting('userData'); - const fireButtonData = { - enabled: isFireButtonEnabled, - }; - return dashboardDataFromTab(tab, userData, fireButtonData); -} - export function getTopBlockedByPages(options) { return Companies.getTopBlockedByPages(options); } @@ -344,9 +300,6 @@ const messageHandlers = { allowlistOptIn, getBrowser, openOptions, - submitBrokenSiteReport, - getBreakageFormOptions: getDisclosureDetails, - getPrivacyDashboardData, getTopBlockedByPages, getClickToLoadState, getYouTubeVideoDetails, diff --git a/shared/js/background/storage/tds.js b/shared/js/background/storage/tds.js index 0168ec480..bbbe562aa 100644 --- a/shared/js/background/storage/tds.js +++ b/shared/js/background/storage/tds.js @@ -15,6 +15,7 @@ export default { _config: { features: {} }, _tds: { entities: {}, trackers: {}, domains: {}, cnames: {} }, _surrogates: '', + /** @type {import('@duckduckgo/privacy-configuration/schema/config').GenericV4Config} */ get config() { return globalThis.components?.remoteConfig.config || this._config; }, @@ -60,7 +61,7 @@ export default { return Promise.resolve(); } if (configName && listNames.includes(configName)) { - return tdsStorage[configName].ready; + return tdsStorage[configName].allLoadingFinished; } return Promise.all(listNames.map((n) => tdsStorage[n].ready)); }, From 7f4d8125386b17085f72d2a3ea919960f168c877 Mon Sep 17 00:00:00 2001 From: Sam Macbeth Date: Thu, 6 Feb 2025 12:51:15 +0100 Subject: [PATCH 03/11] Update tests to use a submitBrokenSiteReport stub --- shared/js/background/background.js | 2 +- unit-test/background/toggle-reports.js | 59 +++++--------------------- 2 files changed, 12 insertions(+), 49 deletions(-) diff --git a/shared/js/background/background.js b/shared/js/background/background.js index 5f0664ca6..4c6600cad 100644 --- a/shared/js/background/background.js +++ b/shared/js/background/background.js @@ -52,7 +52,7 @@ const remoteConfig = new RemoteConfig({ settings }); const abnMetrics = BUILD_TARGET !== 'firefox' ? new AbnExperimentMetrics({ remoteConfig }) : null; const tds = new TDSStorage({ settings, remoteConfig, abnMetrics }); const devtools = new Devtools({ tds }); -const dashboardMessaging = new DashboardMessaging({ settings, tds, tabManager }) +const dashboardMessaging = new DashboardMessaging({ settings, tds, tabManager }); /** * @type {{ * autofill: EmailAutofill; diff --git a/unit-test/background/toggle-reports.js b/unit-test/background/toggle-reports.js index c1cbc44d2..0934e4472 100644 --- a/unit-test/background/toggle-reports.js +++ b/unit-test/background/toggle-reports.js @@ -1,17 +1,20 @@ import browser from 'webextension-polyfill'; -import load from '../../shared/js/background/load'; import settings from '../../shared/js/background/settings'; import ToggleReports from '../../shared/js/background/components/toggle-reports'; -import { _formatPixelRequestForTesting } from '../../shared/js/shared-utils/pixels'; import tabManager from '../../shared/js/background/tab-manager'; import tdsStorageStub from '../helpers/tds'; describe('ToggleReports', () => { - const actualSentReports = []; let currentTabDetails = null; let currentTimestamp = 1; let settingsStorage = null; - const toggleReports = new ToggleReports(); + const toggleReports = new ToggleReports({ + dashboardMessaging: { + submitBrokenSiteReport: () => {}, + }, + }); + /** @type {jasmine.Spy} */ + let submittedReports; let toggleReportsConfig = null; // Dummy timestamps. @@ -61,17 +64,10 @@ describe('ToggleReports', () => { }); spyOn(Date, 'now').and.callFake(() => currentTimestamp); - // Stub the load.url function (used for pixel requests). - spyOn(load, 'url').and.callFake((url) => { - const pixel = _formatPixelRequestForTesting(url); - if (pixel?.name?.startsWith('epbf') || pixel?.name?.startsWith('protection-toggled-off-breakage-report')) { - actualSentReports.push(pixel); - } - }); + submittedReports = spyOn(toggleReports.dashboardMessaging, 'submitBrokenSiteReport'); }); beforeEach(() => { - actualSentReports.length = 0; currentTabDetails = null; currentTimestamp = 1; settingsStorage.clear(); @@ -116,8 +112,8 @@ describe('ToggleReports', () => { it('toggleReportFinished()', async () => { const expectReports = async (reports, accepted, declined) => { expect(await ToggleReports.countResponses()).toEqual({ accepted, declined }); - expect(actualSentReports).toEqual(reports); - actualSentReports.length = 0; + expect(submittedReports).toHaveBeenCalledTimes(reports.length); + expect(submittedReports.calls.all().map((c) => c.args)).toEqual(reports); }; // Set things up, so that breakage reports can be sent. @@ -148,40 +144,7 @@ describe('ToggleReports', () => { // If user accepts, report should be sent. await toggleReports.toggleReportFinished(true); - await expectReports( - [ - { - name: 'protection-toggled-off-breakage-report_chrome', - params: { - siteUrl: 'https://domain.example/path', - tds: 'tds-etag-123', - remoteConfigEtag: 'config-etag-123', - remoteConfigVersion: '2021.6.7', - upgradedHttps: 'false', - urlParametersRemoved: 'false', - ctlYouTube: 'false', - ctlFacebookPlaceholderShown: 'false', - ctlFacebookLogin: 'false', - performanceWarning: 'false', - userRefreshCount: '0', - jsPerformance: 'undefined', - locale: 'en-US', - errorDescriptions: '[]', - openerContext: 'external', - reportFlow: 'on_protections_off_dashboard_main', - extensionVersion: '1234.56', - ignoreRequests: '', - blockedTrackers: '', - surrogates: '', - noActionRequests: '', - adAttributionRequests: '', - ignoredByUserRequests: '', - }, - }, - ], - 1, - 2, - ); + await expectReports([[{}, 'protection-toggled-off-breakage-report', 'on_protections_off_dashboard_main']], 1, 2); // Tidy up. tabManager.delete(currentTabDetails.id); From 9e4c3d080782b2fc679d1fdb4683c708b76e1996 Mon Sep 17 00:00:00 2001 From: Sam Macbeth Date: Thu, 13 Feb 2025 12:29:45 +0100 Subject: [PATCH 04/11] Update shared/js/background/components/dashboard-messaging.js Co-authored-by: Dave Vandyke --- shared/js/background/components/dashboard-messaging.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/js/background/components/dashboard-messaging.js b/shared/js/background/components/dashboard-messaging.js index 9624eb992..e11259b6c 100644 --- a/shared/js/background/components/dashboard-messaging.js +++ b/shared/js/background/components/dashboard-messaging.js @@ -8,7 +8,7 @@ import { isFireButtonEnabled } from './fire-button'; /** * Message handlers for communication from the dashboard to the extension background. * - * Note, additional message handles for toggle reports is in the separate ToggleReports component. + * Note, additional message handlers for toggle reports is in the separate ToggleReports component. */ export default class DashboardMessaging { /** From 04964d6add521c13679f015383ed5652e940544e Mon Sep 17 00:00:00 2001 From: Sam Macbeth Date: Fri, 14 Feb 2025 11:58:34 +0100 Subject: [PATCH 05/11] Unit tests for broken site report sending. --- .../components/dashboard-messaging.js | 2 +- shared/js/background/message-handlers.js | 3 - unit-test/background/abn-framework.js | 15 +-- unit-test/background/dashboard-messaging.js | 120 ++++++++++++++++++ unit-test/helpers/mocks.js | 51 ++++++++ 5 files changed, 173 insertions(+), 18 deletions(-) create mode 100644 unit-test/background/dashboard-messaging.js create mode 100644 unit-test/helpers/mocks.js diff --git a/shared/js/background/components/dashboard-messaging.js b/shared/js/background/components/dashboard-messaging.js index e11259b6c..7e2f817da 100644 --- a/shared/js/background/components/dashboard-messaging.js +++ b/shared/js/background/components/dashboard-messaging.js @@ -37,7 +37,7 @@ export default class DashboardMessaging { */ async submitBrokenSiteReport(breakageReport, pixelName = 'epbf', reportFlow = undefined) { // wait for config and TDS so we can get etags and config version - await Promise.all([this.tds.remoteConfig.allLoadingFinished, this.tds.tds.ready]); + await Promise.all([this.tds.remoteConfig.ready, this.tds.tds.ready]); const { category, description } = breakageReport; const tab = await this.tabManager.getOrRestoreCurrentTab(); if (!tab) { diff --git a/shared/js/background/message-handlers.js b/shared/js/background/message-handlers.js index 91d369096..a88dc7586 100644 --- a/shared/js/background/message-handlers.js +++ b/shared/js/background/message-handlers.js @@ -279,9 +279,6 @@ export function addDebugFlag(message, sender, req) { * @param {(options: any, sender: any, req: any) => any} func */ export function registerMessageHandler(name, func) { - if (messageHandlers[name]) { - throw new Error(`Attempt to re-register existing message handler ${name}`); - } messageHandlers[name] = func; } diff --git a/unit-test/background/abn-framework.js b/unit-test/background/abn-framework.js index e94f7528e..7d85e797a 100644 --- a/unit-test/background/abn-framework.js +++ b/unit-test/background/abn-framework.js @@ -7,23 +7,10 @@ import load from '../../shared/js/background/load'; import commonParams from '../../pixel-definitions/common_params.json'; import commonSuffixes from '../../pixel-definitions/common_suffixes.json'; import experimentPixels from '../../pixel-definitions/pixels/experiments.json'; +import { MockSettings } from '../helpers/mocks'; const ONE_HOUR_MS = 1000 * 60 * 60; -class MockSettings { - constructor() { - this.mockSettingData = new Map(); - this.ready = () => Promise.resolve(); - } - - getSetting(key) { - return structuredClone(this.mockSettingData.get(key)); - } - updateSetting(key, value) { - this.mockSettingData.set(key, value); - } -} - function constructMockComponents() { // clear message handlers to prevent conflict when registering Object.keys(messageHandlers).forEach((k) => delete messageHandlers[k]); diff --git a/unit-test/background/dashboard-messaging.js b/unit-test/background/dashboard-messaging.js new file mode 100644 index 000000000..e7d968e8a --- /dev/null +++ b/unit-test/background/dashboard-messaging.js @@ -0,0 +1,120 @@ +import browser from 'webextension-polyfill'; +import load from '../../shared/js/background/load'; +import tabManager from '../../shared/js/background/tab-manager'; +import DashboardMessaging from '../../shared/js/background/components/dashboard-messaging'; +import { MockSettings, mockTdsStorage } from '../helpers/mocks'; +import { _formatPixelRequestForTesting } from '../../shared/js/shared-utils/pixels'; + +const defaultBrokenSitePixelParams = { + upgradedHttps: 'false', + urlParametersRemoved: 'false', + ctlYouTube: 'false', + ctlFacebookPlaceholderShown: 'false', + ctlFacebookLogin: 'false', + performanceWarning: 'false', + userRefreshCount: '0', + jsPerformance: 'undefined', + locale: 'en-US', + errorDescriptions: '[]', + openerContext: 'external', + extensionVersion: '1234.56', + ignoreRequests: '', + blockedTrackers: '', + surrogates: '', + noActionRequests: '', + adAttributionRequests: '', + ignoredByUserRequests: '', +}; + +describe('DashboardMessaging component', () => { + describe('submitBrokenSiteReport', () => { + let currentTabDetails = null; + const actualSentReports = []; + /** @type {DashboardMessaging} */ + let dashboardMessaging = null; + /** @type {import('../../shared/js/background/components/tds').default} */ + let tds = null; + + beforeEach(() => { + currentTabDetails = null; + actualSentReports.length = 0; + spyOn(browser.tabs, 'sendMessage').and.callFake((tabId, message) => { + if (message.getBreakagePageParams) { + return Promise.resolve({}); + } + }); + spyOn(load, 'url').and.callFake((url) => { + const pixel = _formatPixelRequestForTesting(url); + if (pixel?.name?.startsWith('epbf') || pixel?.name?.startsWith('protection-toggled-off-breakage-report')) { + actualSentReports.push(pixel); + } + }); + spyOn(browser.tabs, 'query').and.callFake(() => { + const result = []; + + if (currentTabDetails) { + result.push(currentTabDetails); + } + + return Promise.resolve(result); + }); + const settings = new MockSettings(); + tds = mockTdsStorage(settings); + dashboardMessaging = new DashboardMessaging({ + settings, + tds, + tabManager, + }); + }); + + it('sends a broken site report pixel with provide category and description', async () => { + currentTabDetails = { + id: 123, + url: 'https://domain.example/path?param=value', + }; + tabManager.create(currentTabDetails); + await dashboardMessaging.submitBrokenSiteReport({ category: 'foo', description: 'ben' }); + expect(actualSentReports).toHaveSize(1); + expect(actualSentReports[0]).toEqual({ + name: 'epbf_chrome', + params: { + category: 'foo', + description: 'ben', + siteUrl: 'https://domain.example/path', + tds: tds.tds.etag, + remoteConfigEtag: tds.remoteConfig.etag, + remoteConfigVersion: tds.remoteConfig.config.version, + protectionsState: 'false', + ...defaultBrokenSitePixelParams, + }, + }); + }); + + it('does not send a pixel if there is no active tab', async () => { + await dashboardMessaging.submitBrokenSiteReport({ category: 'foo', description: 'ben' }); + expect(actualSentReports).toHaveSize(0); + }); + + it('can send toggle reports', async () => { + currentTabDetails = { + id: 123, + url: 'https://domain.example/path?param=value', + }; + tabManager.create(currentTabDetails); + await dashboardMessaging.submitBrokenSiteReport({}, 'protection-toggled-off-breakage-report', 'on_protections_off_dashboard_main'); + expect(actualSentReports).toHaveSize(1); + expect(actualSentReports[0]).toEqual({ + name: 'protection-toggled-off-breakage-report_chrome', + params: { + siteUrl: 'https://domain.example/path', + tds: tds.tds.etag, + remoteConfigEtag: tds.remoteConfig.etag, + remoteConfigVersion: tds.remoteConfig.config.version, + reportFlow: 'on_protections_off_dashboard_main', + // protectionsState is removed from these reports + ...defaultBrokenSitePixelParams, + }, + }); + }); + }); +}); diff --git a/unit-test/helpers/mocks.js b/unit-test/helpers/mocks.js new file mode 100644 index 000000000..d8b0ee0b1 --- /dev/null +++ b/unit-test/helpers/mocks.js @@ -0,0 +1,51 @@ +import RemoteConfig from '../../shared/js/background/components/remote-config'; +import TDSStorage from '../../shared/js/background/components/tds'; + +export class MockSettings { + constructor() { + this.mockSettingData = new Map(); + this.ready = () => Promise.resolve(); + } + + getSetting(key) { + return structuredClone(this.mockSettingData.get(key)); + } + updateSetting(key, value) { + this.mockSettingData.set(key, value); + } + removeSetting(name) { + this.mockSettingData.delete(name); + } + clearSettings() { + this.mockSettingData.clear(); + } +} + +export function mockTdsStorage(settings) { + // delay settings ready until we have fetch mocking in place + let settingsResolve = null; + const settingsReadyPromise = new Promise((resolve) => { + settingsResolve = resolve; + }); + settings.ready = () => settingsReadyPromise + const etags = require('../../shared/data/etags.json'); + const remoteConfig = new RemoteConfig({ settings }); + remoteConfig._loadFromURL = () => + Promise.resolve({ + contents: require('./../data/extension-config.json'), + etag: etags['config-etag'], + }); + const tds = new TDSStorage({ settings, remoteConfig }); + tds.tds._loadFromURL = () => + Promise.resolve({ + contents: require('./../data/tds.json'), + etag: etags['current-mv3-tds-etag'], + }); + tds.surrogates._loadFromURL = () => + Promise.resolve({ + contents: require('./../data/surrogates.js').surrogates, + etag: 'surrogates' + }); + settingsResolve(); + return tds; +} From d8cb0360681a32fddc71f0e4540c43be6da3d5c5 Mon Sep 17 00:00:00 2001 From: Sam Macbeth Date: Fri, 14 Feb 2025 12:10:51 +0100 Subject: [PATCH 06/11] Re-create ToggleReports for each test to prevent context leakage --- unit-test/background/toggle-reports.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/unit-test/background/toggle-reports.js b/unit-test/background/toggle-reports.js index 0934e4472..35b2b4240 100644 --- a/unit-test/background/toggle-reports.js +++ b/unit-test/background/toggle-reports.js @@ -8,11 +8,8 @@ describe('ToggleReports', () => { let currentTabDetails = null; let currentTimestamp = 1; let settingsStorage = null; - const toggleReports = new ToggleReports({ - dashboardMessaging: { - submitBrokenSiteReport: () => {}, - }, - }); + /** @type {ToggleReports} */ + let toggleReports = null; /** @type {jasmine.Spy} */ let submittedReports; let toggleReportsConfig = null; @@ -63,14 +60,19 @@ describe('ToggleReports', () => { callback(); }); spyOn(Date, 'now').and.callFake(() => currentTimestamp); - - submittedReports = spyOn(toggleReports.dashboardMessaging, 'submitBrokenSiteReport'); }); beforeEach(() => { currentTabDetails = null; currentTimestamp = 1; settingsStorage.clear(); + + toggleReports = new ToggleReports({ + dashboardMessaging: { + submitBrokenSiteReport: () => {}, + }, + }); + submittedReports = spyOn(toggleReports.dashboardMessaging, 'submitBrokenSiteReport'); }); it('toggleReportStarted()', async () => { From 22ecc30ead51b4c50c303faa17be19f6a19e2303 Mon Sep 17 00:00:00 2001 From: Sam Macbeth Date: Fri, 14 Feb 2025 12:21:10 +0100 Subject: [PATCH 07/11] Workaround lastSentDay still using global settings --- unit-test/background/dashboard-messaging.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/unit-test/background/dashboard-messaging.js b/unit-test/background/dashboard-messaging.js index e7d968e8a..98572d960 100644 --- a/unit-test/background/dashboard-messaging.js +++ b/unit-test/background/dashboard-messaging.js @@ -98,15 +98,19 @@ describe('DashboardMessaging component', () => { it('can send toggle reports', async () => { currentTabDetails = { id: 123, - url: 'https://domain.example/path?param=value', + url: 'https://domain2.example/path?param=value', }; tabManager.create(currentTabDetails); - await dashboardMessaging.submitBrokenSiteReport({}, 'protection-toggled-off-breakage-report', 'on_protections_off_dashboard_main'); + await dashboardMessaging.submitBrokenSiteReport( + {}, + 'protection-toggled-off-breakage-report', + 'on_protections_off_dashboard_main', + ); expect(actualSentReports).toHaveSize(1); expect(actualSentReports[0]).toEqual({ name: 'protection-toggled-off-breakage-report_chrome', params: { - siteUrl: 'https://domain.example/path', + siteUrl: 'https://domain2.example/path', tds: tds.tds.etag, remoteConfigEtag: tds.remoteConfig.etag, remoteConfigVersion: tds.remoteConfig.config.version, From 511ba7c775b8127a5cc81bda357633e81eadf579 Mon Sep 17 00:00:00 2001 From: Sam Macbeth Date: Fri, 14 Feb 2025 18:31:27 +0100 Subject: [PATCH 08/11] Document where all the dashboard message handlers are. --- .../components/dashboard-messaging.js | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/shared/js/background/components/dashboard-messaging.js b/shared/js/background/components/dashboard-messaging.js index 7e2f817da..e1042879d 100644 --- a/shared/js/background/components/dashboard-messaging.js +++ b/shared/js/background/components/dashboard-messaging.js @@ -8,7 +8,31 @@ import { isFireButtonEnabled } from './fire-button'; /** * Message handlers for communication from the dashboard to the extension background. * - * Note, additional message handlers for toggle reports is in the separate ToggleReports component. + * Note, handlers are split over multiple components, and some are not yet encapsulated in a component. + * + * Implemented in this component: + * - getBreakageFormOptions + * - getPrivacyDashboardData + * - submitBrokenSiteReport + * + * FireButton component: + * - doBurn + * - getBurnOptions + * - setBurnDefaultOption + * + * EmailAutofill component: + * - refreshAlias + * + * ToggleReports component: + * - getToggleReportOptions + * - rejectToggleReport + * - sendToggleReport + * - seeWhatIsSent + * + * Static message handlers: + * - openOptions + * - search + * - setLists */ export default class DashboardMessaging { /** From 4d2a979105776d3aa67c7ea2533f17bb86e29825 Mon Sep 17 00:00:00 2001 From: Sam Macbeth Date: Fri, 14 Feb 2025 18:34:50 +0100 Subject: [PATCH 09/11] lint fix --- unit-test/helpers/mocks.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/unit-test/helpers/mocks.js b/unit-test/helpers/mocks.js index d8b0ee0b1..82dfcf1b9 100644 --- a/unit-test/helpers/mocks.js +++ b/unit-test/helpers/mocks.js @@ -27,7 +27,7 @@ export function mockTdsStorage(settings) { const settingsReadyPromise = new Promise((resolve) => { settingsResolve = resolve; }); - settings.ready = () => settingsReadyPromise + settings.ready = () => settingsReadyPromise; const etags = require('../../shared/data/etags.json'); const remoteConfig = new RemoteConfig({ settings }); remoteConfig._loadFromURL = () => @@ -44,7 +44,7 @@ export function mockTdsStorage(settings) { tds.surrogates._loadFromURL = () => Promise.resolve({ contents: require('./../data/surrogates.js').surrogates, - etag: 'surrogates' + etag: 'surrogates', }); settingsResolve(); return tds; From 848164139784a64ce9115420686b1ef9cc1703dc Mon Sep 17 00:00:00 2001 From: Sam Macbeth Date: Fri, 14 Feb 2025 18:44:23 +0100 Subject: [PATCH 10/11] Correct mock config version --- unit-test/background/dashboard-messaging.js | 4 ++-- unit-test/data/extension-config.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/unit-test/background/dashboard-messaging.js b/unit-test/background/dashboard-messaging.js index 98572d960..f2023f29a 100644 --- a/unit-test/background/dashboard-messaging.js +++ b/unit-test/background/dashboard-messaging.js @@ -83,7 +83,7 @@ describe('DashboardMessaging component', () => { siteUrl: 'https://domain.example/path', tds: tds.tds.etag, remoteConfigEtag: tds.remoteConfig.etag, - remoteConfigVersion: tds.remoteConfig.config.version, + remoteConfigVersion: `${tds.remoteConfig.config.version}`, protectionsState: 'false', ...defaultBrokenSitePixelParams, }, @@ -113,7 +113,7 @@ describe('DashboardMessaging component', () => { siteUrl: 'https://domain2.example/path', tds: tds.tds.etag, remoteConfigEtag: tds.remoteConfig.etag, - remoteConfigVersion: tds.remoteConfig.config.version, + remoteConfigVersion: `${tds.remoteConfig.config.version}`, reportFlow: 'on_protections_off_dashboard_main', // protectionsState is removed from these reports ...defaultBrokenSitePixelParams, diff --git a/unit-test/data/extension-config.json b/unit-test/data/extension-config.json index 0624fbf43..21bfefbfb 100644 --- a/unit-test/data/extension-config.json +++ b/unit-test/data/extension-config.json @@ -1,5 +1,5 @@ { - "version": "2021.6.7", + "version": 1738568676812, "readme": "https://github.com/duckduckgo/privacy-configuration", "features": { "adClickAttribution": { From a7ef3f7c1d161bb2b0cc289091a889fbd8d86eb1 Mon Sep 17 00:00:00 2001 From: Sam Macbeth Date: Mon, 17 Feb 2025 11:50:22 +0100 Subject: [PATCH 11/11] Update shared/js/background/components/dashboard-messaging.js Co-authored-by: Dave Vandyke --- shared/js/background/components/dashboard-messaging.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/shared/js/background/components/dashboard-messaging.js b/shared/js/background/components/dashboard-messaging.js index e1042879d..c57c602ec 100644 --- a/shared/js/background/components/dashboard-messaging.js +++ b/shared/js/background/components/dashboard-messaging.js @@ -33,6 +33,8 @@ import { isFireButtonEnabled } from './fire-button'; * - openOptions * - search * - setLists + * + * See https://duckduckgo.github.io/privacy-dashboard/modules/Browser_Extensions_integration.html */ export default class DashboardMessaging { /**