From 8dd16259287f58f9273002717ec4d27e97127719 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Wed, 12 Jun 2024 07:43:14 +0200 Subject: Merging upstream version 127.0. Signed-off-by: Daniel Baumann --- browser/modules/BrowserUsageTelemetry.sys.mjs | 35 ++ browser/modules/ExtensionsUI.sys.mjs | 12 +- .../modules/FirefoxBridgeExtensionUtils.sys.mjs | 121 +----- browser/modules/metrics.yaml | 46 ++ browser/modules/pings.yaml | 22 + browser/modules/test/browser/browser.toml | 4 + .../browser/browser_ProcessHangNotifications.js | 2 +- .../browser/browser_UnsubmittedCrashHandler.js | 6 +- .../browser/browser_UsageTelemetry_interaction.js | 206 ++++++++- .../test/unit/test_FirefoxBridgeExtensionUtils.js | 461 ++++++++++++--------- ...st_FirefoxBridgeExtensionUtilsNativeManifest.js | 115 +++++ 11 files changed, 713 insertions(+), 317 deletions(-) create mode 100644 browser/modules/pings.yaml (limited to 'browser/modules') diff --git a/browser/modules/BrowserUsageTelemetry.sys.mjs b/browser/modules/BrowserUsageTelemetry.sys.mjs index 955a3338ec..0635d17bed 100644 --- a/browser/modules/BrowserUsageTelemetry.sys.mjs +++ b/browser/modules/BrowserUsageTelemetry.sys.mjs @@ -160,6 +160,11 @@ const PLACES_OPEN_COMMANDS = [ "placesCmd_open:tab", ]; +// How long of a delay between events means the start of a new flow? +// Used by Browser UI Interaction event instrumentation. +// Default: 5min. +const FLOW_IDLE_TIME = 5 * 60 * 1000; + function telemetryId(widgetId, obscureAddons = true) { // Add-on IDs need to be obscured. function addonId(id) { @@ -872,6 +877,7 @@ export let BrowserUsageTelemetry = { let source = this._getWidgetContainer(node); if (item && source) { + this.recordInteractionEvent(item, source); let scalar = `browser.ui.interaction.${source.replace(/-/g, "_")}`; Services.telemetry.keyedScalarAdd(scalar, telemetryId(item), 1); if (SET_USAGECOUNT_PREF_BUTTONS.includes(item)) { @@ -889,6 +895,7 @@ export let BrowserUsageTelemetry = { node.closest("menupopup")?.triggerNode ); if (triggerContainer) { + this.recordInteractionEvent(item, contextMenu); let scalar = `browser.ui.interaction.${contextMenu.replace(/-/g, "_")}`; Services.telemetry.keyedScalarAdd( scalar, @@ -899,6 +906,34 @@ export let BrowserUsageTelemetry = { } }, + _flowId: null, + _flowIdTS: 0, + + recordInteractionEvent(widgetId, source) { + // A note on clocks. Cu.now() is monotonic, but its behaviour across + // computer sleeps is different per platform. + // We're okay with this for flows because we're looking at idle times + // on the order of minutes and within the same machine, so the weirdest + // thing we may expect is a flow that accidentally continues across a + // sleep. Until we have evidence that this is common, we're in the clear. + if (!this._flowId || this._flowIdTS + FLOW_IDLE_TIME < Cu.now()) { + // We submit the ping full o' events on every new flow, + // including at startup. + GleanPings.prototypeNoCodeEvents.submit(); + // We use a GUID here because we need to identify events in a flow + // out of all events from all flows across all clients. + this._flowId = Services.uuid.generateUUID(); + } + this._flowIdTS = Cu.now(); + + const extra = { + source, + widgetId: telemetryId(widgetId), + flowId: this._flowId, + }; + Glean.browserUsage.interaction.record(extra); + }, + /** * Listens for UI interactions in the window. */ diff --git a/browser/modules/ExtensionsUI.sys.mjs b/browser/modules/ExtensionsUI.sys.mjs index 6b2781670a..f6cbcfcd88 100644 --- a/browser/modules/ExtensionsUI.sys.mjs +++ b/browser/modules/ExtensionsUI.sys.mjs @@ -119,12 +119,12 @@ export var ExtensionsUI = { showAddonsManager(tabbrowser, strings, icon) { let global = tabbrowser.selectedBrowser.ownerGlobal; - return global - .BrowserOpenAddonsMgr("addons://list/extension") - .then(aomWin => { + return global.BrowserAddonUI.openAddonsMgr("addons://list/extension").then( + aomWin => { let aomBrowser = aomWin.docShell.chromeEventHandler; return this.showPermissionsPrompt(aomBrowser, strings, icon); - }); + } + ); }, showSideloaded(tabbrowser, addon) { @@ -134,7 +134,7 @@ export var ExtensionsUI = { let strings = this._buildStrings({ addon, - permissions: addon.userPermissions, + permissions: addon.installPermissions, type: "sideload", }); @@ -339,8 +339,6 @@ export var ExtensionsUI = { async showPermissionsPrompt(target, strings, icon) { let { browser, window } = getTabBrowser(target); - await window.ensureCustomElements("moz-support-link"); - // Wait for any pending prompts to complete before showing the next one. let pending; while ((pending = this.pendingNotifications.get(browser))) { diff --git a/browser/modules/FirefoxBridgeExtensionUtils.sys.mjs b/browser/modules/FirefoxBridgeExtensionUtils.sys.mjs index 7b0094205d..e1222db6e0 100644 --- a/browser/modules/FirefoxBridgeExtensionUtils.sys.mjs +++ b/browser/modules/FirefoxBridgeExtensionUtils.sys.mjs @@ -56,6 +56,10 @@ export const FirefoxBridgeExtensionUtils = { * In Firefox 122, we enabled the firefox and firefox-private protocols. * We switched over to using firefox-bridge and firefox-private-bridge, * + * In Firefox 126, we deleted the above firefox-bridge and + * firefox-private-bridge protocols in favor of using native + * messaging so we are only keeping the deletion code. + * * but we want to clean up the use of the other protocols. * * deleteBridgeProtocolRegistryEntryHelper handles everything outside of the logic needed for @@ -66,7 +70,15 @@ export const FirefoxBridgeExtensionUtils = { * them with. If the entries are changed in any way, it is assumed that the user * mucked with them manually and knows what they are doing. */ + + PUBLIC_PROTOCOL: "firefox-bridge", + PRIVATE_PROTOCOL: "firefox-private-bridge", + OLD_PUBLIC_PROTOCOL: "firefox", + OLD_PRIVATE_PROTOCOL: "firefox-private", + maybeDeleteBridgeProtocolRegistryEntries( + publicProtocol = this.PUBLIC_PROTOCOL, + privateProtocol = this.PRIVATE_PROTOCOL, deleteBridgeProtocolRegistryEntryHelper = new DeleteBridgeProtocolRegistryEntryHelperImplementation() ) { try { @@ -110,9 +122,9 @@ export const FirefoxBridgeExtensionUtils = { } }; - maybeDeleteRegistryKey("firefox", `\"${path}\" -osint -url \"%1\"`); + maybeDeleteRegistryKey(publicProtocol, `\"${path}\" -osint -url \"%1\"`); maybeDeleteRegistryKey( - "firefox-private", + privateProtocol, `\"${path}\" -osint -private-window \"%1\"` ); } catch (err) { @@ -122,111 +134,6 @@ export const FirefoxBridgeExtensionUtils = { } }, - /** - * Registers the firefox-bridge and firefox-private-bridge protocols - * on the Windows platform. - */ - maybeRegisterFirefoxBridgeProtocols() { - const FIREFOX_BRIDGE_HANDLER_NAME = "firefox-bridge"; - const FIREFOX_PRIVATE_BRIDGE_HANDLER_NAME = "firefox-private-bridge"; - const path = Services.dirsvc.get("XREExeF", Ci.nsIFile).path; - let wrk = Cc["@mozilla.org/windows-registry-key;1"].createInstance( - Ci.nsIWindowsRegKey - ); - try { - wrk.open(wrk.ROOT_KEY_CLASSES_ROOT, "", wrk.ACCESS_READ); - let FxSet = wrk.hasChild(FIREFOX_BRIDGE_HANDLER_NAME); - let FxPrivateSet = wrk.hasChild(FIREFOX_PRIVATE_BRIDGE_HANDLER_NAME); - wrk.close(); - if (FxSet && FxPrivateSet) { - return; - } - wrk.open(wrk.ROOT_KEY_CURRENT_USER, "Software\\Classes", wrk.ACCESS_ALL); - const maybeUpdateRegistry = (isSetAlready, handler, protocolName) => { - if (isSetAlready) { - return; - } - let FxKey = wrk.createChild(handler, wrk.ACCESS_ALL); - try { - // Write URL protocol key - FxKey.writeStringValue("", protocolName); - FxKey.writeStringValue("URL Protocol", ""); - FxKey.close(); - // Write defaultIcon key - FxKey.create( - FxKey.ROOT_KEY_CURRENT_USER, - "Software\\Classes\\" + handler + "\\DefaultIcon", - FxKey.ACCESS_ALL - ); - FxKey.open( - FxKey.ROOT_KEY_CURRENT_USER, - "Software\\Classes\\" + handler + "\\DefaultIcon", - FxKey.ACCESS_ALL - ); - FxKey.writeStringValue("", `\"${path}\",1`); - FxKey.close(); - // Write shell\\open\\command key - FxKey.create( - FxKey.ROOT_KEY_CURRENT_USER, - "Software\\Classes\\" + handler + "\\shell", - FxKey.ACCESS_ALL - ); - FxKey.create( - FxKey.ROOT_KEY_CURRENT_USER, - "Software\\Classes\\" + handler + "\\shell\\open", - FxKey.ACCESS_ALL - ); - FxKey.create( - FxKey.ROOT_KEY_CURRENT_USER, - "Software\\Classes\\" + handler + "\\shell\\open\\command", - FxKey.ACCESS_ALL - ); - FxKey.open( - FxKey.ROOT_KEY_CURRENT_USER, - "Software\\Classes\\" + handler + "\\shell\\open\\command", - FxKey.ACCESS_ALL - ); - if (handler == FIREFOX_PRIVATE_BRIDGE_HANDLER_NAME) { - FxKey.writeStringValue( - "", - `\"${path}\" -osint -private-window \"%1\"` - ); - } else { - FxKey.writeStringValue("", `\"${path}\" -osint -url \"%1\"`); - } - } catch (ex) { - console.error(ex); - } finally { - FxKey.close(); - } - }; - - try { - maybeUpdateRegistry( - FxSet, - FIREFOX_BRIDGE_HANDLER_NAME, - "URL:Firefox Bridge Protocol" - ); - } catch (ex) { - console.error(ex); - } - - try { - maybeUpdateRegistry( - FxPrivateSet, - FIREFOX_PRIVATE_BRIDGE_HANDLER_NAME, - "URL:Firefox Private Bridge Protocol" - ); - } catch (ex) { - console.error(ex); - } - } catch (ex) { - console.error(ex); - } finally { - wrk.close(); - } - }, - getNativeMessagingHostId() { let nativeMessagingHostId = "org.mozilla.firefox_bridge_nmh"; if (AppConstants.NIGHTLY_BUILD) { diff --git a/browser/modules/metrics.yaml b/browser/modules/metrics.yaml index ac4e0c6ef9..a4fdba875d 100644 --- a/browser/modules/metrics.yaml +++ b/browser/modules/metrics.yaml @@ -118,3 +118,49 @@ performance.interaction: - mconley@mozilla.com - perf-telemetry-alerts@mozilla.com expires: never + +browser.usage: + interaction: + type: event + description: > + The user interacted with something in the Firefox Desktop frontend. + Could be via mouse or keyboard, could be a command or a UI element. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1889111 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1889111 + expires: 132 + data_sensitivity: [interaction] + notification_emails: + - chutten@mozilla.com + extra_keys: + flow_id: + type: string + description: > + An UUIDv4 used to group interaction events together under the + assumption that they're part of the same user activity. + See BrowserUsageTelemetry's FLOW_IDLE_TIME for details. + source: + type: string + description: > + The source of the interaction. Usually a UI section + (like `bookmarks_bar` or `content_context`), but can also be an input + method (like `keyboard`). + The full list of supported `source`s can be found in + `BrowserUsageTelemetry`'s `BROWSER_UI_CONTAINER_IDS. Plus `keyboard` + and panes from `about:preferences` listed in `PREFERENCES_PANES`. + See `_getWidgetContainer` for details. + widget_id: + type: string + description: > + The item interacted with. + Usually the `id` of the DOM Node that the user used, + sometimes the `id` of the parent or ancestor Node instead. + This node is then conjugated by obscuring any addon id in it + (turning it to the string `addonX` where `X` is a number stable + within a browsing session) and then replacing any underscore with a + hyphen. + See `BrowserUsageTelemetry#_getWidgetID` and `telemetryId`. + e.g. "Browser:Reload", "key-newNavigatorTab", "PanelUI-Bookmarks". + send_in_pings: + - prototype-no-code-events diff --git a/browser/modules/pings.yaml b/browser/modules/pings.yaml new file mode 100644 index 0000000000..0bc4d2227f --- /dev/null +++ b/browser/modules/pings.yaml @@ -0,0 +1,22 @@ +# 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/. + +--- +$schema: moz://mozilla.org/schemas/glean/pings/2-0-0 + +prototype-no-code-events: + description: | + **Prototype-only ping not for general use!** + Transport for no-code Firefox Desktop frontend instrumentation, + should mostly contain no-code events in browser.ui.* categories. + Submitted whenever the next flow of events begins (including startup). + include_client_id: true + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1889111 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1889111 + notification_emails: + - chutten@mozilla.com + - tlong@mozilla.com + enabled: false # To be enabled by Server Knobs for selected populations. diff --git a/browser/modules/test/browser/browser.toml b/browser/modules/test/browser/browser.toml index 21b3cdf18c..82611ed4b2 100644 --- a/browser/modules/test/browser/browser.toml +++ b/browser/modules/test/browser/browser.toml @@ -36,6 +36,10 @@ support-files = [ "../../../base/content/test/tabs/file_mediaPlayback.html", "../../../base/content/test/general/audio.ogg", ] +skip-if = [ + "os == 'linux' && os_version == '18.04' && asan", # Bug 1781868 + "os == 'linux' && os_version == '18.04' && tsan", # Bug 1781868 +] ["browser_Telemetry_numberOfSiteOrigins.js"] support-files = ["contain_iframe.html"] diff --git a/browser/modules/test/browser/browser_ProcessHangNotifications.js b/browser/modules/test/browser/browser_ProcessHangNotifications.js index 9150c36d4c..963dc2d4b4 100644 --- a/browser/modules/test/browser/browser_ProcessHangNotifications.js +++ b/browser/modules/test/browser/browser_ProcessHangNotifications.js @@ -184,7 +184,7 @@ add_task(async function waitForScriptTest() { }); // Click the "Close" button this time, we shouldn't get a callback at all. - notification.currentNotification.closeButtonEl.click(); + notification.currentNotification.closeButton.click(); // send another hang pulse, we should not get a notification here Services.obs.notifyObservers(hangReport, "process-hang-report"); diff --git a/browser/modules/test/browser/browser_UnsubmittedCrashHandler.js b/browser/modules/test/browser/browser_UnsubmittedCrashHandler.js index 6300bd17ba..d105e8374e 100644 --- a/browser/modules/test/browser/browser_UnsubmittedCrashHandler.js +++ b/browser/modules/test/browser/browser_UnsubmittedCrashHandler.js @@ -292,7 +292,7 @@ add_task(async function test_other_ignored() { Assert.ok(notification, "There should be a notification"); // Dismiss notification, creating the .dmp.ignore file - notification.closeButtonEl.click(); + notification.closeButton.click(); gNotificationBox.removeNotification(notification, true); await waitForIgnoredReports(toIgnore); @@ -525,7 +525,7 @@ add_task(async function test_can_ignore() { Assert.ok(notification, "There should be a notification"); // Dismiss the notification by clicking on the "X" button. - notification.closeButtonEl.click(); + notification.closeButton.click(); // We'll not wait for the notification to finish its transition - // we'll just remove it right away. gNotificationBox.removeNotification(notification, true); @@ -599,7 +599,7 @@ add_task(async function test_shutdown_while_not_showing() { Assert.ok(notification, "There should be a notification"); // Dismiss the notification by clicking on the "X" button. - notification.closeButtonEl.click(); + notification.closeButton.click(); // We'll not wait for the notification to finish its transition - // we'll just remove it right away. gNotificationBox.removeNotification(notification, true); diff --git a/browser/modules/test/browser/browser_UsageTelemetry_interaction.js b/browser/modules/test/browser/browser_UsageTelemetry_interaction.js index 5fa436a349..56a7f530ad 100644 --- a/browser/modules/test/browser/browser_UsageTelemetry_interaction.js +++ b/browser/modules/test/browser/browser_UsageTelemetry_interaction.js @@ -33,6 +33,8 @@ const AREAS = [ // keys in the scalars. Also runs keyed scalar checks against non-area types // passed in through expectedOther. function assertInteractionScalars(expectedAreas, expectedOther = {}) { + // Every time this checks Scalars, it clears them. So clear FOG too. + Services.fog.testResetFOG(); let processScalars = Services.telemetry.getSnapshotForKeyedScalars("main", true)?.parent ?? {}; @@ -83,6 +85,7 @@ add_task(async function toolbarButtons() { }); Services.telemetry.getSnapshotForKeyedScalars("main", true); + Services.fog.testResetFOG(); let newTab = await BrowserTestUtils.openNewForegroundTab(gBrowser); let tabClose = BrowserTestUtils.waitForTabClosing(newTab); @@ -164,6 +167,22 @@ add_task(async function toolbarButtons() { click(customButton); + let events = Glean.browserUsage.interaction + .testGetValue() + .map(e => [e.extra.source, e.extra.widget_id]); + Assert.deepEqual( + [ + ["nav-bar", "stop-reload-button"], + ["nav-bar", "back-button"], + ["nav-bar", "back-button"], + ["all-tabs-panel-entrypoint", "alltabs-button"], + ["tabs-bar", "alltabs-button"], + ["tabs-bar", "tab-close-button"], + ["bookmarks-bar", "bookmark-item"], + ["nav-bar", "12foo"], + ], + events + ); assertInteractionScalars( { nav_bar: { @@ -192,6 +211,7 @@ add_task(async function toolbarButtons() { add_task(async function contextMenu() { await BrowserTestUtils.withNewTab("https://example.com", async browser => { Services.telemetry.getSnapshotForKeyedScalars("main", true); + Services.fog.testResetFOG(); let tab = gBrowser.getTabForBrowser(browser); let context = elem("tabContextMenu"); @@ -207,6 +227,16 @@ add_task(async function contextMenu() { context.activateItem(document.getElementById("context_toggleMuteTab")); await hidden; + let events = Glean.browserUsage.interaction + .testGetValue() + .map(e => [e.extra.source, e.extra.widget_id]); + Assert.deepEqual( + [ + ["tabs-context", "context-toggleMuteTab"], + ["tabs-context-entrypoint", "context-toggleMuteTab"], + ], + events + ); assertInteractionScalars({ tabs_context: { "context-toggleMuteTab": 1, @@ -233,6 +263,16 @@ add_task(async function contextMenu() { ); await hidden; + events = Glean.browserUsage.interaction + .testGetValue() + .map(e => [e.extra.source, e.extra.widget_id]); + Assert.deepEqual( + [ + ["tabs-context", "toolbar-context-selectAllTabs"], + ["tabs-context-entrypoint", "toolbar-context-selectAllTabs"], + ], + events + ); assertInteractionScalars({ tabs_context: { "toolbar-context-selectAllTabs": 1, @@ -318,6 +358,7 @@ add_task(async function contextMenu_entrypoints() { add_task(async function appMenu() { await BrowserTestUtils.withNewTab("https://example.com", async () => { Services.telemetry.getSnapshotForKeyedScalars("main", true); + Services.fog.testResetFOG(); let shown = BrowserTestUtils.waitForEvent( elem("appMenu-popup"), @@ -339,9 +380,21 @@ add_task(async function appMenu() { nav_bar: { "PanelUI-menu-button": 1, }, - app_menu: {}, + app_menu: { + [findButtonID]: 1, + }, }; - expectedScalars.app_menu[findButtonID] = 1; + + let events = Glean.browserUsage.interaction + .testGetValue() + .map(e => [e.extra.source, e.extra.widget_id]); + Assert.deepEqual( + [ + ["nav-bar", "PanelUI-menu-button"], + ["app-menu", findButtonID], + ], + events + ); assertInteractionScalars(expectedScalars); }); @@ -350,6 +403,7 @@ add_task(async function appMenu() { add_task(async function devtools() { await BrowserTestUtils.withNewTab("https://example.com", async () => { Services.telemetry.getSnapshotForKeyedScalars("main", true); + Services.fog.testResetFOG(); let shown = BrowserTestUtils.waitForEvent( elem("appMenu-popup"), @@ -381,6 +435,17 @@ add_task(async function devtools() { BrowserTestUtils.removeTab(tab); // Note that item ID's have '_' converted to '-'. + let events = Glean.browserUsage.interaction + .testGetValue() + .map(e => [e.extra.source, e.extra.widget_id]); + Assert.deepEqual( + [ + ["nav-bar", "PanelUI-menu-button"], + ["app-menu", "appMenu-more-button2"], + ["app-menu", "key-viewSource"], + ], + events + ); assertInteractionScalars({ nav_bar: { "PanelUI-menu-button": 1, @@ -398,6 +463,7 @@ add_task(async function webextension() { await BrowserTestUtils.withNewTab("https://example.com", async browser => { Services.telemetry.getSnapshotForKeyedScalars("main", true); + Services.fog.testResetFOG(); function background() { browser.commands.onCommand.addListener(() => { @@ -470,6 +536,11 @@ add_task(async function webextension() { // As the first add-on interacted with this should show up as `addon0`. click("random_addon_example_com-browser-action"); + let events = Glean.browserUsage.interaction.testGetValue(); + Assert.deepEqual( + [["nav-bar", "addon0"]], + events.map(e => [e.extra.source, e.extra.widget_id]) + ); assertInteractionScalars({ nav_bar: { addon0: 1, @@ -482,6 +553,11 @@ add_task(async function webextension() { ); click("pageAction-urlbar-random_addon_example_com"); + events = Glean.browserUsage.interaction.testGetValue(); + Assert.deepEqual( + [["pageaction-urlbar", "addon0"]], + events.map(e => [e.extra.source, e.extra.widget_id]) + ); assertInteractionScalars({ pageaction_urlbar: { addon0: 1, @@ -490,6 +566,11 @@ add_task(async function webextension() { EventUtils.synthesizeKey("j", { altKey: true, shiftKey: true }); await extension.awaitMessage("oncommand"); + events = Glean.browserUsage.interaction.testGetValue(); + Assert.deepEqual( + [["keyboard", "addon0"]], + events.map(e => [e.extra.source, e.extra.widget_id]) + ); assertInteractionScalars({ keyboard: { addon0: 1, @@ -498,6 +579,11 @@ add_task(async function webextension() { EventUtils.synthesizeKey("q", { altKey: true, shiftKey: true }); await extension.awaitMessage("sidebar-opened"); + events = Glean.browserUsage.interaction.testGetValue(); + Assert.deepEqual( + [["keyboard", "addon0"]], + events.map(e => [e.extra.source, e.extra.widget_id]) + ); assertInteractionScalars({ keyboard: { addon0: 1, @@ -537,6 +623,11 @@ add_task(async function webextension() { // A second extension should be `addon1`. click("random_addon2_example_com-browser-action"); + events = Glean.browserUsage.interaction.testGetValue(); + Assert.deepEqual( + [["nav-bar", "addon1"]], + events.map(e => [e.extra.source, e.extra.widget_id]) + ); assertInteractionScalars({ nav_bar: { addon1: 1, @@ -549,6 +640,11 @@ add_task(async function webextension() { ); click("pageAction-urlbar-random_addon2_example_com"); + events = Glean.browserUsage.interaction.testGetValue(); + Assert.deepEqual( + [["pageaction-urlbar", "addon1"]], + events.map(e => [e.extra.source, e.extra.widget_id]) + ); assertInteractionScalars({ pageaction_urlbar: { addon1: 1, @@ -557,6 +653,11 @@ add_task(async function webextension() { EventUtils.synthesizeKey("9", { altKey: true, shiftKey: true }); await extension2.awaitMessage("oncommand"); + events = Glean.browserUsage.interaction.testGetValue(); + Assert.deepEqual( + [["keyboard", "addon1"]], + events.map(e => [e.extra.source, e.extra.widget_id]) + ); assertInteractionScalars({ keyboard: { addon1: 1, @@ -565,6 +666,11 @@ add_task(async function webextension() { // The first should have retained its ID. click("random_addon_example_com-browser-action"); + events = Glean.browserUsage.interaction.testGetValue(); + Assert.deepEqual( + [["nav-bar", "addon0"]], + events.map(e => [e.extra.source, e.extra.widget_id]) + ); assertInteractionScalars({ nav_bar: { addon0: 1, @@ -573,6 +679,11 @@ add_task(async function webextension() { EventUtils.synthesizeKey("j", { altKey: true, shiftKey: true }); await extension.awaitMessage("oncommand"); + events = Glean.browserUsage.interaction.testGetValue(); + Assert.deepEqual( + [["keyboard", "addon0"]], + events.map(e => [e.extra.source, e.extra.widget_id]) + ); assertInteractionScalars({ keyboard: { addon0: 1, @@ -580,6 +691,11 @@ add_task(async function webextension() { }); click("pageAction-urlbar-random_addon_example_com"); + events = Glean.browserUsage.interaction.testGetValue(); + Assert.deepEqual( + [["pageaction-urlbar", "addon0"]], + events.map(e => [e.extra.source, e.extra.widget_id]) + ); assertInteractionScalars({ pageaction_urlbar: { addon0: 1, @@ -590,11 +706,19 @@ add_task(async function webextension() { // Clear the last opened ID so if this test runs again the sidebar won't // automatically open when the extension is installed. - window.SidebarUI.lastOpenedId = null; + window.SidebarController.lastOpenedId = null; // The second should retain its ID. click("random_addon2_example_com-browser-action"); click("random_addon2_example_com-browser-action"); + events = Glean.browserUsage.interaction.testGetValue(); + Assert.deepEqual( + [ + ["nav-bar", "addon1"], + ["nav-bar", "addon1"], + ], + events.map(e => [e.extra.source, e.extra.widget_id]) + ); assertInteractionScalars({ nav_bar: { addon1: 2, @@ -602,6 +726,11 @@ add_task(async function webextension() { }); click("pageAction-urlbar-random_addon2_example_com"); + events = Glean.browserUsage.interaction.testGetValue(); + Assert.deepEqual( + [["pageaction-urlbar", "addon1"]], + events.map(e => [e.extra.source, e.extra.widget_id]) + ); assertInteractionScalars({ pageaction_urlbar: { addon1: 1, @@ -610,6 +739,11 @@ add_task(async function webextension() { EventUtils.synthesizeKey("9", { altKey: true, shiftKey: true }); await extension2.awaitMessage("oncommand"); + events = Glean.browserUsage.interaction.testGetValue(); + Assert.deepEqual( + [["keyboard", "addon1"]], + events.map(e => [e.extra.source, e.extra.widget_id]) + ); assertInteractionScalars({ keyboard: { addon1: 1, @@ -643,6 +777,11 @@ add_task(async function webextension() { await shown; click("random_addon3_example_com-browser-action"); + events = Glean.browserUsage.interaction.testGetValue(); + Assert.deepEqual( + [["unified-extensions-area", "addon2"]], + events.map(e => [e.extra.source, e.extra.widget_id]) + ); assertInteractionScalars({ unified_extensions_area: { addon2: 1, @@ -669,6 +808,7 @@ add_task(async function mainMenu() { await BrowserTestUtils.withNewTab("https://example.com", async () => { Services.telemetry.getSnapshotForKeyedScalars("main", true); + Services.fog.testResetFOG(); CustomizableUI.setToolbarVisibility("toolbar-menubar", true); @@ -686,6 +826,11 @@ add_task(async function mainMenu() { click("menu_selectAll"); await hidden; + let events = Glean.browserUsage.interaction.testGetValue(); + Assert.deepEqual( + [["menu-bar", "menu-selectAll"]], + events.map(e => [e.extra.source, e.extra.widget_id]) + ); assertInteractionScalars({ menu_bar: { // Note that the _ is replaced with - for telemetry identifiers. @@ -706,6 +851,7 @@ add_task(async function preferences() { await finalPrefPaneLoaded; Services.telemetry.getSnapshotForKeyedScalars("main", true); + Services.fog.testResetFOG(); await BrowserTestUtils.synthesizeMouseAtCenter( "#browserRestoreSession", @@ -742,6 +888,16 @@ add_task(async function preferences() { await onLearnMoreOpened; gBrowser.removeCurrentTab(); + let events = Glean.browserUsage.interaction + .testGetValue() + .map(e => [e.extra.source, e.extra.widget_id]); + Assert.deepEqual( + [ + ["preferences_paneGeneral", "browserRestoreSession"], + ["preferences_panePrivacy", "contentBlockingLearnMore"], + ], + events + ); assertInteractionScalars({ preferences_paneGeneral: { browserRestoreSession: 1, @@ -806,6 +962,17 @@ async function history_appMenu(useContextClick) { app_menu: { "history-item": 1, "appMenu-history-button": 1 }, }; + let events = Glean.browserUsage.interaction + .testGetValue() + .map(e => [e.extra.source, e.extra.widget_id]); + Assert.deepEqual( + [ + ["nav-bar", "PanelUI-menu-button"], + ["app-menu", "appMenu-history-button"], + ["app-menu", "history-item"], + ], + events + ); assertInteractionScalars(expectedScalars); }); } @@ -852,6 +1019,17 @@ async function bookmarks_appMenu(useContextClick) { app_menu: { "bookmark-item": 1, "appMenu-bookmarks-button": 1 }, }; + let events = Glean.browserUsage.interaction + .testGetValue() + .map(e => [e.extra.source, e.extra.widget_id]); + Assert.deepEqual( + [ + ["nav-bar", "PanelUI-menu-button"], + ["app-menu", "appMenu-bookmarks-button"], + ["app-menu", "bookmark-item"], + ], + events + ); assertInteractionScalars(expectedScalars); }); } @@ -893,6 +1071,17 @@ async function bookmarks_library_navbar(useContextClick) { "appMenu-library-bookmarks-button": 1, }, }; + let events = Glean.browserUsage.interaction + .testGetValue() + .map(e => [e.extra.source, e.extra.widget_id]); + Assert.deepEqual( + [ + ["nav-bar", "library-button"], + ["nav-bar", "appMenu-library-bookmarks-button"], + ["nav-bar", "bookmark-item"], + ], + events + ); assertInteractionScalars(expectedScalars); }); @@ -940,6 +1129,17 @@ async function history_library_navbar(useContextClick) { "appMenu-library-history-button": 1, }, }; + let events = Glean.browserUsage.interaction + .testGetValue() + .map(e => [e.extra.source, e.extra.widget_id]); + Assert.deepEqual( + [ + ["nav-bar", "library-button"], + ["nav-bar", "appMenu-library-history-button"], + ["nav-bar", "history-item"], + ], + events + ); assertInteractionScalars(expectedScalars); }); diff --git a/browser/modules/test/unit/test_FirefoxBridgeExtensionUtils.js b/browser/modules/test/unit/test_FirefoxBridgeExtensionUtils.js index 77db0d8286..d4dcd4fad6 100644 --- a/browser/modules/test/unit/test_FirefoxBridgeExtensionUtils.js +++ b/browser/modules/test/unit/test_FirefoxBridgeExtensionUtils.js @@ -7,9 +7,10 @@ const { FirefoxBridgeExtensionUtils } = ChromeUtils.importESModule( "resource:///modules/FirefoxBridgeExtensionUtils.sys.mjs" ); -const FIREFOX_SHELL_OPEN_COMMAND_PATH = "firefox\\shell\\open\\command"; -const FIREFOX_PRIVATE_SHELL_OPEN_COMMAND_PATH = - "firefox-private\\shell\\open\\command"; +const OLD_FIREFOX_SHELL_OPEN_COMMAND_PATH = `${FirefoxBridgeExtensionUtils.OLD_PUBLIC_PROTOCOL}\\shell\\open\\command`; +const OLD_FIREFOX_PRIVATE_SHELL_OPEN_COMMAND_PATH = `${FirefoxBridgeExtensionUtils.OLD_PRIVATE_PROTOCOL}\\shell\\open\\command`; +const FIREFOX_SHELL_OPEN_COMMAND_PATH = `${FirefoxBridgeExtensionUtils.PUBLIC_PROTOCOL}\\shell\\open\\command`; +const FIREFOX_PRIVATE_SHELL_OPEN_COMMAND_PATH = `${FirefoxBridgeExtensionUtils.PRIVATE_PROTOCOL}\\shell\\open\\command`; class StubbedRegistryKey { #children; @@ -145,206 +146,274 @@ class StubbedDeleteBridgeProtocolRegistryEntryHelper { } add_task(async function test_DeleteWhenSameFirefoxInstall() { - const applicationPath = "testPath"; - - const firefoxEntries = new Map(); - firefoxEntries.set("", `\"${applicationPath}\" -osint -url \"%1\"`); - - const firefoxProtocolRegKey = new StubbedRegistryKey( - new Map(), - firefoxEntries - ); - - const firefoxPrivateEntries = new Map(); - firefoxPrivateEntries.set( - "", - `\"${applicationPath}\" -osint -private-window \"%1\"` - ); - const firefoxPrivateProtocolRegKey = new StubbedRegistryKey( - new Map(), - firefoxPrivateEntries - ); - - const children = new Map(); - children.set(FIREFOX_SHELL_OPEN_COMMAND_PATH, firefoxProtocolRegKey); - children.set( - FIREFOX_PRIVATE_SHELL_OPEN_COMMAND_PATH, - firefoxPrivateProtocolRegKey - ); - - const registryRootKey = new StubbedRegistryKey(children, new Map()); - - const stubbedDeleteBridgeProtocolRegistryHelper = - new StubbedDeleteBridgeProtocolRegistryEntryHelper({ - applicationPath, - registryRootKey, - }); - - FirefoxBridgeExtensionUtils.maybeDeleteBridgeProtocolRegistryEntries( - stubbedDeleteBridgeProtocolRegistryHelper - ); - - ok(registryRootKey.wasCloseCalled, "Root key closed"); - - ok(firefoxProtocolRegKey.wasOpenedForRead, "Firefox key opened"); - ok(firefoxProtocolRegKey.wasCloseCalled, "Firefox key closed"); - ok( - registryRootKey.isChildDeleted("firefox"), - "Firefox protocol registry entry deleted" - ); - - ok( - firefoxPrivateProtocolRegKey.wasOpenedForRead, - "Firefox private key opened" - ); - ok(firefoxPrivateProtocolRegKey.wasCloseCalled, "Firefox private key closed"); - ok( - registryRootKey.isChildDeleted("firefox-private"), - "Firefox private protocol registry entry deleted" - ); + for (let protocols of [ + [ + FirefoxBridgeExtensionUtils.OLD_PUBLIC_PROTOCOL, + FirefoxBridgeExtensionUtils.OLD_PRIVATE_PROTOCOL, + OLD_FIREFOX_SHELL_OPEN_COMMAND_PATH, + OLD_FIREFOX_PRIVATE_SHELL_OPEN_COMMAND_PATH, + ], + [ + FirefoxBridgeExtensionUtils.PUBLIC_PROTOCOL, + FirefoxBridgeExtensionUtils.PRIVATE_PROTOCOL, + FIREFOX_SHELL_OPEN_COMMAND_PATH, + FIREFOX_PRIVATE_SHELL_OPEN_COMMAND_PATH, + ], + ]) { + let [publicProtocol, privateProtocol, publicPath, privatePath] = protocols; + const applicationPath = "testPath"; + + const firefoxEntries = new Map(); + firefoxEntries.set("", `\"${applicationPath}\" -osint -url \"%1\"`); + + const firefoxProtocolRegKey = new StubbedRegistryKey( + new Map(), + firefoxEntries + ); + + const firefoxPrivateEntries = new Map(); + firefoxPrivateEntries.set( + "", + `\"${applicationPath}\" -osint -private-window \"%1\"` + ); + const firefoxPrivateProtocolRegKey = new StubbedRegistryKey( + new Map(), + firefoxPrivateEntries + ); + + const children = new Map(); + children.set(publicPath, firefoxProtocolRegKey); + children.set(privatePath, firefoxPrivateProtocolRegKey); + + const registryRootKey = new StubbedRegistryKey(children, new Map()); + + const stubbedDeleteBridgeProtocolRegistryHelper = + new StubbedDeleteBridgeProtocolRegistryEntryHelper({ + applicationPath, + registryRootKey, + }); + + FirefoxBridgeExtensionUtils.maybeDeleteBridgeProtocolRegistryEntries( + publicProtocol, + privateProtocol, + stubbedDeleteBridgeProtocolRegistryHelper + ); + + ok(registryRootKey.wasCloseCalled, "Root key closed"); + + ok(firefoxProtocolRegKey.wasOpenedForRead, "Firefox key opened"); + ok(firefoxProtocolRegKey.wasCloseCalled, "Firefox key closed"); + ok( + registryRootKey.isChildDeleted(publicProtocol), + "Firefox protocol registry entry deleted" + ); + + ok( + firefoxPrivateProtocolRegKey.wasOpenedForRead, + "Firefox private key opened" + ); + ok( + firefoxPrivateProtocolRegKey.wasCloseCalled, + "Firefox private key closed" + ); + ok( + registryRootKey.isChildDeleted(privateProtocol), + "Firefox private protocol registry entry deleted" + ); + } }); add_task(async function test_DeleteWhenDifferentFirefoxInstall() { - const applicationPath = "testPath"; - const badApplicationPath = "testPath2"; - - const firefoxEntries = new Map(); - firefoxEntries.set("", `\"${badApplicationPath}\" -osint -url \"%1\"`); - - const firefoxProtocolRegKey = new StubbedRegistryKey( - new Map(), - firefoxEntries - ); - - const firefoxPrivateEntries = new Map(); - firefoxPrivateEntries.set( - "", - `\"${badApplicationPath}\" -osint -private-window \"%1\"` - ); - const firefoxPrivateProtocolRegKey = new StubbedRegistryKey( - new Map(), - firefoxPrivateEntries - ); - - const children = new Map(); - children.set(FIREFOX_SHELL_OPEN_COMMAND_PATH, firefoxProtocolRegKey); - children.set( - FIREFOX_PRIVATE_SHELL_OPEN_COMMAND_PATH, - firefoxPrivateProtocolRegKey - ); - - const registryRootKey = new StubbedRegistryKey(children, new Map()); - - const stubbedDeleteBridgeProtocolRegistryHelper = - new StubbedDeleteBridgeProtocolRegistryEntryHelper({ - applicationPath, - registryRootKey, - }); - - FirefoxBridgeExtensionUtils.maybeDeleteBridgeProtocolRegistryEntries( - stubbedDeleteBridgeProtocolRegistryHelper - ); - - ok(registryRootKey.wasCloseCalled, "Root key closed"); - - ok(firefoxProtocolRegKey.wasOpenedForRead, "Firefox key opened"); - ok(firefoxProtocolRegKey.wasCloseCalled, "Firefox key closed"); - ok( - !registryRootKey.isChildDeleted("firefox"), - "Firefox protocol registry entry not deleted" - ); - - ok( - firefoxPrivateProtocolRegKey.wasOpenedForRead, - "Firefox private key opened" - ); - ok(firefoxPrivateProtocolRegKey.wasCloseCalled, "Firefox private key closed"); - ok( - !registryRootKey.isChildDeleted("firefox-private"), - "Firefox private protocol registry entry not deleted" - ); + for (let protocols of [ + [ + FirefoxBridgeExtensionUtils.OLD_PUBLIC_PROTOCOL, + FirefoxBridgeExtensionUtils.OLD_PRIVATE_PROTOCOL, + OLD_FIREFOX_SHELL_OPEN_COMMAND_PATH, + OLD_FIREFOX_PRIVATE_SHELL_OPEN_COMMAND_PATH, + ], + [ + FirefoxBridgeExtensionUtils.PUBLIC_PROTOCOL, + FirefoxBridgeExtensionUtils.PRIVATE_PROTOCOL, + FIREFOX_SHELL_OPEN_COMMAND_PATH, + FIREFOX_PRIVATE_SHELL_OPEN_COMMAND_PATH, + ], + ]) { + let [publicProtocol, privateProtocol, publicPath, privatePath] = protocols; + const applicationPath = "testPath"; + const badApplicationPath = "testPath2"; + + const firefoxEntries = new Map(); + firefoxEntries.set("", `\"${badApplicationPath}\" -osint -url \"%1\"`); + + const firefoxProtocolRegKey = new StubbedRegistryKey( + new Map(), + firefoxEntries + ); + + const firefoxPrivateEntries = new Map(); + firefoxPrivateEntries.set( + "", + `\"${badApplicationPath}\" -osint -private-window \"%1\"` + ); + const firefoxPrivateProtocolRegKey = new StubbedRegistryKey( + new Map(), + firefoxPrivateEntries + ); + + const children = new Map(); + children.set(publicPath, firefoxProtocolRegKey); + children.set(privatePath, firefoxPrivateProtocolRegKey); + + const registryRootKey = new StubbedRegistryKey(children, new Map()); + + const stubbedDeleteBridgeProtocolRegistryHelper = + new StubbedDeleteBridgeProtocolRegistryEntryHelper({ + applicationPath, + registryRootKey, + }); + + FirefoxBridgeExtensionUtils.maybeDeleteBridgeProtocolRegistryEntries( + publicProtocol, + privateProtocol, + stubbedDeleteBridgeProtocolRegistryHelper + ); + + ok(registryRootKey.wasCloseCalled, "Root key closed"); + + ok(firefoxProtocolRegKey.wasOpenedForRead, "Firefox key opened"); + ok(firefoxProtocolRegKey.wasCloseCalled, "Firefox key closed"); + ok( + !registryRootKey.isChildDeleted(publicProtocol), + "Firefox protocol registry entry not deleted" + ); + + ok( + firefoxPrivateProtocolRegKey.wasOpenedForRead, + "Firefox private key opened" + ); + ok( + firefoxPrivateProtocolRegKey.wasCloseCalled, + "Firefox private key closed" + ); + ok( + !registryRootKey.isChildDeleted(privateProtocol), + "Firefox private protocol registry entry not deleted" + ); + } }); add_task(async function test_DeleteWhenNoRegistryEntries() { - const applicationPath = "testPath"; - - const firefoxPrivateEntries = new Map(); - const firefoxPrivateProtocolRegKey = new StubbedRegistryKey( - new Map(), - firefoxPrivateEntries - ); - - const children = new Map(); - children.set( - FIREFOX_PRIVATE_SHELL_OPEN_COMMAND_PATH, - firefoxPrivateProtocolRegKey - ); - - const registryRootKey = new StubbedRegistryKey(children, new Map()); - - const stubbedDeleteBridgeProtocolRegistryHelper = - new StubbedDeleteBridgeProtocolRegistryEntryHelper({ - applicationPath, - registryRootKey, - }); - - FirefoxBridgeExtensionUtils.maybeDeleteBridgeProtocolRegistryEntries( - stubbedDeleteBridgeProtocolRegistryHelper - ); - - ok(registryRootKey.wasCloseCalled, "Root key closed"); - - ok( - firefoxPrivateProtocolRegKey.wasOpenedForRead, - "Firefox private key opened" - ); - ok(firefoxPrivateProtocolRegKey.wasCloseCalled, "Firefox private key closed"); - ok( - !registryRootKey.isChildDeleted("firefox"), - "Firefox protocol registry entry deleted when it shouldn't be" - ); - ok( - !registryRootKey.isChildDeleted("firefox-private"), - "Firefox private protocol registry deleted when it shouldn't be" - ); + for (let protocols of [ + [ + FirefoxBridgeExtensionUtils.OLD_PUBLIC_PROTOCOL, + FirefoxBridgeExtensionUtils.OLD_PRIVATE_PROTOCOL, + OLD_FIREFOX_PRIVATE_SHELL_OPEN_COMMAND_PATH, + ], + [ + FirefoxBridgeExtensionUtils.PUBLIC_PROTOCOL, + FirefoxBridgeExtensionUtils.PRIVATE_PROTOCOL, + FIREFOX_PRIVATE_SHELL_OPEN_COMMAND_PATH, + ], + ]) { + let [publicProtocol, privateProtocol, privatePath] = protocols; + const applicationPath = "testPath"; + + const firefoxPrivateEntries = new Map(); + const firefoxPrivateProtocolRegKey = new StubbedRegistryKey( + new Map(), + firefoxPrivateEntries + ); + + const children = new Map(); + children.set(privatePath, firefoxPrivateProtocolRegKey); + + const registryRootKey = new StubbedRegistryKey(children, new Map()); + + const stubbedDeleteBridgeProtocolRegistryHelper = + new StubbedDeleteBridgeProtocolRegistryEntryHelper({ + applicationPath, + registryRootKey, + }); + + FirefoxBridgeExtensionUtils.maybeDeleteBridgeProtocolRegistryEntries( + publicProtocol, + privateProtocol, + stubbedDeleteBridgeProtocolRegistryHelper + ); + + ok(registryRootKey.wasCloseCalled, "Root key closed"); + + ok( + firefoxPrivateProtocolRegKey.wasOpenedForRead, + "Firefox private key opened" + ); + ok( + firefoxPrivateProtocolRegKey.wasCloseCalled, + "Firefox private key closed" + ); + ok( + !registryRootKey.isChildDeleted(publicProtocol), + "Firefox protocol registry entry deleted when it shouldn't be" + ); + ok( + !registryRootKey.isChildDeleted(privateProtocol), + "Firefox private protocol registry deleted when it shouldn't be" + ); + } }); add_task(async function test_DeleteWhenUnexpectedRegistryEntries() { - const applicationPath = "testPath"; - - const firefoxEntries = new Map(); - firefoxEntries.set("", `\"${applicationPath}\" -osint -url \"%1\"`); - firefoxEntries.set("extraEntry", "extraValue"); - const firefoxProtocolRegKey = new StubbedRegistryKey( - new Map(), - firefoxEntries - ); - - const children = new Map(); - children.set(FIREFOX_SHELL_OPEN_COMMAND_PATH, firefoxProtocolRegKey); - - const registryRootKey = new StubbedRegistryKey(children, new Map()); - - const stubbedDeleteBridgeProtocolRegistryHelper = - new StubbedDeleteBridgeProtocolRegistryEntryHelper({ - applicationPath, - registryRootKey, - }); - - FirefoxBridgeExtensionUtils.maybeDeleteBridgeProtocolRegistryEntries( - stubbedDeleteBridgeProtocolRegistryHelper - ); - - ok(registryRootKey.wasCloseCalled, "Root key closed"); - - ok(firefoxProtocolRegKey.wasOpenedForRead, "Firefox key opened"); - ok(firefoxProtocolRegKey.wasCloseCalled, "Firefox key closed"); - ok( - !registryRootKey.isChildDeleted("firefox"), - "Firefox protocol registry entry deleted when it shouldn't be" - ); - ok( - !registryRootKey.isChildDeleted("firefox-private"), - "Firefox private protocol registry deleted when it shouldn't be" - ); + for (let protocols of [ + [ + FirefoxBridgeExtensionUtils.OLD_PUBLIC_PROTOCOL, + FirefoxBridgeExtensionUtils.OLD_PRIVATE_PROTOCOL, + OLD_FIREFOX_SHELL_OPEN_COMMAND_PATH, + ], + [ + FirefoxBridgeExtensionUtils.PUBLIC_PROTOCOL, + FirefoxBridgeExtensionUtils.PRIVATE_PROTOCOL, + FIREFOX_SHELL_OPEN_COMMAND_PATH, + ], + ]) { + let [publicProtocol, privateProtocol, publicPath] = protocols; + const applicationPath = "testPath"; + + const firefoxEntries = new Map(); + firefoxEntries.set("", `\"${applicationPath}\" -osint -url \"%1\"`); + firefoxEntries.set("extraEntry", "extraValue"); + const firefoxProtocolRegKey = new StubbedRegistryKey( + new Map(), + firefoxEntries + ); + + const children = new Map(); + children.set(publicPath, firefoxProtocolRegKey); + + const registryRootKey = new StubbedRegistryKey(children, new Map()); + + const stubbedDeleteBridgeProtocolRegistryHelper = + new StubbedDeleteBridgeProtocolRegistryEntryHelper({ + applicationPath, + registryRootKey, + }); + + FirefoxBridgeExtensionUtils.maybeDeleteBridgeProtocolRegistryEntries( + publicProtocol, + privateProtocol, + stubbedDeleteBridgeProtocolRegistryHelper + ); + + ok(registryRootKey.wasCloseCalled, "Root key closed"); + + ok(firefoxProtocolRegKey.wasOpenedForRead, "Firefox key opened"); + ok(firefoxProtocolRegKey.wasCloseCalled, "Firefox key closed"); + ok( + !registryRootKey.isChildDeleted(publicProtocol), + "Firefox protocol registry entry deleted when it shouldn't be" + ); + ok( + !registryRootKey.isChildDeleted(privateProtocol), + "Firefox private protocol registry deleted when it shouldn't be" + ); + } }); diff --git a/browser/modules/test/unit/test_FirefoxBridgeExtensionUtilsNativeManifest.js b/browser/modules/test/unit/test_FirefoxBridgeExtensionUtilsNativeManifest.js index cef550d705..8686871255 100644 --- a/browser/modules/test/unit/test_FirefoxBridgeExtensionUtilsNativeManifest.js +++ b/browser/modules/test/unit/test_FirefoxBridgeExtensionUtilsNativeManifest.js @@ -16,6 +16,30 @@ const { FirefoxBridgeExtensionUtils } = ChromeUtils.importESModule( const DUAL_BROWSER_EXTENSION_ORIGIN = ["chrome-extension://fake-origin/"]; const NATIVE_MESSAGING_HOST_ID = "org.mozilla.firefox_bridge_test"; +if (AppConstants.platform == "win") { + var { MockRegistry } = ChromeUtils.importESModule( + "resource://testing-common/MockRegistry.sys.mjs" + ); +} + +let registry = null; +add_setup(() => { + if (AppConstants.platform == "win") { + registry = new MockRegistry(); + registerCleanupFunction(() => { + registry.shutdown(); + }); + } +}); + +function resetMockRegistry() { + if (AppConstants.platform != "win") { + return; + } + registry.shutdown(); + registry = new MockRegistry(); +} + let dir = FileUtils.getDir("TmpD", ["NativeMessagingHostsTest"]); dir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); @@ -75,6 +99,35 @@ function getExpectedOutput() { }; } +function DumpWindowsRegistry() { + let key = ""; + let pathBuffer = []; + + if (AppConstants.platform == "win") { + function bufferPrint(line) { + let regPath = line.trimStart(); + if (regPath.includes(":")) { + // After trimming white space, keys are formatted as + // ": ()". We can assume it's only ever + // going to be of type REG_SZ for this test. + key = regPath.slice(2, regPath.length - " (REG_SZ)".length); + } else { + pathBuffer.push(regPath); + } + } + + MockRegistry.dump( + MockRegistry.getRoot(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER), + "", + bufferPrint + ); + } else { + Assert.ok(false, "Only windows has a registry!"); + } + + return [pathBuffer, key]; +} + add_task(async function test_maybeWriteManifestFiles() { await FirefoxBridgeExtensionUtils.maybeWriteManifestFiles( USER_TEST_PATH, @@ -196,6 +249,7 @@ add_task(async function test_ensureRegistered() { appDir.path, "Mozilla\\Firefox" ); + resetMockRegistry(); } else { throw new Error("Unsupported platform"); } @@ -219,4 +273,65 @@ add_task(async function test_ensureRegistered() { let JSONContent = await IOUtils.readUTF8(expectedJSONPath); await IOUtils.remove(expectedJSONPath); Assert.equal(JSONContent, expectedOutput); + + // Test that the registry key is written for Windows only + if (AppConstants.platform == "win") { + let [pathBuffer, key] = DumpWindowsRegistry(); + Assert.equal( + pathBuffer.toString(), + [ + "Software", + "Google", + "Chrome", + "NativeMessagingHosts", + nativeHostId, + ].toString() + ); + Assert.equal(key, expectedJSONPath); + } +}); + +add_task(async function test_maybeWriteNativeMessagingRegKeys() { + if (AppConstants.platform != "win") { + return; + } + resetMockRegistry(); + FirefoxBridgeExtensionUtils.maybeWriteNativeMessagingRegKeys( + "Test\\Path\\For\\Reg\\Key", + binFile.parent.path, + NATIVE_MESSAGING_HOST_ID + ); + let [pathBuffer, key] = DumpWindowsRegistry(); + registry.shutdown(); + Assert.equal( + pathBuffer.toString(), + ["Test", "Path", "For", "Reg", "Key", NATIVE_MESSAGING_HOST_ID].toString() + ); + console.log("The key is: " + key); + Assert.equal(key, `${binFile.parent.path}\\${NATIVE_MESSAGING_HOST_ID}.json`); +}); + +add_task(async function test_maybeWriteNativeMessagingRegKeysIncorrectValue() { + if (AppConstants.platform != "win") { + return; + } + resetMockRegistry(); + registry.setValue( + Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, + `Test\\Path\\For\\Reg\\Key\\${NATIVE_MESSAGING_HOST_ID}`, + "", + "IncorrectValue" + ); + FirefoxBridgeExtensionUtils.maybeWriteNativeMessagingRegKeys( + "Test\\Path\\For\\Reg\\Key", + binFile.parent.path, + NATIVE_MESSAGING_HOST_ID + ); + let [pathBuffer, key] = DumpWindowsRegistry(); + registry.shutdown(); + Assert.equal( + pathBuffer.toString(), + ["Test", "Path", "For", "Reg", "Key", NATIVE_MESSAGING_HOST_ID].toString() + ); + Assert.equal(key, `${binFile.parent.path}\\${NATIVE_MESSAGING_HOST_ID}.json`); }); -- cgit v1.2.3