"use strict"; const { ExtensionPermissions } = ChromeUtils.importESModule( "resource://gre/modules/ExtensionPermissions.sys.mjs" ); loadTestSubscript("head_unified_extensions.js"); async function makeExtension({ useAddonManager = "temporary", manifest_version = 3, id, permissions, host_permissions, content_scripts, granted, }) { info( `Loading extension ` + JSON.stringify({ id, permissions, host_permissions, granted }) ); let manifest = { manifest_version, browser_specific_settings: { gecko: { id } }, permissions, host_permissions, content_scripts, action: { default_popup: "popup.html", default_area: "navbar", }, }; if (manifest_version < 3) { manifest.browser_action = manifest.action; delete manifest.action; } let ext = ExtensionTestUtils.loadExtension({ manifest, useAddonManager, background() { browser.permissions.onAdded.addListener(({ origins }) => { browser.test.sendMessage("granted", origins.join()); }); browser.permissions.onRemoved.addListener(({ origins }) => { browser.test.sendMessage("revoked", origins.join()); }); if (browser.menus) { let submenu = browser.menus.create({ id: "parent", title: "submenu", contexts: ["action"], }); browser.menus.create({ id: "child1", title: "child1", parentId: submenu, }); browser.menus.create({ id: "child2", title: "child2", parentId: submenu, }); } }, files: { "popup.html": `Test Popup`, }, }); if (granted) { info("Granting initial permissions."); await ExtensionPermissions.add(id, { permissions: [], origins: granted }); } await ext.startup(); return ext; } async function testOriginControls( extension, { contextMenuId }, { items, selected, click, granted, revoked, attention } ) { info( `Testing ${extension.id} on ${gBrowser.currentURI.spec} with contextMenuId=${contextMenuId}.` ); let buttonOrWidget; let menu; let nextMenuItemClassName; switch (contextMenuId) { case "toolbar-context-menu": let target = `#${CSS.escape(makeWidgetId(extension.id))}-BAP`; buttonOrWidget = document.querySelector(target).parentElement; menu = await openChromeContextMenu(contextMenuId, target); nextMenuItemClassName = "customize-context-manageExtension"; break; case "unified-extensions-context-menu": await openExtensionsPanel(); buttonOrWidget = getUnifiedExtensionsItem(extension.id); menu = await openUnifiedExtensionsContextMenu(extension.id); nextMenuItemClassName = "unified-extensions-context-menu-pin-to-toolbar"; break; default: throw new Error(`unexpected context menu "${contextMenuId}"`); } let doc = menu.ownerDocument; let visibleOriginItems = menu.querySelectorAll( ":is(menuitem, menuseparator):not([hidden])" ); info("Check expected menu items."); for (let i = 0; i < items.length; i++) { let l10n = doc.l10n.getAttributes(visibleOriginItems[i]); Assert.deepEqual( l10n, items[i], `Visible menu item ${i} has correct l10n attrs.` ); let checked = visibleOriginItems[i].getAttribute("checked") === "true"; is(i === selected, checked, `Expected checked value for item ${i}.`); } if (items.length) { is( visibleOriginItems[items.length].nodeName, "menuseparator", "Found separator." ); is( visibleOriginItems[items.length + 1].className, nextMenuItemClassName, "All items accounted for." ); } is( buttonOrWidget.hasAttribute("attention"), !!attention, "Expected attention badge before clicking." ); Assert.deepEqual( document.l10n.getAttributes( buttonOrWidget.querySelector(".unified-extensions-item-action-button") ), { id: attention ? "origin-controls-toolbar-button-permission-needed" : "origin-controls-toolbar-button", args: { extensionTitle: "Generated extension", }, }, "Correct l10n message." ); let itemToClick; if (click) { itemToClick = visibleOriginItems[click]; } // Clicking a menu item of the unified extensions context menu should close // the unified extensions panel automatically. let panelHidden = itemToClick && contextMenuId === "unified-extensions-context-menu" ? BrowserTestUtils.waitForEvent(document, "popuphidden", true) : Promise.resolve(); await closeChromeContextMenu(contextMenuId, itemToClick); await panelHidden; // When there is no menu item to close, we should manually close the unified // extensions panel because simply closing the context menu will not close // it. if (!itemToClick && contextMenuId === "unified-extensions-context-menu") { await closeExtensionsPanel(); } if (granted) { info("Waiting for the permissions.onAdded event."); let host = await extension.awaitMessage("granted"); is(host, granted.join(), "Expected host permission granted."); } if (revoked) { info("Waiting for the permissions.onRemoved event."); let host = await extension.awaitMessage("revoked"); is(host, revoked.join(), "Expected host permission revoked."); } } // Move the widget to the toolbar or the addons panel (if Unified Extensions // is enabled) or the overflow panel otherwise. function moveWidget(ext, pinToToolbar = false) { let area = pinToToolbar ? CustomizableUI.AREA_NAVBAR : CustomizableUI.AREA_ADDONS; let widgetId = `${makeWidgetId(ext.id)}-browser-action`; CustomizableUI.addWidgetToArea(widgetId, area); } const originControlsInContextMenu = async options => { // Has no permissions. let ext1 = await makeExtension({ id: "ext1@test" }); // Has activeTab and (ungranted) example.com permissions. let ext2 = await makeExtension({ id: "ext2@test", permissions: ["activeTab"], host_permissions: ["*://example.com/*"], useAddonManager: "permanent", }); // Has ungranted , and granted example.com. let ext3 = await makeExtension({ id: "ext3@test", host_permissions: [""], granted: ["*://example.com/*"], useAddonManager: "permanent", }); // Has granted . let ext4 = await makeExtension({ id: "ext4@test", host_permissions: [""], granted: [""], useAddonManager: "permanent", }); // MV2 extension with an content script and activeTab. let ext5 = await makeExtension({ manifest_version: 2, id: "ext5@test", permissions: ["activeTab"], content_scripts: [ { matches: [""], css: [], }, ], useAddonManager: "permanent", }); let extensions = [ext1, ext2, ext3, ext4, ext5]; let unifiedButton; if (options.contextMenuId === "unified-extensions-context-menu") { // Unified button should only show a notification indicator when extensions // asking for attention are not already visible in the toolbar. moveWidget(ext1, false); moveWidget(ext2, false); moveWidget(ext3, false); moveWidget(ext4, false); moveWidget(ext5, false); unifiedButton = document.querySelector("#unified-extensions-button"); } else { // TestVerify runs this again in the same Firefox instance, so move the // widgets back to the toolbar for testing outside the unified extensions // panel. moveWidget(ext1, true); moveWidget(ext2, true); moveWidget(ext3, true); moveWidget(ext4, true); moveWidget(ext5, true); } const NO_ACCESS = { id: "origin-controls-no-access", args: null }; const QUARANTINED = { id: "origin-controls-quarantined", args: null }; const ACCESS_OPTIONS = { id: "origin-controls-options", args: null }; const ALL_SITES = { id: "origin-controls-option-all-domains", args: null }; const WHEN_CLICKED = { id: "origin-controls-option-when-clicked", args: null, }; const UNIFIED_NO_ATTENTION = { id: "unified-extensions-button", args: null }; const UNIFIED_ATTENTION = { id: "unified-extensions-button-permissions-needed", args: null, }; await BrowserTestUtils.withNewTab("about:blank", async () => { await testOriginControls(ext1, options, { items: [NO_ACCESS] }); await testOriginControls(ext2, options, { items: [NO_ACCESS] }); await testOriginControls(ext3, options, { items: [NO_ACCESS] }); await testOriginControls(ext4, options, { items: [NO_ACCESS] }); await testOriginControls(ext5, options, { items: [] }); if (unifiedButton) { ok( !unifiedButton.hasAttribute("attention"), "No extension will have attention indicator on about:blank." ); Assert.deepEqual( document.l10n.getAttributes(unifiedButton), UNIFIED_NO_ATTENTION, "Unified button has no permissions needed tooltip." ); } }); await BrowserTestUtils.withNewTab("http://mochi.test:8888/", async () => { const ALWAYS_ON = { id: "origin-controls-option-always-on", args: { domain: "mochi.test" }, }; await testOriginControls(ext1, options, { items: [NO_ACCESS] }); // Has activeTab. await testOriginControls(ext2, options, { items: [ACCESS_OPTIONS, WHEN_CLICKED], selected: 1, attention: true, }); // Could access mochi.test when clicked. await testOriginControls(ext3, options, { items: [ACCESS_OPTIONS, WHEN_CLICKED, ALWAYS_ON], selected: 1, attention: true, }); // Has granted. await testOriginControls(ext4, options, { items: [ACCESS_OPTIONS, ALL_SITES], selected: 1, attention: false, }); // MV2 extension, has no origin controls, and never flags for attention. await testOriginControls(ext5, options, { items: [], attention: false }); if (unifiedButton) { ok( unifiedButton.hasAttribute("attention"), "Both ext2 and ext3 are WHEN_CLICKED for example.com, so show attention indicator." ); Assert.deepEqual( document.l10n.getAttributes(unifiedButton), UNIFIED_ATTENTION, "UEB has permissions needed tooltip." ); } }); info("Testing again with mochi.test now quarantined."); await SpecialPowers.pushPrefEnv({ set: [ ["extensions.quarantinedDomains.enabled", true], ["extensions.quarantinedDomains.list", "mochi.test"], ], }); await BrowserTestUtils.withNewTab("http://mochi.test:8888/", async () => { await testOriginControls(ext1, options, { items: [NO_ACCESS] }); await testOriginControls(ext2, options, { items: [QUARANTINED] }); await testOriginControls(ext3, options, { items: [QUARANTINED] }); await testOriginControls(ext4, options, { items: [QUARANTINED] }); // MV2 normally don't have controls, but we show the quarantined status. await testOriginControls(ext5, options, { items: [QUARANTINED] }); }); await SpecialPowers.popPrefEnv(); await BrowserTestUtils.withNewTab("http://example.com/", async () => { const ALWAYS_ON = { id: "origin-controls-option-always-on", args: { domain: "example.com" }, }; await testOriginControls(ext1, options, { items: [NO_ACCESS] }); // Click alraedy selected options, expect no permission changes. await testOriginControls(ext2, options, { items: [ACCESS_OPTIONS, WHEN_CLICKED, ALWAYS_ON], selected: 1, click: 1, attention: true, }); await testOriginControls(ext3, options, { items: [ACCESS_OPTIONS, WHEN_CLICKED, ALWAYS_ON], selected: 2, click: 2, attention: false, }); await testOriginControls(ext4, options, { items: [ACCESS_OPTIONS, ALL_SITES], selected: 1, click: 1, attention: false, }); await testOriginControls(ext5, options, { items: [], attention: false }); if (unifiedButton) { ok( unifiedButton.hasAttribute("attention"), "ext2 is WHEN_CLICKED for example.com, show attention indicator." ); Assert.deepEqual( document.l10n.getAttributes(unifiedButton), UNIFIED_ATTENTION, "UEB attention for only one extension." ); } // Click the other option, expect example.com permission granted/revoked. await testOriginControls(ext2, options, { items: [ACCESS_OPTIONS, WHEN_CLICKED, ALWAYS_ON], selected: 1, click: 2, granted: ["*://example.com/*"], attention: true, }); if (unifiedButton) { ok( !unifiedButton.hasAttribute("attention"), "Bot ext2 and ext3 are ALWAYS_ON for example.com, so no attention indicator." ); Assert.deepEqual( document.l10n.getAttributes(unifiedButton), UNIFIED_NO_ATTENTION, "Unified button has no permissions needed tooltip." ); } await testOriginControls(ext3, options, { items: [ACCESS_OPTIONS, WHEN_CLICKED, ALWAYS_ON], selected: 2, click: 1, revoked: ["*://example.com/*"], attention: false, }); if (unifiedButton) { ok( unifiedButton.hasAttribute("attention"), "ext3 is now WHEN_CLICKED for example.com, show attention indicator." ); Assert.deepEqual( document.l10n.getAttributes(unifiedButton), UNIFIED_ATTENTION, "UEB attention for only one extension." ); } // Other option is now selected. await testOriginControls(ext2, options, { items: [ACCESS_OPTIONS, WHEN_CLICKED, ALWAYS_ON], selected: 2, attention: false, }); await testOriginControls(ext3, options, { items: [ACCESS_OPTIONS, WHEN_CLICKED, ALWAYS_ON], selected: 1, attention: true, }); if (unifiedButton) { ok( unifiedButton.hasAttribute("attention"), "Still showing the attention indicator." ); Assert.deepEqual( document.l10n.getAttributes(unifiedButton), UNIFIED_ATTENTION, "UEB attention for only one extension." ); } }); await Promise.all(extensions.map(e => e.unload())); }; add_task(async function originControls_in_browserAction_contextMenu() { await originControlsInContextMenu({ contextMenuId: "toolbar-context-menu" }); }); add_task(async function originControls_in_unifiedExtensions_contextMenu() { await originControlsInContextMenu({ contextMenuId: "unified-extensions-context-menu", }); }); add_task(async function test_attention_dot_when_pinning_extension() { const extension = await makeExtension({ permissions: ["activeTab"] }); await extension.startup(); const unifiedButton = document.querySelector("#unified-extensions-button"); const extensionWidgetID = AppUiTestInternals.getBrowserActionWidgetId( extension.id ); const extensionWidget = CustomizableUI.getWidget(extensionWidgetID).forWindow(window).node; await BrowserTestUtils.withNewTab("http://mochi.test:8888/", async () => { // The extensions should be placed in the navbar by default so we do not // expect an attention dot on the Unifed Extensions Button (UEB), only on // the extension (widget) itself. ok( !unifiedButton.hasAttribute("attention"), "expected no attention attribute on the UEB" ); ok( extensionWidget.hasAttribute("attention"), "expected attention attribute on the extension widget" ); // Open the context menu of the extension and unpin the extension. let contextMenu = await openChromeContextMenu( "toolbar-context-menu", `#${CSS.escape(extensionWidgetID)}` ); let pinToToolbar = contextMenu.querySelector( ".customize-context-pinToToolbar" ); ok(pinToToolbar, "expected a 'Pin to Toolbar' menu item"); // Passing the `pinToToolbar` item to `closeChromeContextMenu()` will // activate it before closing the context menu. await closeChromeContextMenu(contextMenu.id, pinToToolbar); ok( unifiedButton.hasAttribute("attention"), "expected attention attribute on the UEB" ); // We still expect the attention dot on the extension. ok( extensionWidget.hasAttribute("attention"), "expected attention attribute on the extension widget" ); // Now let's open the unified extensions panel, and pin the same extension // to the toolbar, which should hide the attention dot on the UEB again. await openExtensionsPanel(); contextMenu = await openUnifiedExtensionsContextMenu(extension.id); pinToToolbar = contextMenu.querySelector( ".unified-extensions-context-menu-pin-to-toolbar" ); ok(pinToToolbar, "expected a 'Pin to Toolbar' menu item"); const hidden = BrowserTestUtils.waitForEvent( gUnifiedExtensions.panel, "popuphidden", true ); contextMenu.activateItem(pinToToolbar); await hidden; ok( !unifiedButton.hasAttribute("attention"), "expected no attention attribute on the UEB" ); // We still expect the attention dot on the extension. ok( extensionWidget.hasAttribute("attention"), "expected attention attribute on the extension widget" ); }); await extension.unload(); }); async function testWithSubmenu(menu, nextItemClassName) { function expectMenuItems() { info("Checking expected menu items."); let [submenu, sep1, ocMessage, sep2, next] = menu.children; is(submenu.tagName, "menu", "First item is a submenu."); is(submenu.label, "submenu", "Submenu has the expected label."); is(sep1.tagName, "menuseparator", "Second item is a separator."); let l10n = menu.ownerDocument.l10n.getAttributes(ocMessage); is(ocMessage.tagName, "menuitem", "Third is origin controls message."); is(l10n.id, "origin-controls-no-access", "Expected l10n id."); is(sep2.tagName, "menuseparator", "Fourth item is a separator."); is(next.className, nextItemClassName, "All items accounted for."); } // Repeat a few times. for (let i = 0; i < 3; i++) { expectMenuItems(); let shown = BrowserTestUtils.waitForEvent(menu, "popupshown"); menu.children[0].click(); let popup = (await shown).target; expectMenuItems(); let closed = promiseContextMenuClosed(popup); popup.hidePopup(); await closed; } menu.hidePopup(); } add_task(async function test_originControls_with_submenus() { if (AppConstants.platform === "macosx") { ok(true, "Probably some context menus quirks on macOS."); return; } let extension = await makeExtension({ id: "submenus@test", permissions: ["menus"], }); await BrowserTestUtils.withNewTab("about:blank", async () => { info(`Testing with submenus.`); moveWidget(extension, true); let target = `#${CSS.escape(makeWidgetId(extension.id))}-BAP`; await testWithSubmenu( await openChromeContextMenu("toolbar-context-menu", target), "customize-context-manageExtension" ); info(`Testing with submenus inside extensions panel.`); moveWidget(extension, false); await openExtensionsPanel(); await testWithSubmenu( await openUnifiedExtensionsContextMenu(extension.id), "unified-extensions-context-menu-pin-to-toolbar" ); }); await extension.unload(); });