From cb31c16d87dd9fc63e9237007ade5c7b987c1bfa Mon Sep 17 00:00:00 2001 From: William Durand Date: Fri, 19 Aug 2022 06:25:13 +0000 Subject: [PATCH] Bug 1784292 - Anchor extension popups to the unified extensions button. r=mconley,mixedpuppy Differential Revision: https://phabricator.services.mozilla.com/D154408 --- browser/base/content/browser-addons.js | 71 +++++++++--- .../extensions/test/browser/browser.ini | 1 + .../browser_unified_extensions_doorhangers.js | 100 ++++++++++++++++ browser/modules/ExtensionsUI.jsm | 35 +++++- toolkit/modules/PopupNotifications.jsm | 12 +- .../xpinstall/browser_doorhanger_installs.js | 107 ++++++++++++++---- 6 files changed, 282 insertions(+), 44 deletions(-) create mode 100644 browser/components/extensions/test/browser/browser_unified_extensions_doorhangers.js diff --git a/browser/base/content/browser-addons.js b/browser/base/content/browser-addons.js index cdb9eb299792..87f8dbc777af 100644 --- a/browser/base/content/browser-addons.js +++ b/browser/base/content/browser-addons.js @@ -270,8 +270,6 @@ var gXPInstallObserver = { return; } - const anchorID = "addons-notification-icon"; - // Make notifications persistent var options = { displayURI: installInfo.originatingURI, @@ -279,6 +277,14 @@ var gXPInstallObserver = { hideClose: true, }; + if (gUnifiedExtensions.isEnabled) { + options.popupOptions = { + position: "bottomcenter topright", + x: 2, + y: 0, + }; + } + let acceptInstallation = () => { for (let install of installInfo.installs) { install.install(); @@ -422,7 +428,7 @@ var gXPInstallObserver = { browser, "addon-install-confirmation", messageString, - anchorID, + gUnifiedExtensions.getPopupAnchorID(browser, window), action, [secondaryAction], options @@ -496,7 +502,6 @@ var gXPInstallObserver = { return; } - const anchorID = "addons-notification-icon"; var messageString, action; var brandShortName = brandBundle.getString("brandShortName"); @@ -509,6 +514,14 @@ var gXPInstallObserver = { timeout: Date.now() + 30000, }; + if (gUnifiedExtensions.isEnabled) { + options.popupOptions = { + position: "bottomcenter topright", + x: 2, + y: 0, + }; + } + switch (aTopic) { case "addon-install-disabled": { notificationID = "xpinstall-disabled"; @@ -550,7 +563,7 @@ var gXPInstallObserver = { browser, notificationID, messageString, - anchorID, + gUnifiedExtensions.getPopupAnchorID(browser, window), action, secondaryActions, options @@ -597,7 +610,7 @@ var gXPInstallObserver = { browser, notificationID, messageString, - anchorID, + gUnifiedExtensions.getPopupAnchorID(browser, window), null, null, options @@ -728,7 +741,7 @@ var gXPInstallObserver = { browser, notificationID, messageString, - anchorID, + gUnifiedExtensions.getPopupAnchorID(browser, window), action, [dontAllowAction, neverAllowAction], options @@ -793,7 +806,7 @@ var gXPInstallObserver = { browser, notificationID, messageString, - anchorID, + gUnifiedExtensions.getPopupAnchorID(browser, window), action, [secondaryAction], options @@ -876,7 +889,7 @@ var gXPInstallObserver = { browser, notificationID, messageString, - anchorID, + gUnifiedExtensions.getPopupAnchorID(browser, window), action, null, options @@ -950,7 +963,7 @@ var gXPInstallObserver = { browser, notificationID, messageString, - anchorID, + gUnifiedExtensions.getPopupAnchorID(browser, window), action, secondaryActions, options @@ -1291,22 +1304,46 @@ var gUnifiedExtensions = { return; } - const unifiedExtensionsEnabled = Services.prefs.getBoolPref( - "extensions.unifiedExtensions.enabled", - false - ); - - if (unifiedExtensionsEnabled) { + if (this.isEnabled) { MozXULElement.insertFTLIfNeeded("preview/unifiedExtensions.ftl"); this._button = document.getElementById("unified-extensions-button"); // TODO: Bug 1778684 - Auto-hide button when there is no active extension. - this._button.hidden = !unifiedExtensionsEnabled; + this._button.hidden = false; } this._initialized = true; }, + get isEnabled() { + return Services.prefs.getBoolPref( + "extensions.unifiedExtensions.enabled", + false + ); + }, + + getPopupAnchorID(aBrowser, aWindow) { + if (this.isEnabled) { + const anchorID = "unified-extensions-button"; + const attr = anchorID + "popupnotificationanchor"; + + if (!aBrowser[attr]) { + // A hacky way of setting the popup anchor outside the usual url bar + // icon box, similar to how it was done for CFR. + // See: https://searchfox.org/mozilla-central/rev/847b64cc28b74b44c379f9bff4f415b97da1c6d7/toolkit/modules/PopupNotifications.jsm#42 + aBrowser[attr] = aWindow.document.getElementById( + anchorID + // Anchor on the toolbar icon to position the popup right below the + // button. + ).firstElementChild; + } + + return anchorID; + } + + return "addons-notification-icon"; + }, + get button() { return this._button; }, diff --git a/browser/components/extensions/test/browser/browser.ini b/browser/components/extensions/test/browser/browser.ini index 81455df9bc2c..2d42b0fd6a42 100644 --- a/browser/components/extensions/test/browser/browser.ini +++ b/browser/components/extensions/test/browser/browser.ini @@ -403,3 +403,4 @@ https_first_disabled = true [browser_unified_extensions.js] [browser_unified_extensions_accessibility.js] [browser_unified_extensions_context_menu.js] +[browser_unified_extensions_doorhangers.js] diff --git a/browser/components/extensions/test/browser/browser_unified_extensions_doorhangers.js b/browser/components/extensions/test/browser/browser_unified_extensions_doorhangers.js new file mode 100644 index 000000000000..141e56c96d5d --- /dev/null +++ b/browser/components/extensions/test/browser/browser_unified_extensions_doorhangers.js @@ -0,0 +1,100 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +loadTestSubscript("head_unified_extensions.js"); + +let win; + +add_setup(async function() { + // Only load a new window with the unified extensions feature enabled once to + // speed up the execution of this test file. + win = await promiseEnableUnifiedExtensions(); + + registerCleanupFunction(async () => { + await BrowserTestUtils.closeWindow(win); + }); +}); + +const verifyPermissionsPrompt = async (win, expectedAnchorID) => { + const ext = ExtensionTestUtils.loadExtension({ + manifest: { + optional_permissions: ["history"], + }, + + background: async () => { + await browser.tabs.create({ + url: browser.runtime.getURL("content.html"), + active: true, + }); + }, + + files: { + "content.html": ``, + "content.js": async () => { + browser.test.onMessage.addListener(async msg => { + browser.test.assertEq( + msg, + "grant-permission", + "expected message to grant permission" + ); + + const granted = await new Promise(resolve => { + browser.test.withHandlingUserInput(() => { + resolve( + browser.permissions.request({ permissions: ["history"] }) + ); + }); + }); + browser.test.assertTrue(granted, "permission request succeeded"); + + browser.test.sendMessage("ok"); + }); + + browser.test.sendMessage("ready"); + }, + }, + }); + + await BrowserTestUtils.withNewTab({ gBrowser: win.gBrowser }, async () => { + await ext.startup(); + await ext.awaitMessage("ready"); + + const popupPromise = promisePopupNotificationShown( + "addon-webext-permissions", + win + ); + ext.sendMessage("grant-permission"); + const panel = await popupPromise; + const notification = win.PopupNotifications.getNotification( + "addon-webext-permissions" + ); + ok(notification, "expected notification"); + is( + // We access the parent element because the anchor is on the icon, not on + // the unified extensions button itself. + notification.anchorElement.id || + notification.anchorElement.parentElement.id, + expectedAnchorID, + "expected the right anchor ID" + ); + + panel.button.click(); + await ext.awaitMessage("ok"); + + await ext.unload(); + }); +}; + +add_task(async function test_permissions_prompt_with_pref_enabled() { + await verifyPermissionsPrompt(win, "unified-extensions-button"); +}); + +add_task(async function test_permissions_prompt_with_pref_disabled() { + const anotherWindow = await promiseDisableUnifiedExtensions(); + + await verifyPermissionsPrompt(anotherWindow, "addons-notification-icon"); + + await BrowserTestUtils.closeWindow(anotherWindow); +}); diff --git a/browser/modules/ExtensionsUI.jsm b/browser/modules/ExtensionsUI.jsm index 55ad341c2fdf..74db7100d6a0 100644 --- a/browser/modules/ExtensionsUI.jsm +++ b/browser/modules/ExtensionsUI.jsm @@ -421,7 +421,7 @@ var ExtensionsUI = { return false; } - let popupOptions = { + let options = { hideClose: true, popupIconURL: icon || DEFAULT_EXTENSION_ICON, popupIconClass: icon ? "" : "addon-warning-icon", @@ -454,14 +454,25 @@ var ExtensionsUI = { }, ]; + if (browser.ownerGlobal.gUnifiedExtensions.isEnabled) { + options.popupOptions = { + position: "bottomcenter topright", + x: 2, + y: 0, + }; + } + window.PopupNotifications.show( browser, "addon-webext-permissions", strings.header, - "addons-notification-icon", + browser.ownerGlobal.gUnifiedExtensions.getPopupAnchorID( + browser, + window + ), action, secondaryActions, - popupOptions + options ); }); @@ -472,7 +483,7 @@ var ExtensionsUI = { showDefaultSearchPrompt(target, strings, icon) { return new Promise(resolve => { - let popupOptions = { + let options = { hideClose: true, popupIconURL: icon || DEFAULT_EXTENSION_ICON, persistent: true, @@ -503,14 +514,26 @@ var ExtensionsUI = { ]; let { browser, window } = getTabBrowser(target); + + if (browser.ownerGlobal.gUnifiedExtensions.isEnabled) { + options.popupOptions = { + position: "bottomcenter topright", + x: 2, + y: 0, + }; + } + window.PopupNotifications.show( browser, "addon-webext-defaultsearch", strings.text, - "addons-notification-icon", + browser.ownerGlobal.gUnifiedExtensions.getPopupAnchorID( + browser, + window + ), action, secondaryActions, - popupOptions + options ); }); }, diff --git a/toolkit/modules/PopupNotifications.jsm b/toolkit/modules/PopupNotifications.jsm index f70a76c8e859..1857ee2eb1e4 100644 --- a/toolkit/modules/PopupNotifications.jsm +++ b/toolkit/modules/PopupNotifications.jsm @@ -528,6 +528,9 @@ PopupNotifications.prototype = { * extraAttr: * An optional string value which will be given to the * extraAttr attribute on the notification's anchorElement + * popupOptions: + * An optional object containing popup options passed to + * `openPopup()` when defined. * @returns the Notification object corresponding to the added notification. */ show: function PopupNotifications_show( @@ -1336,7 +1339,14 @@ PopupNotifications.prototype = { this._popupshownListener = this._popupshownListener.bind(this); target.addEventListener("popupshown", this._popupshownListener, true); - this.panel.openPopup(anchorElement, "bottomcenter topleft", 0, 0); + let popupOptions = notificationsToShow.findLast( + n => n.options?.popupOptions + )?.options?.popupOptions; + if (popupOptions) { + this.panel.openPopup(anchorElement, popupOptions); + } else { + this.panel.openPopup(anchorElement, "bottomcenter topleft", 0, 0); + } }); }, diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_doorhanger_installs.js b/toolkit/mozapps/extensions/test/xpinstall/browser_doorhanger_installs.js index ec5d82076e43..699ec1013450 100644 --- a/toolkit/mozapps/extensions/test/xpinstall/browser_doorhanger_installs.js +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_doorhanger_installs.js @@ -61,7 +61,9 @@ function getObserverTopic(aNotificationId) { async function waitForProgressNotification( aPanelOpen = false, aExpectedCount = 1, - wantDisabled = true + wantDisabled = true, + expectedAnchorID = "addons-notification-icon", + win = window ) { let notificationId = PROGRESS_NOTIFICATION; info("Waiting for " + notificationId + " notification"); @@ -87,7 +89,7 @@ async function waitForProgressNotification( panelEventPromise = Promise.resolve(); } else { panelEventPromise = new Promise(resolve => { - PopupNotifications.panel.addEventListener( + win.PopupNotifications.panel.addEventListener( "popupshowing", function() { resolve(); @@ -102,14 +104,14 @@ async function waitForProgressNotification( await waitForTick(); info("Saw a notification"); - ok(PopupNotifications.isPanelOpen, "Panel should be open"); + ok(win.PopupNotifications.isPanelOpen, "Panel should be open"); is( - PopupNotifications.panel.childNodes.length, + win.PopupNotifications.panel.childNodes.length, aExpectedCount, "Should be the right number of notifications" ); - if (PopupNotifications.panel.childNodes.length) { - let nodes = Array.from(PopupNotifications.panel.childNodes); + if (win.PopupNotifications.panel.childNodes.length) { + let nodes = Array.from(win.PopupNotifications.panel.childNodes); let notification = nodes.find( n => n.id == notificationId + "-notification" ); @@ -119,9 +121,16 @@ async function waitForProgressNotification( wantDisabled, "The install button should be disabled?" ); + + let n = win.PopupNotifications.getNotification(PROGRESS_NOTIFICATION); + is( + n?.anchorElement?.id || n?.anchorElement?.parentElement?.id, + expectedAnchorID, + "expected the right anchor ID" + ); } - return PopupNotifications.panel; + return win.PopupNotifications.panel; } function acceptAppMenuNotificationWhenShown( @@ -205,7 +214,12 @@ function acceptAppMenuNotificationWhenShown( }); } -async function waitForNotification(aId, aExpectedCount = 1) { +async function waitForNotification( + aId, + aExpectedCount = 1, + expectedAnchorID = "addons-notification-icon", + win = window +) { info("Waiting for " + aId + " notification"); let topic = getObserverTopic(aId); @@ -228,14 +242,14 @@ async function waitForNotification(aId, aExpectedCount = 1) { } let panelEventPromise = new Promise(resolve => { - PopupNotifications.panel.addEventListener( + win.PopupNotifications.panel.addEventListener( "PanelUpdated", function eventListener(e) { // Skip notifications that are not the one that we are supposed to be looking for if (!e.detail.includes(aId)) { return; } - PopupNotifications.panel.removeEventListener( + win.PopupNotifications.panel.removeEventListener( "PanelUpdated", eventListener ); @@ -249,20 +263,27 @@ async function waitForNotification(aId, aExpectedCount = 1) { await waitForTick(); info("Saw a " + aId + " notification"); - ok(PopupNotifications.isPanelOpen, "Panel should be open"); + ok(win.PopupNotifications.isPanelOpen, "Panel should be open"); is( - PopupNotifications.panel.childNodes.length, + win.PopupNotifications.panel.childNodes.length, aExpectedCount, "Should be the right number of notifications" ); - if (PopupNotifications.panel.childNodes.length) { - let nodes = Array.from(PopupNotifications.panel.childNodes); + if (win.PopupNotifications.panel.childNodes.length) { + let nodes = Array.from(win.PopupNotifications.panel.childNodes); let notification = nodes.find(n => n.id == aId + "-notification"); ok(notification, "Should have seen the " + aId + " notification"); - } - await SimpleTest.promiseFocus(PopupNotifications.window); - return PopupNotifications.panel; + let n = win.PopupNotifications.getNotification(aId); + is( + n?.anchorElement?.id || n?.anchorElement?.parentElement?.id, + expectedAnchorID, + "expected the right anchor ID" + ); + } + await SimpleTest.promiseFocus(win.PopupNotifications.window); + + return win.PopupNotifications.panel; } function waitForNotificationClose() { @@ -431,6 +452,7 @@ var TESTS = [ await addon.uninstall(); await BrowserTestUtils.removeTab(gBrowser.selectedTab); + await SpecialPowers.popPrefEnv(); }, async function test_blockedInstallDomain() { @@ -1198,7 +1220,10 @@ var TESTS = [ async function test_failedSecurity() { SpecialPowers.pushPrefEnv({ - set: [[PREF_INSTALL_REQUIREBUILTINCERTS, false]], + set: [ + [PREF_INSTALL_REQUIREBUILTINCERTS, false], + ["extensions.postDownloadThirdPartyPrompt", false], + ], }); setupRedirect({ @@ -1334,7 +1359,6 @@ var TESTS = [ await addon.uninstall(); await removeTabAndWaitForNotificationClose(); - await SpecialPowers.popPrefEnv(); }, async function test_incognito_checkbox_new_window() { @@ -1428,9 +1452,52 @@ var TESTS = [ await addon.uninstall(); - await SpecialPowers.popPrefEnv(); await BrowserTestUtils.closeWindow(win); }, + + async function test_blockedInstallDomain_with_unified_extensions() { + SpecialPowers.pushPrefEnv({ + set: [["extensions.unifiedExtensions.enabled", true]], + }); + + let win = await BrowserTestUtils.openNewBrowserWindow(); + await SimpleTest.promiseFocus(win); + await new Promise(resolve => { + win.requestIdleCallback(resolve); + }); + await TestUtils.waitForCondition( + () => win.gUnifiedExtensions._initialized, + "Wait gUnifiedExtensions to have been initialized" + ); + + let progressPromise = waitForProgressNotification( + false, + 1, + true, + "unified-extensions-button", + win + ); + let notificationPromise = waitForNotification( + "addon-install-failed", + 1, + "unified-extensions-button", + win + ); + let triggers = encodeURIComponent( + JSON.stringify({ + XPI: TESTROOT2 + "webmidi_permission.xpi", + }) + ); + BrowserTestUtils.openNewForegroundTab( + win.gBrowser, + TESTROOT + "installtrigger.html?" + triggers + ); + await progressPromise; + await notificationPromise; + + await BrowserTestUtils.closeWindow(win); + await SpecialPowers.popPrefEnv(); + }, ]; var gTestStart = null;