Source code

Revision control

Copy as Markdown

Other Tools

Test Info: Warnings

/* 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";
const PAGE =
// Load an extension that has the "menus" permission. The returned Extension
// instance has a `callMenuApi` method to easily call a browser.menus method
// and wait for its result. It also emits the "onShown fired" message whenever
// the menus.onShown event is fired.
// The `getXULElementByMenuId` method returns the XUL element that corresponds
// to the menu item ID from the browser.menus API (if existent, null otherwise).
function loadExtensionWithMenusApi() {
async function background() {
function shownHandler() {
browser.test.sendMessage("onShown fired");
}
browser.menus.onShown.addListener(shownHandler);
browser.test.onMessage.addListener((method, ...params) => {
let result;
if (method === "* remove onShown listener") {
browser.menus.onShown.removeListener(shownHandler);
result = Promise.resolve();
} else if (method === "create") {
result = new Promise(resolve => {
browser.menus.create(params[0], resolve);
});
} else {
result = browser.menus[method](...params);
}
result.then(() => {
browser.test.sendMessage(`${method}-result`);
});
});
}
let extension = ExtensionTestUtils.loadExtension({
background,
manifest: {
browser_action: {
default_area: "navbar",
},
permissions: ["menus"],
},
});
extension.callMenuApi = async function (method, ...params) {
info(`Calling ${method}(${JSON.stringify(params)})`);
extension.sendMessage(method, ...params);
return extension.awaitMessage(`${method}-result`);
};
extension.removeOnShownListener = async function () {
extension.callMenuApi("* remove onShown listener");
};
extension.getXULElementByMenuId = id => {
// Same implementation as elementId getter in ext-menus.js
if (typeof id != "number") {
id = `_${id}`;
}
let xulId = `${makeWidgetId(extension.id)}-menuitem-${id}`;
return document.getElementById(xulId);
};
return extension;
}
// Tests whether browser.menus.refresh works as expected with respect to the
// menu items that are added/updated/removed before/during/after opening a menu:
// - browser.refresh before a menu is shown should not have any effect.
// - browser.refresh while a menu is shown should update the menu.
// - browser.refresh after a menu is hidden should not have any effect.
async function testRefreshMenusWhileVisible({
contexts,
doOpenMenu,
doCloseMenu,
}) {
let extension = loadExtensionWithMenusApi();
await extension.startup();
await extension.callMenuApi("create", {
id: "abc",
title: "first",
contexts,
});
let elem = extension.getXULElementByMenuId("abc");
is(elem, null, "Menu item should not be visible");
// Refresh before a menu is shown - should be noop.
await extension.callMenuApi("refresh");
elem = extension.getXULElementByMenuId("abc");
is(elem, null, "Menu item should still not be visible");
// Open menu and expect menu to be rendered.
await doOpenMenu(extension);
elem = extension.getXULElementByMenuId("abc");
is(elem.getAttribute("label"), "first", "expected label");
await extension.awaitMessage("onShown fired");
// Add new menus, but don't expect them to be rendered yet.
await extension.callMenuApi("update", "abc", { title: "updated first" });
await extension.callMenuApi("create", {
id: "def",
title: "second",
contexts,
});
elem = extension.getXULElementByMenuId("abc");
is(elem.getAttribute("label"), "first", "expected unchanged label");
elem = extension.getXULElementByMenuId("def");
is(elem, null, "Second menu item should not be visible");
// Refresh while a menu is shown - should be updated.
await extension.callMenuApi("refresh");
elem = extension.getXULElementByMenuId("abc");
is(elem.getAttribute("label"), "updated first", "expected updated label");
elem = extension.getXULElementByMenuId("def");
is(elem.getAttribute("label"), "second", "expected second label");
// Update the two menu items again.
await extension.callMenuApi("update", "abc", { enabled: false });
await extension.callMenuApi("update", "def", { enabled: false });
await extension.callMenuApi("refresh");
elem = extension.getXULElementByMenuId("abc");
is(elem.getAttribute("disabled"), "true", "1st menu item should be disabled");
elem = extension.getXULElementByMenuId("def");
is(elem.getAttribute("disabled"), "true", "2nd menu item should be disabled");
// Remove one.
await extension.callMenuApi("remove", "abc");
await extension.callMenuApi("refresh");
elem = extension.getXULElementByMenuId("def");
is(elem.getAttribute("label"), "second", "other menu item should exist");
elem = extension.getXULElementByMenuId("abc");
is(elem, null, "removed menu item should be gone");
// Remove the last one.
await extension.callMenuApi("removeAll");
await extension.callMenuApi("refresh");
elem = extension.getXULElementByMenuId("def");
is(elem, null, "all menu items should be gone");
// At this point all menu items have been removed. Create a new menu item so
// we can confirm that browser.menus.refresh() does not render the menu item
// after the menu has been hidden.
await extension.callMenuApi("create", {
// The menu item with ID "abc" was removed before, so re-using the ID should
// not cause any issues:
id: "abc",
title: "re-used",
contexts,
});
await extension.callMenuApi("refresh");
elem = extension.getXULElementByMenuId("abc");
is(elem.getAttribute("label"), "re-used", "menu item should be created");
await doCloseMenu();
elem = extension.getXULElementByMenuId("abc");
is(elem, null, "menu item must be gone");
// Refresh after menu was hidden - should be noop.
await extension.callMenuApi("refresh");
elem = extension.getXULElementByMenuId("abc");
is(elem, null, "menu item must still be gone");
await extension.unload();
}
// Check that one extension calling refresh() doesn't interfere with others.
// When expectOtherItems == false, the other extension's menu items should not
// show at all (e.g. for browserAction).
async function testRefreshOther({
contexts,
doOpenMenu,
doCloseMenu,
expectOtherItems,
}) {
let extension = loadExtensionWithMenusApi();
let other_extension = loadExtensionWithMenusApi();
await extension.startup();
await other_extension.startup();
await extension.callMenuApi("create", {
id: "action_item",
title: "visible menu item",
contexts: contexts,
});
await other_extension.callMenuApi("create", {
id: "action_item",
title: "other menu item",
contexts: contexts,
});
await doOpenMenu(extension);
await extension.awaitMessage("onShown fired");
if (expectOtherItems) {
await other_extension.awaitMessage("onShown fired");
}
let elem = extension.getXULElementByMenuId("action_item");
is(elem.getAttribute("label"), "visible menu item", "extension menu shown");
elem = other_extension.getXULElementByMenuId("action_item");
if (expectOtherItems) {
is(
elem.getAttribute("label"),
"other menu item",
"other extension's menu is also shown"
);
} else {
is(elem, null, "other extension's menu should be hidden");
}
await extension.callMenuApi("update", "action_item", { title: "changed" });
await other_extension.callMenuApi("update", "action_item", { title: "foo" });
await other_extension.callMenuApi("refresh");
// refreshing the menu of an unrelated extension should not affect the menu
// of another extension.
elem = extension.getXULElementByMenuId("action_item");
is(elem.getAttribute("label"), "visible menu item", "extension menu shown");
elem = other_extension.getXULElementByMenuId("action_item");
if (expectOtherItems) {
is(elem.getAttribute("label"), "foo", "other extension's item is updated");
} else {
is(elem, null, "other extension's menu should still be hidden");
}
await doCloseMenu();
await extension.unload();
await other_extension.unload();
}
add_task(async function refresh_menus_with_browser_action() {
const args = {
contexts: ["browser_action"],
async doOpenMenu(extension) {
await openActionContextMenu(extension, "browser");
},
async doCloseMenu() {
await closeActionContextMenu();
},
};
await testRefreshMenusWhileVisible(args);
args.expectOtherItems = false;
await testRefreshOther(args);
});
add_task(async function refresh_menus_with_tab() {
const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE);
const args = {
contexts: ["tab"],
async doOpenMenu() {
await openTabContextMenu();
},
async doCloseMenu() {
await closeTabContextMenu();
},
};
await testRefreshMenusWhileVisible(args);
args.expectOtherItems = true;
await testRefreshOther(args);
BrowserTestUtils.removeTab(tab);
});
add_task(async function refresh_menus_with_tools_menu() {
const args = {
contexts: ["tools_menu"],
async doOpenMenu() {
await openToolsMenu();
},
async doCloseMenu() {
await closeToolsMenu();
},
};
await testRefreshMenusWhileVisible(args);
args.expectOtherItems = true;
await testRefreshOther(args);
});
add_task(async function refresh_menus_with_page() {
const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE);
const args = {
contexts: ["page"],
async doOpenMenu() {
await openContextMenu("body");
},
async doCloseMenu() {
await closeExtensionContextMenu();
},
};
await testRefreshMenusWhileVisible(args);
args.expectOtherItems = true;
await testRefreshOther(args);
BrowserTestUtils.removeTab(tab);
});
add_task(async function refresh_without_menus_at_onShown() {
const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE);
let extension = loadExtensionWithMenusApi();
await extension.startup();
const doOpenMenu = () => openContextMenu("body");
const doCloseMenu = () => closeExtensionContextMenu();
await doOpenMenu();
await extension.awaitMessage("onShown fired");
await extension.callMenuApi("create", {
id: "too late",
title: "created after shown",
});
await extension.callMenuApi("refresh");
let elem = extension.getXULElementByMenuId("too late");
is(
elem.getAttribute("label"),
"created after shown",
"extension without visible menu items can add new items"
);
await extension.callMenuApi("update", "too late", { title: "the menu item" });
await extension.callMenuApi("refresh");
elem = extension.getXULElementByMenuId("too late");
is(elem.getAttribute("label"), "the menu item", "label should change");
// The previously created menu item should be visible if the menu is closed
// and re-opened.
await doCloseMenu();
await doOpenMenu();
await extension.awaitMessage("onShown fired");
elem = extension.getXULElementByMenuId("too late");
is(elem.getAttribute("label"), "the menu item", "previously registered item");
await doCloseMenu();
await extension.unload();
BrowserTestUtils.removeTab(tab);
});
add_task(async function refresh_without_onShown() {
const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE);
let extension = loadExtensionWithMenusApi();
await extension.startup();
await extension.removeOnShownListener();
const doOpenMenu = () => openContextMenu("body");
const doCloseMenu = () => closeExtensionContextMenu();
await doOpenMenu();
await extension.callMenuApi("create", {
id: "too late",
title: "created after shown",
});
is(
extension.getXULElementByMenuId("too late"),
null,
"item created after shown is not visible before refresh"
);
await extension.callMenuApi("refresh");
let elem = extension.getXULElementByMenuId("too late");
is(
elem.getAttribute("label"),
"created after shown",
"refresh updates the menu even without onShown"
);
await doCloseMenu();
await extension.unload();
BrowserTestUtils.removeTab(tab);
});
add_task(async function refresh_menus_during_navigation() {
const tab = await BrowserTestUtils.openNewForegroundTab(
gBrowser,
PAGE + "?1"
);
let extension = loadExtensionWithMenusApi();
await extension.startup();
await extension.callMenuApi("create", {
id: "item1",
title: "item1",
contexts: ["browser_action"],
documentUrlPatterns: ["*://*/*?1*"],
});
await extension.callMenuApi("create", {
id: "item2",
title: "item2",
contexts: ["browser_action"],
documentUrlPatterns: ["*://*/*?2*"],
});
await openActionContextMenu(extension, "browser");
await extension.awaitMessage("onShown fired");
let elem = extension.getXULElementByMenuId("item1");
is(elem.getAttribute("label"), "item1", "menu item 1 should be shown");
elem = extension.getXULElementByMenuId("item2");
is(elem, null, "menu item 2 should be hidden");
BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, PAGE + "?2");
await BrowserTestUtils.browserStopped(tab.linkedBrowser);
await extension.callMenuApi("refresh");
// The menu items in a context menu are based on the context at the time of
// opening the menu. Menus are not updated if the context changes, e.g. as a
// result of navigation events after the menu was shown.
// So when refresh() is called during the onShown event, then the original
// URL (before navigation) should be used to determine whether to show a
// URL-specific menu item, and NOT the current URL (after navigation).
elem = extension.getXULElementByMenuId("item1");
is(elem.getAttribute("label"), "item1", "menu item 1 should still be shown");
elem = extension.getXULElementByMenuId("item2");
is(elem, null, "menu item 2 should still be hidden");
await closeActionContextMenu();
await openActionContextMenu(extension, "browser");
await extension.awaitMessage("onShown fired");
// Now after closing and re-opening the menu, the latest contextual info
// should be used.
elem = extension.getXULElementByMenuId("item1");
is(elem, null, "menu item 1 should be hidden");
elem = extension.getXULElementByMenuId("item2");
is(elem.getAttribute("label"), "item2", "menu item 2 should be shown");
await closeActionContextMenu();
await extension.unload();
BrowserTestUtils.removeTab(tab);
});