tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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:
Mbrowser/base/content/browser-addons.js | 60+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mbrowser/components/extensions/test/browser/browser_unified_extensions_appmenu_item.js | 57+++++++++++++++++++++++++++++++++------------------------
Mbrowser/components/extensions/test/browser/browser_unified_extensions_empty_panel.js | 109+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------
Mbrowser/locales/en-US/browser/unifiedExtensions.ftl | 5+++++
Mbrowser/themes/shared/addons/unified-extensions.css | 9+++++++++
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;