summaryrefslogtreecommitdiffstats
path: root/browser/components/extensions/test/browser/browser_ext_originControls.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/extensions/test/browser/browser_ext_originControls.js')
-rw-r--r--browser/components/extensions/test/browser/browser_ext_originControls.js640
1 files changed, 640 insertions, 0 deletions
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": `<!DOCTYPE html><meta charset=utf-8>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 <all_urls>, and granted example.com.
+ let ext3 = await makeExtension({
+ id: "ext3@test",
+ host_permissions: ["<all_urls>"],
+ granted: ["*://example.com/*"],
+ useAddonManager: "permanent",
+ });
+
+ // Has granted <all_urls>.
+ let ext4 = await makeExtension({
+ id: "ext4@test",
+ host_permissions: ["<all_urls>"],
+ granted: ["<all_urls>"],
+ useAddonManager: "permanent",
+ });
+
+ // MV2 extension with an <all_urls> content script and activeTab.
+ let ext5 = await makeExtension({
+ manifest_version: 2,
+ id: "ext5@test",
+ permissions: ["activeTab"],
+ content_scripts: [
+ {
+ matches: ["<all_urls>"],
+ 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 <all_urls> 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();
+});