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_unified_extensions.js | 1543 ++++++++++++++++++++ 1 file changed, 1543 insertions(+) create mode 100644 browser/components/extensions/test/browser/browser_unified_extensions.js (limited to 'browser/components/extensions/test/browser/browser_unified_extensions.js') diff --git a/browser/components/extensions/test/browser/browser_unified_extensions.js b/browser/components/extensions/test/browser/browser_unified_extensions.js new file mode 100644 index 0000000000..0a889b7b56 --- /dev/null +++ b/browser/components/extensions/test/browser/browser_unified_extensions.js @@ -0,0 +1,1543 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +requestLongerTimeout(2); + +/* import-globals-from ../../../../../toolkit/mozapps/extensions/test/browser/head.js */ + +const { ExtensionPermissions } = ChromeUtils.importESModule( + "resource://gre/modules/ExtensionPermissions.sys.mjs" +); + +loadTestSubscript("head_unified_extensions.js"); + +const openCustomizationUI = async () => { + const customizationReady = BrowserTestUtils.waitForEvent( + gNavToolbox, + "customizationready" + ); + gCustomizeMode.enter(); + await customizationReady; + ok( + CustomizationHandler.isCustomizing(), + "expected customizing mode to be enabled" + ); +}; + +const closeCustomizationUI = async () => { + const afterCustomization = BrowserTestUtils.waitForEvent( + gNavToolbox, + "aftercustomization" + ); + gCustomizeMode.exit(); + await afterCustomization; + ok( + !CustomizationHandler.isCustomizing(), + "expected customizing mode to be disabled" + ); +}; + +add_setup(async function () { + // Make sure extension buttons added to the navbar will not overflow in the + // panel, which could happen when a previous test file resizes the current + // window. + await ensureMaximizedWindow(window); +}); + +add_task(async function test_button_enabled_by_pref() { + const { button } = gUnifiedExtensions; + is(button.hidden, false, "expected button to be visible"); + is( + document + .getElementById("nav-bar") + .getAttribute("unifiedextensionsbuttonshown"), + "true", + "expected attribute on nav-bar" + ); +}); + +add_task(async function test_open_panel_on_button_click() { + const extensions = createExtensions([ + { name: "Extension #1" }, + { name: "Another extension", icons: { 16: "test-icon-16.png" } }, + { + name: "Yet another extension with an icon", + icons: { + 32: "test-icon-32.png", + }, + }, + ]); + await Promise.all(extensions.map(extension => extension.startup())); + + await openExtensionsPanel(); + + let item = getUnifiedExtensionsItem(extensions[0].id); + is( + item.querySelector(".unified-extensions-item-name").textContent, + "Extension #1", + "expected name of the first extension" + ); + is( + item.querySelector(".unified-extensions-item-icon").src, + "chrome://mozapps/skin/extensions/extensionGeneric.svg", + "expected generic icon for the first extension" + ); + Assert.deepEqual( + document.l10n.getAttributes( + item.querySelector(".unified-extensions-item-menu-button") + ), + { + id: "unified-extensions-item-open-menu", + args: { extensionName: "Extension #1" }, + }, + "expected l10n attributes for the first extension" + ); + + item = getUnifiedExtensionsItem(extensions[1].id); + is( + item.querySelector(".unified-extensions-item-name").textContent, + "Another extension", + "expected name of the second extension" + ); + ok( + item + .querySelector(".unified-extensions-item-icon") + .src.endsWith("/test-icon-16.png"), + "expected custom icon for the second extension" + ); + Assert.deepEqual( + document.l10n.getAttributes( + item.querySelector(".unified-extensions-item-menu-button") + ), + { + id: "unified-extensions-item-open-menu", + args: { extensionName: "Another extension" }, + }, + "expected l10n attributes for the second extension" + ); + + item = getUnifiedExtensionsItem(extensions[2].id); + is( + item.querySelector(".unified-extensions-item-name").textContent, + "Yet another extension with an icon", + "expected name of the third extension" + ); + ok( + item + .querySelector(".unified-extensions-item-icon") + .src.endsWith("/test-icon-32.png"), + "expected custom icon for the third extension" + ); + Assert.deepEqual( + document.l10n.getAttributes( + item.querySelector(".unified-extensions-item-menu-button") + ), + { + id: "unified-extensions-item-open-menu", + args: { extensionName: "Yet another extension with an icon" }, + }, + "expected l10n attributes for the third extension" + ); + + await closeExtensionsPanel(); + + await Promise.all(extensions.map(extension => extension.unload())); +}); + +// Verify that the context click doesn't open the panel in addition to the +// context menu. +add_task(async function test_clicks_on_unified_extension_button() { + const extensions = createExtensions([{ name: "Extension #1" }]); + await Promise.all(extensions.map(extension => extension.startup())); + + const { button, panel } = gUnifiedExtensions; + ok(button, "expected button"); + ok(panel, "expected panel"); + + info("open panel with primary click"); + await openExtensionsPanel(); + ok( + panel.getAttribute("panelopen") === "true", + "expected panel to be visible" + ); + await closeExtensionsPanel(); + ok(!panel.hasAttribute("panelopen"), "expected panel to be hidden"); + + info("open context menu with non-primary click"); + const contextMenu = document.getElementById("toolbar-context-menu"); + const popupShownPromise = BrowserTestUtils.waitForEvent( + contextMenu, + "popupshown" + ); + EventUtils.synthesizeMouseAtCenter(button, { + type: "contextmenu", + button: 2, + }); + await popupShownPromise; + ok(!panel.hasAttribute("panelopen"), "expected panel to remain hidden"); + await closeChromeContextMenu(contextMenu.id, null); + + // On MacOS, ctrl-click shouldn't open the panel because this normally opens + // the context menu. We can't test anything on MacOS... + if (AppConstants.platform !== "macosx") { + info("open panel with ctrl-click"); + const listView = getListView(); + const viewShown = BrowserTestUtils.waitForEvent(listView, "ViewShown"); + EventUtils.synthesizeMouseAtCenter(button, { ctrlKey: true }); + await viewShown; + ok( + panel.getAttribute("panelopen") === "true", + "expected panel to be visible" + ); + await closeExtensionsPanel(); + ok(!panel.hasAttribute("panelopen"), "expected panel to be hidden"); + } + + await Promise.all(extensions.map(extension => extension.unload())); +}); + +add_task(async function test_item_shows_the_best_addon_icon() { + const extensions = createExtensions([ + { + name: "Extension with different icons", + icons: { + 16: "test-icon-16.png", + 32: "test-icon-32.png", + 64: "test-icon-64.png", + 96: "test-icon-96.png", + 128: "test-icon-128.png", + }, + }, + ]); + await Promise.all(extensions.map(extension => extension.startup())); + + for (const { resolution, expectedIcon } of [ + { resolution: 2, expectedIcon: "test-icon-64.png" }, + { resolution: 1, expectedIcon: "test-icon-32.png" }, + ]) { + await SpecialPowers.pushPrefEnv({ + set: [["layout.css.devPixelsPerPx", String(resolution)]], + }); + is( + window.devicePixelRatio, + resolution, + "window has the required resolution" + ); + + await openExtensionsPanel(); + + const item = getUnifiedExtensionsItem(extensions[0].id); + const iconSrc = item.querySelector(".unified-extensions-item-icon").src; + ok( + iconSrc.endsWith(expectedIcon), + `expected ${expectedIcon}, got: ${iconSrc}` + ); + + await closeExtensionsPanel(); + await SpecialPowers.popPrefEnv(); + } + + await Promise.all(extensions.map(extension => extension.unload())); +}); + +add_task(async function test_panel_has_a_manage_extensions_button() { + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:robots" }, + async () => { + await openExtensionsPanel(); + + const manageExtensionsButton = getListView().querySelector( + "#unified-extensions-manage-extensions" + ); + ok(manageExtensionsButton, "expected a 'manage extensions' button"); + + const tabPromise = BrowserTestUtils.waitForNewTab( + gBrowser, + "about:addons", + true + ); + const popupHiddenPromise = BrowserTestUtils.waitForEvent( + document, + "popuphidden", + true + ); + + manageExtensionsButton.click(); + + const [tab] = await Promise.all([tabPromise, popupHiddenPromise]); + is( + gBrowser.currentURI.spec, + "about:addons", + "Manage opened about:addons" + ); + is( + gBrowser.selectedBrowser.contentWindow.gViewController.currentViewId, + "addons://list/extension", + "expected about:addons to show the list of extensions" + ); + BrowserTestUtils.removeTab(tab); + } + ); +}); + +add_task(async function test_list_active_extensions_only() { + const arrayOfManifestData = [ + { + name: "hidden addon", + browser_specific_settings: { gecko: { id: "ext1@test" } }, + hidden: true, + }, + { + name: "regular addon", + browser_specific_settings: { gecko: { id: "ext2@test" } }, + hidden: false, + }, + { + name: "disabled addon", + browser_specific_settings: { gecko: { id: "ext3@test" } }, + hidden: false, + }, + { + name: "regular addon with browser action", + browser_specific_settings: { gecko: { id: "ext4@test" } }, + hidden: false, + browser_action: { + default_area: "navbar", + }, + }, + { + manifest_version: 3, + name: "regular mv3 addon with browser action", + browser_specific_settings: { gecko: { id: "ext5@test" } }, + hidden: false, + action: { + default_area: "navbar", + }, + }, + { + name: "regular addon with page action", + browser_specific_settings: { gecko: { id: "ext6@test" } }, + hidden: false, + page_action: {}, + }, + ]; + const extensions = createExtensions(arrayOfManifestData, { + useAddonManager: "temporary", + // Allow all extensions in PB mode by default. + incognitoOverride: "spanning", + }); + // This extension is loaded with a different `incognitoOverride` value to + // make sure it won't show up in a private window. + extensions.push( + ExtensionTestUtils.loadExtension({ + manifest: { + browser_specific_settings: { gecko: { id: "ext7@test" } }, + name: "regular addon with private browsing disabled", + }, + useAddonManager: "temporary", + incognitoOverride: "not_allowed", + }) + ); + + await Promise.all(extensions.map(extension => extension.startup())); + + // Disable the "disabled addon". + let addon2 = await AddonManager.getAddonByID(extensions[2].id); + await addon2.disable(); + + for (const isPrivate of [false, true]) { + info( + `verifying extensions listed in the panel with private browsing ${ + isPrivate ? "enabled" : "disabled" + }` + ); + const aWin = await BrowserTestUtils.openNewBrowserWindow({ + private: isPrivate, + }); + // Make sure extension buttons added to the navbar will not overflow in the + // panel, which could happen when a previous test file resizes the current + // window. + await ensureMaximizedWindow(aWin); + + await openExtensionsPanel(aWin); + + ok( + aWin.gUnifiedExtensions._button.open, + "Expected unified extension panel to be open" + ); + + const hiddenAddonItem = getUnifiedExtensionsItem(extensions[0].id, aWin); + is(hiddenAddonItem, null, "didn't expect an item for a hidden add-on"); + + const regularAddonItem = getUnifiedExtensionsItem(extensions[1].id, aWin); + is( + regularAddonItem.querySelector(".unified-extensions-item-name") + .textContent, + "regular addon", + "expected an item for a regular add-on" + ); + + const disabledAddonItem = getUnifiedExtensionsItem(extensions[2].id, aWin); + is(disabledAddonItem, null, "didn't expect an item for a disabled add-on"); + + const browserActionItem = getUnifiedExtensionsItem(extensions[3].id, aWin); + is( + browserActionItem, + null, + "didn't expect an item for an add-on with browser action placed in the navbar" + ); + + const mv3BrowserActionItem = getUnifiedExtensionsItem( + extensions[4].id, + aWin + ); + is( + mv3BrowserActionItem, + null, + "didn't expect an item for a MV3 add-on with browser action placed in the navbar" + ); + + const pageActionItem = getUnifiedExtensionsItem(extensions[5].id, aWin); + is( + pageActionItem.querySelector(".unified-extensions-item-name").textContent, + "regular addon with page action", + "expected an item for a regular add-on with page action" + ); + + const privateBrowsingDisabledItem = getUnifiedExtensionsItem( + extensions[6].id, + aWin + ); + if (isPrivate) { + is( + privateBrowsingDisabledItem, + null, + "didn't expect an item for a regular add-on with private browsing enabled" + ); + } else { + is( + privateBrowsingDisabledItem.querySelector( + ".unified-extensions-item-name" + ).textContent, + "regular addon with private browsing disabled", + "expected an item for a regular add-on with private browsing disabled" + ); + } + + await closeExtensionsPanel(aWin); + + await BrowserTestUtils.closeWindow(aWin); + } + + await Promise.all(extensions.map(extension => extension.unload())); +}); + +add_task(async function test_button_opens_discopane_when_no_extension() { + // The test harness registers regular extensions so we need to mock the + // `getActivePolicies` extension to simulate zero extensions installed. + const origGetActivePolicies = gUnifiedExtensions.getActivePolicies; + gUnifiedExtensions.getActivePolicies = () => []; + + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:robots" }, + async () => { + const { button } = gUnifiedExtensions; + ok(button, "expected button"); + + // Primary click should open about:addons. + const tabPromise = BrowserTestUtils.waitForNewTab( + gBrowser, + "about:addons", + true + ); + + button.click(); + + const tab = await tabPromise; + is( + gBrowser.currentURI.spec, + "about:addons", + "expected about:addons to be open" + ); + is( + gBrowser.selectedBrowser.contentWindow.gViewController.currentViewId, + "addons://discover/", + "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); + } + ); + + gUnifiedExtensions.getActivePolicies = origGetActivePolicies; +}); + +add_task( + async function test_button_opens_extlist_when_no_extension_and_pane_disabled() { + // If extensions.getAddons.showPane is set to false, there is no "Recommended" tab, + // so we need to make sure we don't navigate to it. + + // The test harness registers regular extensions so we need to mock the + // `getActivePolicies` extension to simulate zero extensions installed. + const origGetActivePolicies = gUnifiedExtensions.getActivePolicies; + gUnifiedExtensions.getActivePolicies = () => []; + + await SpecialPowers.pushPrefEnv({ + set: [ + // Set this to another value to make sure not to "accidentally" land on the right page + ["extensions.ui.lastCategory", "addons://list/theme"], + ["extensions.getAddons.showPane", false], + ], + }); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:robots" }, + async () => { + const { button } = gUnifiedExtensions; + ok(button, "expected button"); + + // Primary click should open about:addons. + const tabPromise = BrowserTestUtils.waitForNewTab( + gBrowser, + "about:addons", + true + ); + + button.click(); + + const tab = await tabPromise; + is( + gBrowser.currentURI.spec, + "about:addons", + "expected about:addons to be open" + ); + is( + gBrowser.selectedBrowser.contentWindow.gViewController.currentViewId, + "addons://list/extension", + "expected about:addons to show the extension list" + ); + BrowserTestUtils.removeTab(tab); + } + ); + + await SpecialPowers.popPrefEnv(); + + gUnifiedExtensions.getActivePolicies = origGetActivePolicies; + } +); + +add_task( + async function test_unified_extensions_panel_not_open_in_customization_mode() { + const listView = getListView(); + ok(listView, "expected list view"); + const throwIfExecuted = () => { + throw new Error("panel should not have been shown"); + }; + listView.addEventListener("ViewShown", throwIfExecuted); + + await openCustomizationUI(); + + const unifiedExtensionsButtonToggled = BrowserTestUtils.waitForEvent( + window, + "UnifiedExtensionsTogglePanel" + ); + const button = document.getElementById("unified-extensions-button"); + + button.click(); + await unifiedExtensionsButtonToggled; + + await closeCustomizationUI(); + + listView.removeEventListener("ViewShown", throwIfExecuted); + } +); + +const NO_ACCESS = { id: "origin-controls-state-no-access", args: null }; +const QUARANTINED = { id: "origin-controls-state-quarantined", args: null }; + +const ALWAYS_ON = { id: "origin-controls-state-always-on", args: null }; +const WHEN_CLICKED = { id: "origin-controls-state-when-clicked", args: null }; +const TEMP_ACCESS = { + id: "origin-controls-state-temporary-access", + args: null, +}; + +const HOVER_RUN_VISIT_ONLY = { + id: "origin-controls-state-hover-run-visit-only", + args: null, +}; +const HOVER_RUNNABLE_RUN_EXT = { + id: "origin-controls-state-runnable-hover-run", + args: null, +}; +const HOVER_RUNNABLE_OPEN_EXT = { + id: "origin-controls-state-runnable-hover-open", + args: null, +}; + +add_task(async function test_messages_origin_controls() { + const TEST_CASES = [ + { + title: "MV2 - no access", + manifest: { + manifest_version: 2, + }, + expectedDefaultMessage: NO_ACCESS, + expectedHoverMessage: NO_ACCESS, + expectedActionButtonDisabled: true, + }, + { + title: "MV2 - always on", + manifest: { + manifest_version: 2, + host_permissions: ["*://example.com/*"], + }, + expectedDefaultMessage: ALWAYS_ON, + expectedHoverMessage: ALWAYS_ON, + expectedActionButtonDisabled: true, + }, + { + title: "MV2 - content script", + manifest: { + manifest_version: 2, + content_scripts: [ + { + js: ["script.js"], + matches: ["*://example.com/*"], + }, + ], + }, + expectedDefaultMessage: ALWAYS_ON, + expectedHoverMessage: ALWAYS_ON, + expectedActionButtonDisabled: true, + }, + { + title: "MV2 - non-matching content script", + manifest: { + manifest_version: 2, + content_scripts: [ + { + js: ["script.js"], + matches: ["*://foobar.net/*"], + }, + ], + }, + expectedDefaultMessage: NO_ACCESS, + expectedHoverMessage: NO_ACCESS, + expectedActionButtonDisabled: true, + }, + { + title: "MV2 - all_urls content script", + manifest: { + manifest_version: 2, + content_scripts: [ + { + js: ["script.js"], + matches: [""], + }, + ], + }, + expectedDefaultMessage: ALWAYS_ON, + expectedHoverMessage: ALWAYS_ON, + expectedActionButtonDisabled: true, + }, + { + title: "MV2 - activeTab without browser action", + manifest: { + manifest_version: 2, + permissions: ["activeTab"], + }, + expectedDefaultMessage: NO_ACCESS, + expectedHoverMessage: NO_ACCESS, + expectedActionButtonDisabled: true, + }, + { + title: "MV2 - when clicked: activeTab with browser action", + manifest: { + manifest_version: 2, + permissions: ["activeTab"], + browser_action: {}, + }, + expectedDefaultMessage: WHEN_CLICKED, + expectedHoverMessage: HOVER_RUN_VISIT_ONLY, + expectedActionButtonDisabled: false, + }, + { + title: "MV3 - when clicked: activeTab with action", + manifest: { + manifest_version: 3, + permissions: ["activeTab"], + action: {}, + }, + expectedDefaultMessage: WHEN_CLICKED, + expectedHoverMessage: HOVER_RUN_VISIT_ONLY, + expectedActionButtonDisabled: false, + }, + { + title: "MV2 - browser action - click event - always on", + manifest: { + manifest_version: 2, + browser_action: {}, + host_permissions: ["*://example.com/*"], + }, + expectedDefaultMessage: ALWAYS_ON, + expectedHoverMessage: HOVER_RUNNABLE_RUN_EXT, + expectedActionButtonDisabled: false, + }, + { + title: "MV2 - browser action - popup - always on", + manifest: { + manifest_version: 2, + browser_action: { + default_popup: "popup.html", + }, + host_permissions: ["*://example.com/*"], + }, + expectedDefaultMessage: ALWAYS_ON, + expectedHoverMessage: HOVER_RUNNABLE_OPEN_EXT, + expectedActionButtonDisabled: false, + }, + { + title: "MV2 - browser action - click event - content script", + manifest: { + manifest_version: 2, + browser_action: {}, + content_scripts: [ + { + js: ["script.js"], + matches: ["*://example.com/*"], + }, + ], + }, + expectedDefaultMessage: ALWAYS_ON, + expectedHoverMessage: HOVER_RUNNABLE_RUN_EXT, + expectedActionButtonDisabled: false, + }, + { + title: "MV2 - browser action - popup - content script", + manifest: { + manifest_version: 2, + browser_action: { + default_popup: "popup.html", + }, + content_scripts: [ + { + js: ["script.js"], + matches: ["*://example.com/*"], + }, + ], + }, + expectedDefaultMessage: ALWAYS_ON, + expectedHoverMessage: HOVER_RUNNABLE_OPEN_EXT, + expectedActionButtonDisabled: false, + }, + { + title: "no access", + manifest: { + manifest_version: 3, + }, + expectedDefaultMessage: NO_ACCESS, + expectedHoverMessage: NO_ACCESS, + expectedActionButtonDisabled: true, + }, + { + title: "when clicked with host permissions", + manifest: { + manifest_version: 3, + host_permissions: ["*://example.com/*"], + }, + expectedDefaultMessage: WHEN_CLICKED, + expectedHoverMessage: HOVER_RUN_VISIT_ONLY, + expectedActionButtonDisabled: false, + }, + { + title: "when clicked with host permissions already granted", + manifest: { + manifest_version: 3, + host_permissions: ["*://example.com/*"], + }, + expectedDefaultMessage: ALWAYS_ON, + expectedHoverMessage: ALWAYS_ON, + expectedActionButtonDisabled: true, + grantHostPermissions: true, + }, + { + title: "when clicked", + manifest: { + manifest_version: 3, + permissions: ["activeTab"], + }, + expectedDefaultMessage: WHEN_CLICKED, + expectedHoverMessage: HOVER_RUN_VISIT_ONLY, + expectedActionButtonDisabled: false, + }, + { + title: "page action - no access", + manifest: { + manifest_version: 3, + page_action: {}, + }, + expectedDefaultMessage: NO_ACCESS, + expectedHoverMessage: NO_ACCESS, + expectedActionButtonDisabled: true, + }, + { + title: "page action - when clicked with host permissions", + manifest: { + manifest_version: 3, + host_permissions: ["*://example.com/*"], + page_action: {}, + }, + expectedDefaultMessage: WHEN_CLICKED, + expectedHoverMessage: HOVER_RUN_VISIT_ONLY, + expectedActionButtonDisabled: false, + }, + { + title: "page action - when clicked with host permissions already granted", + manifest: { + manifest_version: 3, + host_permissions: ["*://example.com/*"], + page_action: {}, + }, + expectedDefaultMessage: ALWAYS_ON, + expectedHoverMessage: ALWAYS_ON, + expectedActionButtonDisabled: true, + grantHostPermissions: true, + }, + { + title: "page action - when clicked", + manifest: { + manifest_version: 3, + permissions: ["activeTab"], + page_action: {}, + }, + expectedDefaultMessage: WHEN_CLICKED, + expectedHoverMessage: HOVER_RUN_VISIT_ONLY, + expectedActionButtonDisabled: false, + }, + { + title: "browser action - click event - no access", + manifest: { + manifest_version: 3, + action: {}, + }, + expectedDefaultMessage: NO_ACCESS, + expectedHoverMessage: HOVER_RUNNABLE_RUN_EXT, + expectedActionButtonDisabled: false, + }, + { + title: "browser action - popup - no access", + manifest: { + manifest_version: 3, + action: { + default_popup: "popup.html", + }, + }, + expectedDefaultMessage: NO_ACCESS, + expectedHoverMessage: HOVER_RUNNABLE_OPEN_EXT, + expectedActionButtonDisabled: false, + }, + { + title: "browser action - click event - when clicked", + manifest: { + manifest_version: 3, + action: {}, + host_permissions: ["*://example.com/*"], + }, + expectedDefaultMessage: WHEN_CLICKED, + expectedHoverMessage: HOVER_RUN_VISIT_ONLY, + expectedActionButtonDisabled: false, + }, + { + title: "browser action - popup - when clicked", + manifest: { + manifest_version: 3, + action: { + default_popup: "popup.html", + }, + host_permissions: ["*://example.com/*"], + }, + expectedDefaultMessage: WHEN_CLICKED, + expectedHoverMessage: HOVER_RUN_VISIT_ONLY, + expectedActionButtonDisabled: false, + }, + { + title: + "browser action - click event - when clicked with host permissions already granted", + manifest: { + manifest_version: 3, + action: {}, + host_permissions: ["*://example.com/*"], + }, + expectedDefaultMessage: ALWAYS_ON, + expectedHoverMessage: HOVER_RUNNABLE_RUN_EXT, + expectedActionButtonDisabled: false, + grantHostPermissions: true, + }, + { + title: + "browser action - popup - when clicked with host permissions already granted", + manifest: { + manifest_version: 3, + action: { + default_popup: "popup.html", + }, + host_permissions: ["*://example.com/*"], + }, + expectedDefaultMessage: ALWAYS_ON, + expectedHoverMessage: HOVER_RUNNABLE_OPEN_EXT, + expectedActionButtonDisabled: false, + grantHostPermissions: true, + }, + ]; + + async function runTestCases(testCases) { + await BrowserTestUtils.withNewTab( + { gBrowser, url: "https://example.com/" }, + async () => { + let count = 0; + + for (const { + title, + manifest, + expectedDefaultMessage, + expectedHoverMessage, + expectedActionButtonDisabled, + grantHostPermissions, + } of testCases) { + info(`case: ${title}`); + + const id = `test-origin-controls-${count++}@ext`; + const extension = ExtensionTestUtils.loadExtension({ + manifest: { + name: title, + browser_specific_settings: { gecko: { id } }, + ...manifest, + }, + files: { + "script.js": "", + "popup.html": "", + }, + useAddonManager: "permanent", + }); + + if (grantHostPermissions) { + info("Granting initial permissions."); + await ExtensionPermissions.add(id, { + permissions: [], + origins: manifest.host_permissions, + }); + } + + await extension.startup(); + + // Open the extension panel. + await openExtensionsPanel(); + + const item = getUnifiedExtensionsItem(extension.id); + ok(item, `expected item for ${extension.id}`); + + const messageDeck = item.querySelector( + ".unified-extensions-item-message-deck" + ); + ok(messageDeck, "expected a message deck element"); + + // 1. Verify the default message displayed below the extension's name. + const defaultMessage = item.querySelector( + ".unified-extensions-item-message-default" + ); + ok(defaultMessage, "expected a default message element"); + + Assert.deepEqual( + document.l10n.getAttributes(defaultMessage), + expectedDefaultMessage, + "expected l10n attributes for the default message" + ); + + is( + messageDeck.selectedIndex, + gUnifiedExtensions.MESSAGE_DECK_INDEX_DEFAULT, + "expected selected message in the deck to be the default message" + ); + + // 2. Verify the action button state. + const actionButton = item.querySelector( + ".unified-extensions-item-action-button" + ); + ok(actionButton, "expected an action button"); + is( + actionButton.disabled, + expectedActionButtonDisabled, + `expected action button to be ${ + expectedActionButtonDisabled ? "disabled" : "enabled" + }` + ); + + // 3. Verify the message displayed on hover but only when the action + // button isn't disabled to avoid some test failures. + if (!expectedActionButtonDisabled) { + const hovered = BrowserTestUtils.waitForEvent( + actionButton, + "mouseover" + ); + EventUtils.synthesizeMouseAtCenter(actionButton, { + type: "mouseover", + }); + await hovered; + + const hoverMessage = item.querySelector( + ".unified-extensions-item-message-hover" + ); + ok(hoverMessage, "expected a hover message element"); + + Assert.deepEqual( + document.l10n.getAttributes(hoverMessage), + expectedHoverMessage, + "expected l10n attributes for the message on hover" + ); + + is( + messageDeck.selectedIndex, + gUnifiedExtensions.MESSAGE_DECK_INDEX_HOVER, + "expected selected message in the deck to be the hover message" + ); + } + + await closeExtensionsPanel(); + + // Move cursor elsewhere to avoid issues with previous "hovering". + EventUtils.synthesizeMouseAtCenter(gURLBar.textbox, {}); + + await extension.unload(); + } + } + ); + } + + await runTestCases(TEST_CASES); + + info("Testing again with example.com quarantined."); + await SpecialPowers.pushPrefEnv({ + set: [["extensions.quarantinedDomains.list", "example.com"]], + }); + + await runTestCases([ + { + title: "MV2 - no access", + manifest: { + manifest_version: 2, + }, + expectedDefaultMessage: NO_ACCESS, + expectedHoverMessage: NO_ACCESS, + expectedActionButtonDisabled: true, + }, + { + title: "MV2 - host permission but quarantined", + manifest: { + manifest_version: 2, + host_permissions: ["*://example.com/*"], + }, + expectedDefaultMessage: QUARANTINED, + expectedHoverMessage: QUARANTINED, + expectedActionButtonDisabled: true, + }, + { + title: "MV2 - content script but quarantined", + manifest: { + manifest_version: 2, + content_scripts: [ + { + js: ["script.js"], + matches: ["*://example.com/*"], + }, + ], + }, + expectedDefaultMessage: QUARANTINED, + expectedHoverMessage: QUARANTINED, + expectedActionButtonDisabled: true, + }, + { + title: "MV2 - non-matching content script", + manifest: { + manifest_version: 2, + content_scripts: [ + { + js: ["script.js"], + matches: ["*://foobar.net/*"], + }, + ], + }, + expectedDefaultMessage: NO_ACCESS, + expectedHoverMessage: NO_ACCESS, + expectedActionButtonDisabled: true, + }, + { + title: "MV3 - content script but quarantined", + manifest: { + manifest_version: 2, + content_scripts: [ + { + js: ["script.js"], + matches: ["*://example.com/*"], + }, + ], + host_permissions: ["*://example.com/*"], + }, + expectedDefaultMessage: QUARANTINED, + expectedHoverMessage: QUARANTINED, + expectedActionButtonDisabled: true, + grantHostPermissions: true, + }, + { + title: "MV3 host permissions already granted but quarantined", + manifest: { + manifest_version: 3, + host_permissions: ["*://example.com/*"], + }, + expectedDefaultMessage: QUARANTINED, + expectedHoverMessage: QUARANTINED, + expectedActionButtonDisabled: true, + grantHostPermissions: true, + }, + { + title: "browser action, host permissions already granted, quarantined", + manifest: { + manifest_version: 3, + action: {}, + host_permissions: ["*://example.com/*"], + }, + expectedDefaultMessage: QUARANTINED, + expectedHoverMessage: HOVER_RUNNABLE_RUN_EXT, + expectedActionButtonDisabled: false, + grantHostPermissions: true, + }, + ]); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function test_hover_message_when_button_updates_itself() { + const extension = ExtensionTestUtils.loadExtension({ + manifest: { + manifest_version: 3, + name: "an extension that refreshes its title", + action: {}, + }, + background() { + browser.test.onMessage.addListener(async msg => { + browser.test.assertEq( + "update-button", + msg, + "expected 'update-button' message" + ); + + browser.action.setTitle({ title: "a title" }); + + browser.test.sendMessage(`${msg}-done`); + }); + + browser.test.sendMessage("background-ready"); + }, + useAddonManager: "temporary", + }); + + await extension.startup(); + await extension.awaitMessage("background-ready"); + + await openExtensionsPanel(); + + const item = getUnifiedExtensionsItem(extension.id); + ok(item, "expected item in the panel"); + + const actionButton = item.querySelector( + ".unified-extensions-item-action-button" + ); + ok(actionButton, "expected an action button"); + + const menuButton = item.querySelector(".unified-extensions-item-menu-button"); + ok(menuButton, "expected a menu button"); + + const hovered = BrowserTestUtils.waitForEvent(actionButton, "mouseover"); + EventUtils.synthesizeMouseAtCenter(actionButton, { type: "mouseover" }); + await hovered; + + const messageDeck = item.querySelector( + ".unified-extensions-item-message-deck" + ); + ok(messageDeck, "expected a message deck element"); + + const hoverMessage = item.querySelector( + ".unified-extensions-item-message-hover" + ); + ok(hoverMessage, "expected a hover message element"); + + const expectedL10nAttributes = { + id: "origin-controls-state-runnable-hover-run", + args: null, + }; + Assert.deepEqual( + document.l10n.getAttributes(hoverMessage), + expectedL10nAttributes, + "expected l10n attributes for the hover message" + ); + + is( + messageDeck.selectedIndex, + gUnifiedExtensions.MESSAGE_DECK_INDEX_HOVER, + "expected selected message in the deck to be the hover message" + ); + + extension.sendMessage("update-button"); + await extension.awaitMessage("update-button-done"); + + is( + messageDeck.selectedIndex, + gUnifiedExtensions.MESSAGE_DECK_INDEX_HOVER, + "expected selected message in the deck to remain the same" + ); + + const menuButtonHovered = BrowserTestUtils.waitForEvent( + menuButton, + "mouseover" + ); + EventUtils.synthesizeMouseAtCenter(menuButton, { type: "mouseover" }); + await menuButtonHovered; + + await closeExtensionsPanel(); + + // Move cursor to the center of the entire browser UI to avoid issues with + // other focus/hover checks. We do this to avoid intermittent test failures. + EventUtils.synthesizeMouseAtCenter(document.documentElement, {}); + + await extension.unload(); +}); + +// Test the temporary access state messages and attention indicator. +add_task(async function test_temporary_access() { + const TEST_CASES = [ + { + title: "mv3 with active scripts and browser action", + manifest: { + manifest_version: 3, + action: {}, + content_scripts: [ + { + js: ["script.js"], + matches: ["*://example.com/*"], + }, + ], + }, + before: { + attention: true, + state: WHEN_CLICKED, + disabled: false, + }, + messages: ["action-onClicked", "cs-injected"], + after: { + attention: false, + state: TEMP_ACCESS, + disabled: false, + }, + }, + { + title: "mv3 with active scripts and no browser action", + manifest: { + manifest_version: 3, + content_scripts: [ + { + js: ["script.js"], + matches: ["*://example.com/*"], + }, + ], + }, + before: { + attention: true, + state: WHEN_CLICKED, + disabled: false, + }, + messages: ["cs-injected"], + after: { + attention: false, + state: TEMP_ACCESS, + // TODO: This will need updating for bug 1807835. + disabled: false, + }, + }, + { + title: "mv3 with browser action and host_permission", + manifest: { + manifest_version: 3, + action: {}, + host_permissions: ["*://example.com/*"], + }, + before: { + attention: true, + state: WHEN_CLICKED, + disabled: false, + }, + messages: ["action-onClicked"], + after: { + attention: false, + state: TEMP_ACCESS, + disabled: false, + }, + }, + { + title: "mv3 with browser action no host_permissions", + manifest: { + manifest_version: 3, + action: {}, + }, + before: { + attention: false, + state: NO_ACCESS, + disabled: false, + }, + messages: ["action-onClicked"], + after: { + attention: false, + state: NO_ACCESS, + disabled: false, + }, + }, + // MV2 tests. + { + title: "mv2 with content scripts and browser action", + manifest: { + manifest_version: 2, + browser_action: {}, + content_scripts: [ + { + js: ["script.js"], + matches: ["*://example.com/*"], + }, + ], + }, + before: { + attention: false, + state: ALWAYS_ON, + disabled: false, + }, + messages: ["action-onClicked", "cs-injected"], + after: { + attention: false, + state: ALWAYS_ON, + disabled: false, + }, + }, + { + title: "mv2 with content scripts and no browser action", + manifest: { + manifest_version: 2, + content_scripts: [ + { + js: ["script.js"], + matches: ["*://example.com/*"], + }, + ], + }, + before: { + attention: false, + state: ALWAYS_ON, + disabled: true, + }, + messages: ["cs-injected"], + after: { + attention: false, + state: ALWAYS_ON, + disabled: true, + }, + }, + { + title: "mv2 with browser action and host_permission", + manifest: { + manifest_version: 2, + browser_action: {}, + host_permissions: ["*://example.com/*"], + }, + before: { + attention: false, + state: ALWAYS_ON, + disabled: false, + }, + messages: ["action-onClicked"], + after: { + attention: false, + state: ALWAYS_ON, + disabled: false, + }, + }, + { + title: "mv2 with browser action no host_permissions", + manifest: { + manifest_version: 2, + browser_action: {}, + }, + before: { + attention: false, + state: NO_ACCESS, + disabled: false, + }, + messages: ["action-onClicked"], + after: { + attention: false, + state: NO_ACCESS, + disabled: false, + }, + }, + ]; + + let count = 1; + await Promise.all( + TEST_CASES.map(test => { + let id = `test-temp-access-${count++}@ext`; + test.extension = ExtensionTestUtils.loadExtension({ + manifest: { + name: test.title, + browser_specific_settings: { gecko: { id } }, + ...test.manifest, + }, + files: { + "popup.html": "", + "script.js"() { + browser.test.sendMessage("cs-injected"); + }, + }, + background() { + let action = browser.action ?? browser.browserAction; + action?.onClicked.addListener(() => { + browser.test.sendMessage("action-onClicked"); + }); + }, + useAddonManager: "temporary", + }); + + return test.extension.startup(); + }) + ); + + async function checkButton(extension, expect, click = false) { + await openExtensionsPanel(); + + let item = getUnifiedExtensionsItem(extension.id); + ok(item, `Expected item for ${extension.id}.`); + + let state = item.querySelector(".unified-extensions-item-message-default"); + ok(state, "Expected a default state message element."); + + is( + item.hasAttribute("attention"), + !!expect.attention, + "Expected attention badge." + ); + Assert.deepEqual( + document.l10n.getAttributes(state), + expect.state, + "Expected l10n attributes for the message." + ); + + let button = item.querySelector(".unified-extensions-item-action-button"); + is(button.disabled, !!expect.disabled, "Expect disabled item."); + + // If we should click, and button is not disabled. + if (click && !expect.disabled) { + let onClick = BrowserTestUtils.waitForEvent(button, "click"); + button.click(); + await onClick; + } else { + // Otherwise, just close the panel. + await closeExtensionsPanel(); + } + } + + await BrowserTestUtils.withNewTab( + { gBrowser, url: "https://example.com/" }, + async () => { + for (let { title, extension, before, messages, after } of TEST_CASES) { + info(`Test case: ${title}`); + await checkButton(extension, before, true); + + await Promise.all( + messages.map(msg => { + info(`Waiting for ${msg} from clicking the button.`); + return extension.awaitMessage(msg); + }) + ); + + await checkButton(extension, after); + await extension.unload(); + } + } + ); +}); + +add_task( + async function test_action_and_menu_buttons_css_class_with_new_window() { + const [extension] = createExtensions([ + { + name: "an extension placed in the extensions panel", + browser_action: { + default_area: "menupanel", + }, + }, + ]); + await extension.startup(); + + let aSecondWindow = await BrowserTestUtils.openNewBrowserWindow(); + await ensureMaximizedWindow(aSecondWindow); + + // Open and close the extensions panel in the newly created window to build + // the extensions panel and add the extension widget(s) to it. + await openExtensionsPanel(aSecondWindow); + await closeExtensionsPanel(aSecondWindow); + + for (const { title, win } of [ + { title: "current window", win: window }, + { title: "second window", win: aSecondWindow }, + ]) { + const node = CustomizableUI.getWidget( + AppUiTestInternals.getBrowserActionWidgetId(extension.id) + ).forWindow(win).node; + + let actionButton = node.querySelector( + ".unified-extensions-item-action-button" + ); + ok( + actionButton.classList.contains("subviewbutton"), + `${title} - expected .subviewbutton CSS class on the action button` + ); + ok( + !actionButton.classList.contains("toolbarbutton-1"), + `${title} - expected no .toolbarbutton-1 CSS class on the action button` + ); + let menuButton = node.querySelector( + ".unified-extensions-item-menu-button" + ); + ok( + menuButton.classList.contains("subviewbutton"), + `${title} - expected .subviewbutton CSS class on the menu button` + ); + ok( + !menuButton.classList.contains("toolbarbutton-1"), + `${title} - expected no .toolbarbutton-1 CSS class on the menu button` + ); + } + + await BrowserTestUtils.closeWindow(aSecondWindow); + + await extension.unload(); + } +); -- cgit v1.2.3