/* 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 = "http://mochi.test:8888/browser/browser/components/extensions/test/browser/context.html"; // 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.loadURIString(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); });