Revision control

Copy as Markdown

Other Tools

/* 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("browserContext-copylink"),
`The default 'Copy Link Location' 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]],
});
async function extensionTabScript() {
document.getElementById("link1").addEventListener(
"contextmenu",
() => {
browser.menus.overrideContext({});
browser.test.sendMessage("oncontextmenu_in_dom_part_1");
},
{ once: true }
);
const shadowRoot = document
.getElementById("shadowHost")
.attachShadow({ mode: "open" });
shadowRoot.innerHTML = `<a href="http://example.com/">Link2</a>`;
shadowRoot.firstChild.addEventListener(
"contextmenu",
() => {
browser.menus.overrideContext({});
browser.test.sendMessage("oncontextmenu_in_shadow_dom");
},
{ once: true }
);
await new Promise(resolve =>
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");
},
},
resolve
)
);
await new Promise(resolve =>
browser.menus.create(
{
id: "tab_2",
title: "tab_2",
onclick() {
browser.test.sendMessage("onClicked_tab_2");
},
},
resolve
)
);
browser.test.log("ready");
browser.test.sendMessage("menu-registered");
}
const extension = ExtensionTestUtils.loadExtension({
manifest: {
permissions: ["menus", "menus.overrideContext"],
},
files: {
"tab.html": `<!DOCTYPE html>
<html>
<head>
<title>Popup</title>
<meta charset="utf-8">
<script defer="defer" src="tab.js"></script>
</head>
<body>
<a id="link1" href="http://example.com/">Link1</a>
<div id="shadowHost"></div>
</body>
</html>`,
"tab.js": extensionTabScript,
},
async background() {
// Expected to match and thus be visible.
await new Promise(resolve =>
browser.menus.create({ id: "bg_1", title: "bg_1" }, resolve)
);
await new Promise(resolve =>
browser.menus.create(
{
id: "bg_2",
title: "bg_2",
targetUrlPatterns: ["*://example.com/*"],
},
resolve
)
);
// Expected to not match and be hidden.
await new Promise(resolve =>
browser.menus.create(
{
id: "bg_3",
title: "bg_3",
targetUrlPatterns: ["*://nomatch/*"],
},
resolve
)
);
await new Promise(resolve =>
browser.menus.create(
{
id: "bg_4",
title: "bg_4",
documentUrlPatterns: [document.URL],
},
resolve
)
);
browser.menus.onShown.addListener(info => {
browser.test.assertEq("tab", info.viewType, "Expected viewType");
browser.test.assertEq(
"bg_1,bg_2,tab_1,tab_2",
info.menuIds.join(","),
"Expected menu items."
);
browser.test.assertEq(
"all,link",
info.contexts.sort().join(","),
"Expected menu contexts"
);
browser.test.sendMessage("onShown");
});
browser.tabs.create({ url: "tab.html" });
},
});
const otherExtension = ExtensionTestUtils.loadExtension({
manifest: {
permissions: ["menus"],
},
async background() {
await new Promise(resolve =>
browser.menus.create(
{ id: "other_extension_item", title: "other_extension_item" },
resolve
)
);
browser.test.sendMessage("other_extension_item_created");
},
});
await otherExtension.startup();
await otherExtension.awaitMessage("other_extension_item_created");
await extension.startup();
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.");
const menu = await openBrowserContextMenuInTab("#link1");
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"
);
const menuItems = menu.getElementsByAttribute("label", "tab_1");
await clickItemInMenuPopup(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."
);
const menu = await openBrowserContextMenuInTab("#link1");
await extension.awaitMessage("oncontextmenu_in_dom_part_2");
await extension.awaitMessage("onShown");
const 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."
);
const menuItems = menu.getElementsByAttribute("label", "tab_2");
await clickItemInMenuPopup(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."
);
const menu = await openBrowserContextMenuInTab("#link1");
await extension.awaitMessage("onShown");
checkIsDefaultMenuItemVisible(getVisibleChildrenIds(menu));
const menuItems = menu.getElementsByAttribute("ext-type", "top-level-menu");
is(menuItems.length, 1, "Expected top-level menu element for extension.");
const topLevelExtensionMenuItem = menuItems[0];
is(
topLevelExtensionMenuItem.nextSibling,
null,
"Extension menu should be the last element."
);
const submenu = await openSubMenuPopup(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 closeBrowserContextMenuPopup();
}
{
info(
"Expecting the menu to be replaced by overrideContext from a listener inside shadow DOM."
);
// Tests that overrideContext({}) can be used from a listener inside shadow DOM.
const menu = await openBrowserContextMenuInTab(
() => 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 closeBrowserContextMenuPopup();
}
// Unloading the extension will automatically close the extension's tab.html
await extension.unload();
await otherExtension.unload();
const tabmail = document.getElementById("tabmail");
tabmail.closeTab(tabmail.currentTabInfo);
});
async function run_overrideContext_test_in_popup(testWindow, buttonSelector) {
async function extensionPopupScript() {
document.getElementById("link1").addEventListener(
"contextmenu",
() => {
browser.menus.overrideContext({});
browser.test.sendMessage("oncontextmenu_in_dom_part_1");
},
{ once: true }
);
const shadowRoot = document
.getElementById("shadowHost")
.attachShadow({ mode: "open" });
shadowRoot.innerHTML = `<a href="http://example.com/">Link2</a>`;
shadowRoot.firstChild.addEventListener(
"contextmenu",
() => {
browser.menus.overrideContext({});
browser.test.sendMessage("oncontextmenu_in_shadow_dom");
},
{ once: true }
);
await new Promise(resolve =>
browser.menus.create(
{
id: "popup_1",
title: "popup_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_popup_1");
},
},
resolve
)
);
await new Promise(resolve =>
browser.menus.create(
{
id: "popup_2",
title: "popup_2",
onclick() {
browser.test.sendMessage("onClicked_popup_2");
},
},
resolve
)
);
browser.test.log("ready");
browser.test.sendMessage("menu-registered");
}
const extension = ExtensionTestUtils.loadExtension({
useAddonManager: "temporary",
manifest: {
applications: {
gecko: {
id: `overrideContext@mochi.test`,
},
},
permissions: ["menus", "menus.overrideContext"],
browser_action: {
default_popup: "popup.html",
default_title: "Popup",
},
compose_action: {
default_popup: "popup.html",
default_title: "Popup",
},
message_display_action: {
default_popup: "popup.html",
default_title: "Popup",
},
},
files: {
"popup.html": `<!DOCTYPE html>
<html>
<head>
<title>Popup</title>
<meta charset="utf-8">
<script defer="defer" src="popup.js"></script>
</head>
<body>
<a id="link1" href="http://example.com/">Link1</a>
<div id="shadowHost"></div>
</body>
</html>`,
"popup.js": extensionPopupScript,
},
async background() {
// Expected to match and thus be visible.
await new Promise(resolve =>
browser.menus.create(
{
id: "bg_1",
title: "bg_1",
viewTypes: ["popup"],
},
resolve
)
);
// Expected to not match and be hidden.
await new Promise(resolve =>
browser.menus.create(
{
id: "bg_2",
title: "bg_2",
viewTypes: ["tab"],
},
resolve
)
);
browser.menus.onShown.addListener(info => {
browser.test.assertEq("popup", info.viewType, "Expected viewType");
browser.test.assertEq(
"bg_1,popup_1,popup_2",
info.menuIds.join(","),
"Expected menu items."
);
browser.test.sendMessage("onShown");
});
browser.test.sendMessage("ready");
},
});
await extension.startup();
await extension.awaitMessage("ready");
const EXPECTED_EXTENSION_MENU_IDS = [
`${makeWidgetId(extension.id)}-menuitem-_bg_1`,
`${makeWidgetId(extension.id)}-menuitem-_popup_1`,
`${makeWidgetId(extension.id)}-menuitem-_popup_2`,
];
const button = testWindow.document.querySelector(buttonSelector);
Assert.ok(button, "Button created");
EventUtils.synthesizeMouseAtCenter(button, { clickCount: 1 }, testWindow);
await extension.awaitMessage("menu-registered");
// We received the menu-registered message, so the contextmenu event listeners for
// the two link elements in the popup should be set up by now. But that is not always
// the case. We sometimes see the default context menu and the expected message from
// the popup script is not received.
// eslint-disable-next-line mozilla/no-arbitrary-setTimeout
await new Promise(resolve => testWindow.setTimeout(resolve, 250));
{
// Tests overrideContext({})
info("Expecting the menu to be replaced by overrideContext.");
const menu = await openBrowserContextMenuInActionPopup(
extension,
"#link1",
testWindow
);
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"
);
const menuItems = menu.getElementsByAttribute("label", "popup_1");
await clickItemInMenuPopup(menuItems[0]);
await extension.awaitMessage("onClicked_popup_1");
}
{
// Tests overrideContext({showDefaults:true}))
info(
"Expecting the menu to be replaced by overrideContext, including default menu items."
);
const menu = await openBrowserContextMenuInActionPopup(
extension,
"#link1",
testWindow
);
await extension.awaitMessage("oncontextmenu_in_dom_part_2");
await extension.awaitMessage("onShown");
const 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);
const menuItems = menu.getElementsByAttribute("label", "popup_2");
await clickItemInMenuPopup(menuItems[0]);
await extension.awaitMessage("onClicked_popup_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."
);
const menu = await openBrowserContextMenuInActionPopup(
extension,
"#link1",
testWindow
);
await extension.awaitMessage("onShown");
checkIsDefaultMenuItemVisible(getVisibleChildrenIds(menu));
const menuItems = menu.getElementsByAttribute("ext-type", "top-level-menu");
is(menuItems.length, 1, "Expected top-level menu element for extension.");
const topLevelExtensionMenuItem = menuItems[0];
is(
topLevelExtensionMenuItem.nextSibling,
null,
"Extension menu should be the last element."
);
const submenu = await openSubMenuPopup(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 closeMenuPopup(menu);
}
{
info("Testing overrideContext from a listener inside a shadow DOM.");
// Tests that overrideContext({}) can be used from a listener inside shadow DOM.
const menu = await openBrowserContextMenuInActionPopup(
extension,
() => this.document.getElementById("shadowHost").shadowRoot.firstChild,
testWindow
);
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 closeMenuPopup(menu);
}
await closeBrowserAction(extension, testWindow);
await extension.unload();
}
add_task(async function overrideContext_in_extension_browser_action_popup() {
await run_overrideContext_test_in_popup(
window,
`.unified-toolbar [extension="overrideContext@mochi.test"]`
);
});
add_task(async function overrideContext_in_extension_compose_action_popup() {
const account = createAccount();
addIdentity(account);
const composeWindow = await openComposeWindow(account);
await focusWindow(composeWindow);
await run_overrideContext_test_in_popup(
composeWindow,
"#overridecontext_mochi_test-composeAction-toolbarbutton"
);
composeWindow.close();
});
add_task(
async function overrideContext_in_extension_message_display_action_popup_of_mail3pane() {
const account = createAccount();
addIdentity(account);
const rootFolder = account.incomingServer.rootFolder;
const subFolders = rootFolder.subFolders;
createMessages(subFolders[0], 10);
const about3Pane = document.getElementById("tabmail").currentAbout3Pane;
about3Pane.displayFolder(subFolders[0]);
about3Pane.threadTree.selectedIndex = 0;
await run_overrideContext_test_in_popup(
about3Pane.messageBrowser.contentWindow,
"#overridecontext_mochi_test-messageDisplayAction-toolbarbutton"
);
about3Pane.displayFolder(rootFolder);
}
);
add_task(
async function overrideContext_in_extension_message_display_action_popup_of_window() {
const account = createAccount();
addIdentity(account);
const rootFolder = account.incomingServer.rootFolder;
const subFolders = rootFolder.subFolders;
createMessages(subFolders[0], 10);
const messages = subFolders[0].messages;
const messageWindow = await openMessageInWindow(messages.getNext());
await focusWindow(messageWindow);
await run_overrideContext_test_in_popup(
messageWindow.messageBrowser.contentWindow,
"#overridecontext_mochi_test-messageDisplayAction-toolbarbutton"
);
messageWindow.close();
}
);
add_task(
async function overrideContext_in_extension_message_display_action_popup_of_tab() {
const account = createAccount();
addIdentity(account);
const rootFolder = account.incomingServer.rootFolder;
const subFolders = rootFolder.subFolders;
createMessages(subFolders[0], 10);
const messages = subFolders[0].messages;
await openMessageInTab(messages.getNext());
const tabmail = document.getElementById("tabmail");
await run_overrideContext_test_in_popup(
tabmail.currentAboutMessage,
"#overridecontext_mochi_test-messageDisplayAction-toolbarbutton"
);
tabmail.closeOtherTabs(0);
}
);