commit 992283dcd23eb71cafa983228d9c68d2d297afe0
parent a748e9d61e04fa1f0f6c8f5c6bb312967f4901a2
Author: Rob Wu <rob@robwu.nl>
Date: Thu, 9 Oct 2025 01:59:35 +0000
Bug 1778684 - Show discover button in panel when there are no extensions r=rpl,fluent-reviewers,desktop-theme-reviewers,bolsson,emilio,dao
Differential Revision: https://phabricator.services.mozilla.com/D267520
Diffstat:
5 files changed, 191 insertions(+), 49 deletions(-)
diff --git a/browser/base/content/browser-addons.js b/browser/base/content/browser-addons.js
@@ -2407,6 +2407,28 @@ var gUnifiedExtensions = {
"unified-extensions-empty-content-explain-enable"
);
emptyStateBox.hidden = false;
+ } else if (!policies.length) {
+ document.l10n.setAttributes(
+ emptyStateBox.querySelector("h2"),
+ "unified-extensions-empty-reason-zero-extensions-onboarding"
+ );
+ document.l10n.setAttributes(
+ emptyStateBox.querySelector("description"),
+ "unified-extensions-empty-content-explain-extensions-onboarding"
+ );
+ emptyStateBox.hidden = false;
+
+ // Replace the "Manage Extensions" button with "Discover Extensions".
+ // We add the "Discover Extensions" button, and "Manage Extensions"
+ // button (#unified-extensions-manage-extensions) is hidden by CSS.
+ const discoverButton = this._createDiscoverButton(panelview);
+
+ const manageExtensionsButton = panelview.querySelector(
+ "#unified-extensions-manage-extensions"
+ );
+ // Insert before toolbarseparator, to make it easier to hide the
+ // toolbarseparator and manageExtensionsButton with CSS.
+ manageExtensionsButton.previousElementSibling.before(discoverButton);
}
});
}
@@ -2458,6 +2480,10 @@ var gUnifiedExtensions = {
while (list.lastChild) {
list.lastChild.remove();
}
+ panelview
+ .querySelector("#unified-extensions-discover-extensions")
+ ?.remove();
+
// If temporary access was granted, (maybe) clear attention indicator.
requestAnimationFrame(() => this.updateAttention());
},
@@ -2627,8 +2653,10 @@ var gUnifiedExtensions = {
// The button should directly open `about:addons` when the user does not
// have any active extensions listed in the unified extensions panel,
// and no alternative content is available for display in the panel.
+ const policies = this.getActivePolicies();
if (
- !this.hasExtensionsInPanel() &&
+ policies.length &&
+ !this.hasExtensionsInPanel(policies) &&
!this.isPrivateWindowMissingExtensionsWithoutPBMAccess() &&
!(await this.isAtLeastOneExtensionDisabled())
) {
@@ -3116,6 +3144,36 @@ var gUnifiedExtensions = {
return messageBar;
},
+ _createDiscoverButton() {
+ const discoverButton = document.createElement("moz-button");
+ discoverButton.id = "unified-extensions-discover-extensions";
+ discoverButton.type = "primary";
+ discoverButton.className = "subviewbutton panel-subview-footer-button";
+ document.l10n.setAttributes(
+ discoverButton,
+ "unified-extensions-discover-extensions"
+ );
+
+ discoverButton.addEventListener("click", () => {
+ if (
+ // The "Discover Extensions" button is only shown if the user has not
+ // installed any extension. In that case, we direct to the discopane
+ // in about:addons. If the discopane is disabled, open the default
+ // view (Extensions list) instead. This view shows a link to AMO when
+ // the user does not have any extensions installed.
+ Services.prefs.getBoolPref("extensions.getAddons.showPane", true)
+ ) {
+ BrowserAddonUI.openAddonsMgr("addons://list/discover");
+ } else {
+ BrowserAddonUI.openAddonsMgr("addons://list/extension");
+ }
+ // Close panel.
+ this.togglePanel();
+ });
+
+ return discoverButton;
+ },
+
_shouldShowQuarantinedNotification() {
const { currentURI, selectedTab } = window.gBrowser;
// We should show the quarantined notification when the domain is in the
diff --git a/browser/components/extensions/test/browser/browser_unified_extensions_appmenu_item.js b/browser/components/extensions/test/browser/browser_unified_extensions_appmenu_item.js
@@ -106,9 +106,10 @@ add_task(async function test_appmenu_extensions_opens_panel() {
await SpecialPowers.popPrefEnv();
});
-// When the Extensions Button is visible, it opens about:addons upon click.
+// When the Extensions Button is visible, it used to open about:addons upon
+// click, but now opens the extensions panel instead.
// Do the same when the Extensions app menu item is clicked.
-// This behavior differs from test_appmenu_extensions_opens_panel.
+// This behavior is the same as test_appmenu_extensions_opens_panel.
add_task(async function test_appmenu_extensions_opens_when_no_extensions() {
// The test harness registers regular extensions so we need to mock the
// `getActivePolicies` extension to simulate zero extensions installed.
@@ -122,29 +123,11 @@ add_task(async function test_appmenu_extensions_opens_when_no_extensions() {
});
await gCUITestUtils.openMainMenu();
- const listener = () => {
- ok(false, "Extensions Panel should not be shown");
- };
- gUnifiedExtensions.panel.addEventListener("popupshowing", listener);
-
- // When the list of extensions is empty, the menu opens about:addons.
- // Open a new non-newtab page so that about:addons will be opened in a new
- // tab (instead of reusing the "current" new tab).
- await BrowserTestUtils.withNewTab(
- { gBrowser, url: "about:robots" },
- async () => {
- let tabPromise = BrowserTestUtils.waitForNewTab(gBrowser, "about:addons");
-
- assertExtensionsButtonHidden();
- menuItemThatOpensExtensionsPanel().click();
- assertExtensionsButtonHidden();
- info("Verifying that about:addons is opened");
- BrowserTestUtils.removeTab(await tabPromise);
- }
- );
-
assertExtensionsButtonHidden();
- gUnifiedExtensions.panel.removeEventListener("popupshowing", listener);
+ menuItemThatOpensExtensionsPanel().click();
+ is(PanelUI.panel.state, "closed", "Menu closed after clicking Extensions");
+ // assertExtensionsButtonVisible(); cannot be checked because button showing
+ // is async. We will check its visibility later, before closing the panel.
Assert.deepEqual(
Glean.extensionsButton.openViaAppMenu.testGetValue().map(e => e.extra),
@@ -157,6 +140,32 @@ add_task(async function test_appmenu_extensions_opens_when_no_extensions() {
"extensions_button.open_via_app_menu telemetry on menu click"
);
+ const listView = getListView();
+ await BrowserTestUtils.waitForEvent(listView, "ViewShown");
+ ok(PanelView.forNode(listView).active, "Extensions panel is shown");
+
+ // Sanity check to verify that the extensions list was indeed empty, by
+ // verifying that the empty state is shown. The content of the panel is
+ // verified by browser_unified_extensions_empty_panel.js.
+ const emptyStateBox = gUnifiedExtensions.panel.querySelector(
+ "#unified-extensions-empty-state"
+ );
+ ok(emptyStateBox, "Got container for empty panel state");
+ ok(BrowserTestUtils.isVisible(emptyStateBox), "Empty state is visible");
+ is(
+ emptyStateBox.querySelector("h2").getAttribute("data-l10n-id"),
+ "unified-extensions-empty-reason-zero-extensions-onboarding",
+ "Has header when the user does not have any extensions installed"
+ );
+
+ assertExtensionsButtonVisible();
+ assertExtensionsButtonTelemetry({ extensions_panel_showing: 1 });
+ await closeExtensionsPanel();
+ assertExtensionsButtonHidden();
+
+ // No more counters besides the one that we saw before in this test.
+ assertExtensionsButtonTelemetry({ extensions_panel_showing: 1 });
+
await SpecialPowers.popPrefEnv();
gUnifiedExtensions.getActivePolicies = origGetActivePolicies;
diff --git a/browser/components/extensions/test/browser/browser_unified_extensions_empty_panel.js b/browser/components/extensions/test/browser/browser_unified_extensions_empty_panel.js
@@ -54,6 +54,46 @@ function getEmptyStateContainer(win) {
return emptyStateBox;
}
+function assertIsEmptyPanelOnboardingExtensions(win) {
+ const emptyStateBox = getEmptyStateContainer(win);
+ ok(BrowserTestUtils.isVisible(emptyStateBox), "Empty state is visible");
+ is(
+ emptyStateBox.querySelector("h2").getAttribute("data-l10n-id"),
+ "unified-extensions-empty-reason-zero-extensions-onboarding",
+ "Has header when the user does not have any extensions installed"
+ );
+ is(
+ emptyStateBox.querySelector("description").getAttribute("data-l10n-id"),
+ "unified-extensions-empty-content-explain-extensions-onboarding",
+ "Has description explaining extensions"
+ );
+
+ const discoverButton = getDiscoverButton(win);
+ ok(discoverButton, "Got 'Discover button'");
+ is(
+ discoverButton.getAttribute("data-l10n-id"),
+ "unified-extensions-discover-extensions",
+ "Button in extensions panel should be labeled 'Discover Extensions'"
+ );
+ is(
+ discoverButton.getAttribute("type"),
+ "primary",
+ "Discover button should be styled as a primary call-to-action button"
+ );
+ const manageExtensionsButton = getListView(win).querySelector(
+ "#unified-extensions-manage-extensions"
+ );
+ ok(
+ BrowserTestUtils.isHidden(manageExtensionsButton),
+ "'Manage Extensions' button should be hidden"
+ );
+}
+function getDiscoverButton(win) {
+ return win.gUnifiedExtensions.panel.querySelector(
+ "#unified-extensions-discover-extensions"
+ );
+}
+
add_task(async function test_button_opens_discopane_when_no_extension() {
await BrowserTestUtils.withNewTab(
{ gBrowser, url: "about:robots" },
@@ -61,14 +101,19 @@ add_task(async function test_button_opens_discopane_when_no_extension() {
const { button } = gUnifiedExtensions;
ok(button, "expected button");
- // Primary click should open about:addons.
+ // This clicks on gUnifiedExtensions.button and waits for panel to show.
+ await openExtensionsPanel(window);
+
+ assertIsEmptyPanelOnboardingExtensions(window);
+ const discoverButton = getDiscoverButton(window);
+
const tabPromise = BrowserTestUtils.waitForNewTab(
gBrowser,
"about:addons",
true
);
- button.click();
+ discoverButton.click();
const tab = await tabPromise;
is(
@@ -82,19 +127,6 @@ add_task(async function test_button_opens_discopane_when_no_extension() {
"expected about:addons to show the recommendations"
);
BrowserTestUtils.removeTab(tab);
-
- // "Right-click" should open the context menu only.
- const contextMenu = document.getElementById("toolbar-context-menu");
- const popupShownPromise = BrowserTestUtils.waitForEvent(
- contextMenu,
- "popupshown"
- );
- EventUtils.synthesizeMouseAtCenter(button, {
- type: "contextmenu",
- button: 2,
- });
- await popupShownPromise;
- await closeChromeContextMenu(contextMenu.id, null);
}
);
});
@@ -115,17 +147,19 @@ add_task(
await BrowserTestUtils.withNewTab(
{ gBrowser, url: "about:robots" },
async () => {
- const { button } = gUnifiedExtensions;
- ok(button, "expected button");
+ // This clicks on gUnifiedExtensions.button and waits for panel to show.
+ await openExtensionsPanel(window);
+
+ assertIsEmptyPanelOnboardingExtensions(window);
+ const discoverButton = getDiscoverButton(window);
- // Primary click should open about:addons.
const tabPromise = BrowserTestUtils.waitForNewTab(
gBrowser,
"about:addons",
true
);
- button.click();
+ discoverButton.click();
const tab = await tabPromise;
is(
@@ -133,11 +167,28 @@ add_task(
"about:addons",
"expected about:addons to be open"
);
+ const managerWindow = gBrowser.selectedBrowser.contentWindow;
is(
- gBrowser.selectedBrowser.contentWindow.gViewController.currentViewId,
+ managerWindow.gViewController.currentViewId,
"addons://list/extension",
"expected about:addons to show the extension list"
);
+ if (managerWindow.gViewController.isLoading) {
+ info("Waiting for about:addons to finish loading");
+ await BrowserTestUtils.waitForEvent(
+ managerWindow.document,
+ "view-loaded"
+ );
+ }
+ const amoLink = managerWindow.document.querySelector(
+ `#empty-addons-message a[data-l10n-name="get-extensions"]`
+ );
+ ok(amoLink, "Found link to get extensions");
+ is(
+ amoLink.href,
+ "https://addons.mozilla.org/en-US/firefox/",
+ "Link points to AMO, where the user can discover extensions"
+ );
BrowserTestUtils.removeTab(tab);
}
);
@@ -149,6 +200,12 @@ add_task(
add_task(async function test_button_click_in_pbm_without_any_extensions() {
const win = await BrowserTestUtils.openNewBrowserWindow({ private: true });
+ // This clicks on gUnifiedExtensions.button and waits for panel to show.
+ await openExtensionsPanel(win);
+
+ assertIsEmptyPanelOnboardingExtensions(win);
+ const discoverButton = getDiscoverButton(win);
+
// Button click opens about:addons (reuses about:privatebrowsing tab).
// Primary click should open about:addons.
const tabLoadedPromise = BrowserTestUtils.browserStopped(
@@ -156,7 +213,7 @@ add_task(async function test_button_click_in_pbm_without_any_extensions() {
"about:addons"
);
- win.gUnifiedExtensions.button.click();
+ discoverButton.click();
await tabLoadedPromise;
is(
@@ -338,15 +395,19 @@ add_task(async function test_no_empty_state_with_disabled_non_extension() {
await BrowserTestUtils.withNewTab(
{ gBrowser, url: "about:robots" },
async () => {
- // Primary click should open about:addons. Notably, the extensions panel
- // and "You have extensions installed, but not enabled" is not shown.
+ // This clicks on gUnifiedExtensions.button and waits for panel to show.
+ await openExtensionsPanel(window);
+
+ assertIsEmptyPanelOnboardingExtensions(window);
+ const discoverButton = getDiscoverButton(window);
+
const tabPromise = BrowserTestUtils.waitForNewTab(
gBrowser,
"about:addons",
true
);
- gUnifiedExtensions.button.click();
+ discoverButton.click();
const tab = await tabPromise;
ok(true, "about:addons opened instead of panel about disabled add-ons");
diff --git a/browser/locales/en-US/browser/unifiedExtensions.ftl b/browser/locales/en-US/browser/unifiedExtensions.ftl
@@ -9,9 +9,14 @@
unified-extensions-header-title = Extensions
unified-extensions-manage-extensions =
.label = Manage extensions
+unified-extensions-discover-extensions =
+ .label = Discover extensions
unified-extensions-empty-reason-private-browsing-not-allowed = You have extensions installed, but not enabled in private windows
unified-extensions-empty-reason-extension-not-enabled = You have extensions installed, but not enabled
+# In this headline, “Level up” means to enhance your browsing experience.
+unified-extensions-empty-reason-zero-extensions-onboarding = Level up your browsing with extensions
unified-extensions-empty-content-explain-enable = Select “{ unified-extensions-item-message-manage }” to enable them in settings.
+unified-extensions-empty-content-explain-extensions-onboarding = Personalize { -brand-short-name } by changing how it looks and performs or boosting privacy and safety.
## An extension in the main list
diff --git a/browser/themes/shared/addons/unified-extensions.css b/browser/themes/shared/addons/unified-extensions.css
@@ -45,6 +45,15 @@
}
}
+#unified-extensions-discover-extensions {
+ width: auto; /* full width instead of fit-content */
+
+ + toolbarseparator,
+ + toolbarseparator + #unified-extensions-manage-extensions {
+ display: none;
+ }
+}
+
/* Align extensions rendered with custom elements. */
unified-extensions-item {
display: flex;