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_menus_replace_menu.js | 525 +++++++++++++++++++++ 1 file changed, 525 insertions(+) create mode 100644 browser/components/extensions/test/browser/browser_ext_menus_replace_menu.js (limited to 'browser/components/extensions/test/browser/browser_ext_menus_replace_menu.js') diff --git a/browser/components/extensions/test/browser/browser_ext_menus_replace_menu.js b/browser/components/extensions/test/browser/browser_ext_menus_replace_menu.js new file mode 100644 index 0000000000..c430b6ad71 --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_menus_replace_menu.js @@ -0,0 +1,525 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +function getVisibleChildrenIds(menuElem) { + return Array.from(menuElem.children) + .filter(elem => !elem.hidden) + .map(elem => (elem.tagName == "menuseparator" ? elem.tagName : elem.id)); +} + +function checkIsLinkMenuItemVisible(visibleMenuItemIds) { + // In most of this test file, we open a menu on a link. Assume that all + // relevant menu items are shown if one link-specific menu item is shown. + ok( + visibleMenuItemIds.includes("context-openlink"), + `The default 'Open Link in New Tab' menu item should be in ${visibleMenuItemIds}.` + ); +} + +// Tests the following: +// - Calling overrideContext({}) during oncontextmenu forces the menu to only +// show an extension's own items. +// - These menu items all appear in the root menu. +// - The usual extension filtering behavior (e.g. documentUrlPatterns and +// targetUrlPatterns) is still applied; some menu items are therefore hidden. +// - Calling overrideContext({showDefaults:true}) causes the default menu items +// to be shown, but only after the extension's. +// - overrideContext expires after the menu is opened once. +// - overrideContext can be called from shadow DOM. +add_task(async function overrideContext_in_extension_tab() { + await SpecialPowers.pushPrefEnv({ + set: [["security.allow_eval_with_system_principal", true]], + }); + + function extensionTabScript() { + document.addEventListener( + "contextmenu", + () => { + browser.menus.overrideContext({}); + browser.test.sendMessage("oncontextmenu_in_dom_part_1"); + }, + { once: true } + ); + + let shadowRoot = document + .getElementById("shadowHost") + .attachShadow({ mode: "open" }); + shadowRoot.innerHTML = `Link`; + shadowRoot.firstChild.addEventListener( + "contextmenu", + () => { + browser.menus.overrideContext({}); + browser.test.sendMessage("oncontextmenu_in_shadow_dom"); + }, + { once: true } + ); + + document.querySelector("p").addEventListener( + "contextmenu", + () => { + browser.menus.overrideContext({ showDefaults: true }); + }, + { once: true } + ); + + browser.menus.create({ + id: "tab_1", + title: "tab_1", + documentUrlPatterns: [document.URL], + onclick() { + document.addEventListener( + "contextmenu", + () => { + // Verifies that last call takes precedence. + browser.menus.overrideContext({ showDefaults: false }); + browser.menus.overrideContext({ showDefaults: true }); + browser.test.sendMessage("oncontextmenu_in_dom_part_2"); + }, + { once: true } + ); + browser.test.sendMessage("onClicked_tab_1"); + }, + }); + browser.menus.create( + { + id: "tab_2", + title: "tab_2", + onclick() { + browser.test.sendMessage("onClicked_tab_2"); + }, + }, + () => { + browser.test.sendMessage("menu-registered"); + } + ); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["menus", "menus.overrideContext"], + }, + files: { + "tab.html": ` + + Link +

Some text

+
+ + `, + "tab.js": extensionTabScript, + }, + background() { + // Expected to match and thus be visible. + browser.menus.create({ id: "bg_1", title: "bg_1" }); + browser.menus.create({ + id: "bg_2", + title: "bg_2", + targetUrlPatterns: ["*://example.com/*"], + }); + + // Expected to not match and be hidden. + browser.menus.create({ + id: "bg_3", + title: "bg_3", + targetUrlPatterns: ["*://nomatch/*"], + }); + browser.menus.create({ + id: "bg_4", + title: "bg_4", + documentUrlPatterns: [document.URL], + }); + + browser.menus.onShown.addListener(info => { + browser.test.assertEq("tab", info.viewType, "Expected viewType"); + let sortedContexts = info.contexts.sort().join(","); + if (info.contexts.includes("link")) { + browser.test.assertEq( + "bg_1,bg_2,tab_1,tab_2", + info.menuIds.join(","), + "Expected menu items." + ); + browser.test.assertEq( + "all,link", + sortedContexts, + "Expected menu contexts" + ); + } else if (info.contexts.includes("page")) { + browser.test.assertEq( + "bg_1,tab_1,tab_2", + info.menuIds.join(","), + "Expected menu items." + ); + browser.test.assertEq( + "all,page", + sortedContexts, + "Expected menu contexts" + ); + } else { + browser.test.fail(`Unexpected menu context: ${sortedContexts}`); + } + browser.test.sendMessage("onShown"); + }); + + browser.tabs.create({ url: "tab.html" }); + }, + }); + + let otherExtension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["menus"], + }, + background() { + browser.menus.create( + { id: "other_extension_item", title: "other_extension_item" }, + () => { + browser.test.sendMessage("other_extension_item_created"); + } + ); + }, + }); + await otherExtension.startup(); + await otherExtension.awaitMessage("other_extension_item_created"); + + let extensionTabPromise = BrowserTestUtils.waitForNewTab( + gBrowser, + null, + true + ); + await extension.startup(); + // Must wait for the tab to have loaded completely before calling openContextMenu. + await extensionTabPromise; + await extension.awaitMessage("menu-registered"); + + const EXPECTED_EXTENSION_MENU_IDS = [ + `${makeWidgetId(extension.id)}-menuitem-_bg_1`, + `${makeWidgetId(extension.id)}-menuitem-_bg_2`, + `${makeWidgetId(extension.id)}-menuitem-_tab_1`, + `${makeWidgetId(extension.id)}-menuitem-_tab_2`, + ]; + + const EXPECTED_EXTENSION_MENU_IDS_NOLINK = [ + `${makeWidgetId(extension.id)}-menuitem-_bg_1`, + `${makeWidgetId(extension.id)}-menuitem-_tab_1`, + `${makeWidgetId(extension.id)}-menuitem-_tab_2`, + ]; + const OTHER_EXTENSION_MENU_ID = `${makeWidgetId( + otherExtension.id + )}-menuitem-_other_extension_item`; + + { + // Tests overrideContext({}) + info("Expecting the menu to be replaced by overrideContext."); + let menu = await openContextMenu("a"); + await extension.awaitMessage("oncontextmenu_in_dom_part_1"); + await extension.awaitMessage("onShown"); + + Assert.deepEqual( + getVisibleChildrenIds(menu), + EXPECTED_EXTENSION_MENU_IDS, + "Expected only extension menu items" + ); + + let menuItems = menu.getElementsByAttribute("label", "tab_1"); + await closeExtensionContextMenu(menuItems[0]); + await extension.awaitMessage("onClicked_tab_1"); + } + + { + // Tests overrideContext({showDefaults:true})) + info( + "Expecting the menu to be replaced by overrideContext, including default menu items." + ); + let menu = await openContextMenu("a"); + await extension.awaitMessage("oncontextmenu_in_dom_part_2"); + await extension.awaitMessage("onShown"); + + let visibleMenuItemIds = getVisibleChildrenIds(menu); + Assert.deepEqual( + visibleMenuItemIds.slice(0, EXPECTED_EXTENSION_MENU_IDS.length), + EXPECTED_EXTENSION_MENU_IDS, + "Expected extension menu items at the start." + ); + + checkIsLinkMenuItemVisible(visibleMenuItemIds); + + is( + visibleMenuItemIds[visibleMenuItemIds.length - 1], + OTHER_EXTENSION_MENU_ID, + "Other extension menu item should be at the end." + ); + + let menuItems = menu.getElementsByAttribute("label", "tab_2"); + await closeExtensionContextMenu(menuItems[0]); + await extension.awaitMessage("onClicked_tab_2"); + } + + { + // Tests that previous overrideContext call has been forgotten, + // so the default behavior should occur (=move items into submenu). + info( + "Expecting the default menu to be used when overrideContext is not called." + ); + let menu = await openContextMenu("a"); + await extension.awaitMessage("onShown"); + + checkIsLinkMenuItemVisible(getVisibleChildrenIds(menu)); + + let menuItems = menu.getElementsByAttribute("ext-type", "top-level-menu"); + is(menuItems.length, 1, "Expected top-level menu element for extension."); + let topLevelExtensionMenuItem = menuItems[0]; + is( + topLevelExtensionMenuItem.nextSibling, + null, + "Extension menu should be the last element." + ); + + const submenu = await openSubmenu(topLevelExtensionMenuItem); + is(submenu, topLevelExtensionMenuItem.menupopup, "Correct submenu opened"); + + Assert.deepEqual( + getVisibleChildrenIds(submenu), + EXPECTED_EXTENSION_MENU_IDS, + "Extension menu items should be in the submenu by default." + ); + + await closeContextMenu(); + } + + { + // Tests that overrideContext({}) can be used from a listener inside shadow DOM. + let menu = await openContextMenu( + () => this.document.getElementById("shadowHost").shadowRoot.firstChild + ); + await extension.awaitMessage("oncontextmenu_in_shadow_dom"); + await extension.awaitMessage("onShown"); + + Assert.deepEqual( + getVisibleChildrenIds(menu), + EXPECTED_EXTENSION_MENU_IDS, + "Expected only extension menu items after overrideContext({}) in shadow DOM" + ); + + await closeContextMenu(); + } + + { + // Tests overrideContext({showDefaults:true}) on a non-link + info( + "Expecting overrideContext to insert items after the navigation group." + ); + let menu = await openContextMenu("p"); + await extension.awaitMessage("onShown"); + + let visibleMenuItemIds = getVisibleChildrenIds(menu); + if (AppConstants.platform == "macosx") { + // On mac, the items should be at the top: + Assert.deepEqual( + visibleMenuItemIds.slice(0, EXPECTED_EXTENSION_MENU_IDS_NOLINK.length), + EXPECTED_EXTENSION_MENU_IDS_NOLINK, + "Expected extension menu items at the start." + ); + } else { + // Elsewhere, they should be immediately after the navigation group: + Assert.deepEqual( + visibleMenuItemIds.slice( + 0, + 2 + EXPECTED_EXTENSION_MENU_IDS_NOLINK.length + ), + [ + "context-navigation", + "menuseparator", + ...EXPECTED_EXTENSION_MENU_IDS_NOLINK, + ], + "Expected extension menu items immmediately after navigation items." + ); + } + ok( + visibleMenuItemIds.includes("context-savepage"), + "Default menu items should be there." + ); + + is( + visibleMenuItemIds[visibleMenuItemIds.length - 1], + OTHER_EXTENSION_MENU_ID, + "Other extension menu item should be at the end." + ); + + await closeContextMenu(); + } + + // Unloading the extension will automatically close the extension's tab.html + await extension.unload(); + await otherExtension.unload(); +}); + +// Tests some edge cases: +// - overrideContext() is called without any menu registrations, +// followed by a menu registration + menus.refresh.. +// - overrideContext() is called and event.preventDefault() is also +// called to stop the menu from appearing. +// - Open menu again and verify that the default menu behavior occurs. +add_task(async function overrideContext_sidebar_edge_cases() { + function sidebarJs() { + const TIME_BEFORE_MENU_SHOWN = Date.now(); + let count = 0; + // eslint-disable-next-line mozilla/balanced-listeners + document.addEventListener("contextmenu", event => { + ++count; + if (count === 1) { + browser.menus.overrideContext({}); + } else if (count === 2) { + browser.menus.overrideContext({}); + event.preventDefault(); // Prevent menu from being shown. + + // We are not expecting a menu. Wait for the time it took to show and + // hide the previous menu, to check that no new menu appears. + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + setTimeout(() => { + browser.test.sendMessage( + "stop_waiting_for_menu_shown", + "timer_reached" + ); + }, Date.now() - TIME_BEFORE_MENU_SHOWN); + } else if (count === 3) { + // The overrideContext from the previous call should be forgotten. + // Use the default behavior, i.e. show the default menu. + } else { + browser.test.fail(`Unexpected menu count: ${count}`); + } + + browser.test.sendMessage("oncontextmenu_in_dom"); + }); + + browser.menus.onShown.addListener(info => { + browser.test.assertEq("sidebar", info.viewType, "Expected viewType"); + if (count === 1) { + browser.test.assertEq("", info.menuIds.join(","), "Expected no items"); + browser.menus.create({ id: "some_item", title: "some_item" }, () => { + browser.test.sendMessage("onShown_1_and_menu_item_created"); + }); + } else if (count === 2) { + browser.test.fail( + "onShown should not have fired when the menu is not shown." + ); + } else if (count === 3) { + browser.test.assertEq( + "some_item", + info.menuIds.join(","), + "Expected menu item" + ); + browser.test.sendMessage("onShown_3"); + } else { + browser.test.fail(`Unexpected onShown at count: ${count}`); + } + }); + + browser.test.onMessage.addListener(async msg => { + browser.test.assertEq("refresh_menus", msg, "Expected message"); + browser.test.assertEq(1, count, "Expected at first menu test"); + await browser.menus.refresh(); + browser.test.sendMessage("menus_refreshed"); + }); + + browser.menus.onHidden.addListener(() => { + browser.test.sendMessage("onHidden", count); + }); + + browser.test.sendMessage("sidebar_ready"); + } + let extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "temporary", // To automatically show sidebar on load. + manifest: { + permissions: ["menus", "menus.overrideContext"], + sidebar_action: { + default_panel: "sidebar.html", + }, + }, + files: { + "sidebar.html": ` + + Link + + `, + "sidebar.js": sidebarJs, + }, + background() { + browser.test.assertThrows( + () => { + browser.menus.overrideContext({ someInvalidParameter: true }); + }, + /Unexpected property "someInvalidParameter"/, + "overrideContext should be available and the parameters be validated." + ); + browser.test.sendMessage("bg_test_done"); + }, + }); + + await extension.startup(); + await extension.awaitMessage("bg_test_done"); + await extension.awaitMessage("sidebar_ready"); + + const EXPECTED_EXTENSION_MENU_ID = `${makeWidgetId( + extension.id + )}-menuitem-_some_item`; + + { + // Checks that a menu can initially be empty and be updated. + info( + "Expecting menu without items to appear and be updated after menus.refresh()" + ); + let menu = await openContextMenuInSidebar("a"); + await extension.awaitMessage("oncontextmenu_in_dom"); + await extension.awaitMessage("onShown_1_and_menu_item_created"); + Assert.deepEqual( + getVisibleChildrenIds(menu), + [], + "Expected no items, initially" + ); + extension.sendMessage("refresh_menus"); + await extension.awaitMessage("menus_refreshed"); + Assert.deepEqual( + getVisibleChildrenIds(menu), + [EXPECTED_EXTENSION_MENU_ID], + "Expected updated menu" + ); + await closeContextMenu(menu); + is(await extension.awaitMessage("onHidden"), 1, "Menu hidden"); + } + + { + // Trigger a context menu. The page has prevented the menu from being + // shown, so the promise should not resolve. + info("Expecting menu to not appear because of event.preventDefault()"); + let popupShowingPromise = openContextMenuInSidebar("a"); + await extension.awaitMessage("oncontextmenu_in_dom"); + is( + await Promise.race([ + extension.awaitMessage("stop_waiting_for_menu_shown"), + popupShowingPromise.then(() => "popup_shown"), + ]), + "timer_reached", + "The menu should not be shown." + ); + } + + { + info( + "Expecting default menu to be shown when the menu is reopened after event.preventDefault()" + ); + let menu = await openContextMenuInSidebar("a"); + await extension.awaitMessage("oncontextmenu_in_dom"); + await extension.awaitMessage("onShown_3"); + let visibleMenuItemIds = getVisibleChildrenIds(menu); + checkIsLinkMenuItemVisible(visibleMenuItemIds); + ok( + visibleMenuItemIds.includes(EXPECTED_EXTENSION_MENU_ID), + "Expected extension menu item" + ); + await closeContextMenu(menu); + is(await extension.awaitMessage("onHidden"), 3, "Menu hidden"); + } + + await extension.unload(); +}); -- cgit v1.2.3