From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- .../test/browser/browser_ext_originControls.js | 640 +++++++++++++++++++++ 1 file changed, 640 insertions(+) create mode 100644 browser/components/extensions/test/browser/browser_ext_originControls.js (limited to 'browser/components/extensions/test/browser/browser_ext_originControls.js') diff --git a/browser/components/extensions/test/browser/browser_ext_originControls.js b/browser/components/extensions/test/browser/browser_ext_originControls.js new file mode 100644 index 0000000000..51e6b4ffed --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_originControls.js @@ -0,0 +1,640 @@ +"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(); +}); -- cgit v1.2.3