summaryrefslogtreecommitdiffstats
path: root/browser/components/extensions/test/browser/browser_ext_menus_replace_menu.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/extensions/test/browser/browser_ext_menus_replace_menu.js')
-rw-r--r--browser/components/extensions/test/browser/browser_ext_menus_replace_menu.js525
1 files changed, 525 insertions, 0 deletions
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 = `<a href="http://example.com/">Link</a>`;
+ 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": `
+ <!DOCTYPE html><meta charset="utf-8">
+ <a href="http://example.com/">Link</a>
+ <p>Some text</p>
+ <div id="shadowHost"></div>
+ <script src="tab.js"></script>
+ `,
+ "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": `
+ <!DOCTYPE html><meta charset="utf-8">
+ <a href="http://example.com/">Link</a>
+ <script src="sidebar.js"></script>
+ `,
+ "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();
+});