diff --git a/browser/components/extensions/test/browser/browser-common.ini b/browser/components/extensions/test/browser/browser-common.ini index 61b468e304b8..6f0f717e4867 100644 --- a/browser/components/extensions/test/browser/browser-common.ini +++ b/browser/components/extensions/test/browser/browser-common.ini @@ -116,6 +116,7 @@ skip-if = (verify && (os == 'linux' || os == 'mac')) [browser_ext_menus_event_order.js] [browser_ext_menus_events.js] [browser_ext_menus_refresh.js] +[browser_ext_menus_replace_menu.js] [browser_ext_menus_targetElement.js] [browser_ext_menus_targetElement_extension.js] [browser_ext_menus_targetElement_shadow.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 000000000000..c30f16c9ddfc --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_menus_replace_menu.js @@ -0,0 +1,325 @@ +/* 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.id || elem.tagName); +} + +function checkIsDefaultMenuItemVisible(visibleMenuItemIds) { + // In this whole test file, we open a menu on a link. Assume that all + // default 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. +add_task(async function overrideContext_in_extension_tab() { + function extensionTabScript() { + document.addEventListener("contextmenu", () => { + browser.menus.overrideContext({}); + browser.test.sendMessage("oncontextmenu_in_dom_part_1"); + }, {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"], + }, + files: { + "tab.html": ` + + Link + + `, + "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("bg_1,bg_2,tab_1,tab_2", info.menuIds.join(","), "Expected menu items."); + 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 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."); + + checkIsDefaultMenuItemVisible(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"); + + checkIsDefaultMenuItemVisible(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.firstElementChild, "Correct submenu opened"); + + Assert.deepEqual( + getVisibleChildrenIds(submenu), + EXPECTED_EXTENSION_MENU_IDS, + "Extension menu items should be in the submenu by default."); + + 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 => { + 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"], + 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.assertThrows( + () => { browser.menus.overrideContext({}); }, + /overrideContext must be called during a "contextmenu" event/, + "overrideContext should fail outside of a 'contextmenu' event."); + 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); + checkIsDefaultMenuItemVisible(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(); +});