--- a/browser/components/extensions/test/browser/browser-common.ini
+++ b/browser/components/extensions/test/browser/browser-common.ini
@@ -105,16 +105,18 @@ skip-if = (verify && (os == 'linux' || o
[browser_ext_incognito_popup.js]
[browser_ext_lastError.js]
[browser_ext_menus.js]
[browser_ext_menus_activeTab.js]
[browser_ext_menus_errors.js]
[browser_ext_menus_event_order.js]
[browser_ext_menus_events.js]
[browser_ext_menus_refresh.js]
+[browser_ext_menus_targetElement.js]
+[browser_ext_menus_targetElement_extension.js]
[browser_ext_omnibox.js]
skip-if = (debug && (os == 'linux' || os == 'mac')) || (verify && (os == 'linux' || os == 'mac')) # Bug 1416103 (was bug 1417052)
[browser_ext_openPanel.js]
skip-if = (verify && !debug && (os == 'linux' || os == 'mac'))
[browser_ext_optionsPage_browser_style.js]
[browser_ext_optionsPage_modals.js]
[browser_ext_optionsPage_privileges.js]
[browser_ext_pageAction_context.js]
--- a/browser/components/extensions/test/browser/browser_ext_menus_events.js
+++ b/browser/components/extensions/test/browser/browser_ext_menus_events.js
@@ -2,16 +2,18 @@
* 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";
const PAGE_BASE = PAGE.replace("context.html", "");
const PAGE_HOST_PATTERN = "http://mochi.test/*";
+const EXPECT_TARGET_ELEMENT = 13337;
+
async function grantOptionalPermission(extension, permissions) {
const {GlobalManager} = ChromeUtils.import("resource://gre/modules/Extension.jsm", {});
const {ExtensionPermissions} = ChromeUtils.import("resource://gre/modules/ExtensionPermissions.jsm", {});
let ext = GlobalManager.extensionMap.get(extension.id);
return ExtensionPermissions.add(ext, permissions);
}
// Registers a context menu using menus.create(menuCreateParams) and checks
@@ -41,16 +43,21 @@ async function testShowHideEvent({menuCr
let menuCreateParams = await awaitMessage("create-params");
const [tab] = await browser.tabs.query({active: true, currentWindow: true});
let shownEvents = [];
let hiddenEvents = [];
browser.menus.onShown.addListener((...args) => {
+ if (args[0].targetElementId) {
+ // In this test, we aren't interested in the exact value,
+ // only in whether it is set or not.
+ args[0].targetElementId = 13337; // = EXPECT_TARGET_ELEMENT
+ }
shownEvents.push(args[0]);
if (menuCreateParams.title.includes("TEST_EXPECT_NO_TAB")) {
browser.test.assertEq(undefined, args[1], "expect no tab");
} else {
browser.test.assertEq(tab.id, args[1].id, "expected tab");
}
browser.test.assertEq(2, args.length, "expected number of onShown args");
});
@@ -204,16 +211,18 @@ add_task(async function test_show_hide_w
});
// Now the menu has been shown and hidden, and in another extension the
// onShown/onHidden events have been dispatched.
extension.sendMessage("check menu events");
let events = await extension.awaitMessage("events from menuless extension");
is(events.length, 2, "expect two events");
is(events[1], "onHidden", "last event should be onHidden");
+ ok(events[0].targetElementId, "info.targetElementId must be set in onShown");
+ delete events[0].targetElementId;
Assert.deepEqual(events[0], {
menuIds: [],
contexts: ["page", "all"],
editable: false,
pageUrl: PAGE,
frameId: 0,
}, "expected onShown info from menuless extension");
await extension.unload();
@@ -274,22 +283,24 @@ add_task(async function test_show_hide_b
title: "browserAction popup - TEST_EXPECT_NO_TAB",
contexts: ["all", "browser_action"],
},
expectedShownEvent: {
contexts: ["page", "all"],
frameId: 0,
editable: false,
get pageUrl() { return popupUrl; },
+ targetElementId: EXPECT_TARGET_ELEMENT,
},
expectedShownEventWithPermissions: {
contexts: ["page", "all"],
frameId: 0,
editable: false,
get pageUrl() { return popupUrl; },
+ targetElementId: EXPECT_TARGET_ELEMENT,
},
async doOpenMenu(extension) {
popupUrl = `moz-extension://${extension.uuid}/popup.html`;
await clickBrowserAction(extension);
await openContextMenuInPopup(extension);
},
async doCloseMenu(extension) {
await closeExtensionContextMenu();
@@ -357,16 +368,17 @@ add_task(async function test_show_hide_p
editable: false,
frameId: 0,
},
expectedShownEventWithPermissions: {
contexts: ["page", "all"],
editable: false,
pageUrl: PAGE,
frameId: 0,
+ targetElementId: EXPECT_TARGET_ELEMENT,
},
async doOpenMenu() {
await openContextMenu("body");
},
async doCloseMenu() {
await closeExtensionContextMenu();
},
});
@@ -386,16 +398,17 @@ add_task(async function test_show_hide_f
get frameId() { return frameId; },
},
expectedShownEventWithPermissions: {
contexts: ["frame", "all"],
editable: false,
get frameId() { return frameId; },
pageUrl: PAGE,
frameUrl: PAGE_BASE + "context_frame.html",
+ targetElementId: EXPECT_TARGET_ELEMENT,
},
async doOpenMenu() {
frameId = await ContentTask.spawn(gBrowser.selectedBrowser, {}, function() {
let {contentWindow} = content.document.getElementById("frame");
return WebNavigationFrames.getFrameId(contentWindow);
});
await openContextMenuInFrame("#frame");
},
@@ -416,16 +429,17 @@ add_task(async function test_show_hide_p
editable: true,
frameId: 0,
},
expectedShownEventWithPermissions: {
contexts: ["editable", "password", "all"],
editable: true,
frameId: 0,
pageUrl: PAGE,
+ targetElementId: EXPECT_TARGET_ELEMENT,
},
async doOpenMenu() {
await openContextMenu("#password");
},
async doCloseMenu() {
await closeExtensionContextMenu();
},
});
@@ -444,16 +458,17 @@ add_task(async function test_show_hide_l
},
expectedShownEventWithPermissions: {
contexts: ["link", "all"],
editable: false,
frameId: 0,
linkText: "Some link",
linkUrl: PAGE_BASE + "some-link",
pageUrl: PAGE,
+ targetElementId: EXPECT_TARGET_ELEMENT,
},
async doOpenMenu() {
await openContextMenu("#link1");
},
async doCloseMenu() {
await closeExtensionContextMenu();
},
});
@@ -476,16 +491,17 @@ add_task(async function test_show_hide_i
mediaType: "image",
editable: false,
frameId: 0,
// Apparently, when a link has no content, its href is used as linkText.
linkText: PAGE_BASE + "image-around-some-link",
linkUrl: PAGE_BASE + "image-around-some-link",
srcUrl: PAGE_BASE + "ctxmenu-image.png",
pageUrl: PAGE,
+ targetElementId: EXPECT_TARGET_ELEMENT,
},
async doOpenMenu() {
await openContextMenu("#img-wrapped-in-link");
},
async doCloseMenu() {
await closeExtensionContextMenu();
},
});
@@ -504,16 +520,17 @@ add_task(async function test_show_hide_e
frameId: 0,
},
expectedShownEventWithPermissions: {
contexts: ["editable", "selection", "all"],
editable: true,
frameId: 0,
pageUrl: PAGE,
get selectionText() { return selectionText; },
+ targetElementId: EXPECT_TARGET_ELEMENT,
},
async doOpenMenu() {
// Select lots of text in the test page before opening the menu.
selectionText = await ContentTask.spawn(gBrowser.selectedBrowser, {}, function() {
let node = content.document.getElementById("editabletext");
node.select();
node.focus();
return node.value;
@@ -542,16 +559,17 @@ add_task(async function test_show_hide_v
},
expectedShownEventWithPermissions: {
contexts: ["video", "all"],
mediaType: "video",
editable: false,
frameId: 0,
srcUrl: VIDEO_URL,
pageUrl: PAGE,
+ targetElementId: EXPECT_TARGET_ELEMENT,
},
async doOpenMenu() {
await ContentTask.spawn(gBrowser.selectedBrowser, VIDEO_URL, function(VIDEO_URL) {
let video = content.document.createElement("video");
video.controls = true;
video.src = VIDEO_URL;
content.document.body.appendChild(video);
video.focus();
@@ -580,16 +598,17 @@ add_task(async function test_show_hide_a
},
expectedShownEventWithPermissions: {
contexts: ["audio", "all"],
mediaType: "audio",
editable: false,
frameId: 0,
srcUrl: AUDIO_URL,
pageUrl: PAGE,
+ targetElementId: EXPECT_TARGET_ELEMENT,
},
async doOpenMenu() {
await ContentTask.spawn(gBrowser.selectedBrowser, AUDIO_URL, function(AUDIO_URL) {
let audio = content.document.createElement("audio");
audio.controls = true;
audio.src = AUDIO_URL;
content.document.body.appendChild(audio);
audio.focus();
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_menus_targetElement.js
@@ -0,0 +1,261 @@
+/* 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";
+
+// Loads an extension that records menu visibility events in the current tab.
+// The returned extension has two helper functions "openContextMenu" and
+// "checkIsValid" that are used to verify the behavior of targetElementId.
+async function loadExtensionAndTab() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE);
+ gBrowser.selectedTab = tab;
+
+ function contentScript() {
+ browser.test.onMessage.addListener((msg, targetElementId, expectedSelector, description) => {
+ browser.test.assertEq("checkIsValid", msg, "Expected message");
+
+ let expected = expectedSelector ? document.querySelector(expectedSelector) : null;
+ let elem = browser.menus.getTargetElement(targetElementId);
+ browser.test.assertEq(expected, elem, description);
+ browser.test.sendMessage("checkIsValidDone");
+ });
+ }
+
+ async function background() {
+ browser.menus.onShown.addListener(async (info, tab) => {
+ browser.test.sendMessage("onShownMenu", info.targetElementId);
+ });
+ await browser.tabs.executeScript({file: "contentScript.js"});
+ browser.test.sendMessage("ready");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["menus", "http://mochi.test/*"],
+ },
+ background,
+ files: {
+ "contentScript.js": contentScript,
+ },
+ });
+
+ extension.openAndCloseMenu = async (selector) => {
+ await openContextMenu(selector);
+ let targetElementId = await extension.awaitMessage("onShownMenu");
+ await closeContextMenu();
+ return targetElementId;
+ };
+
+ extension.checkIsValid = async (targetElementId, expectedSelector, description) => {
+ extension.sendMessage("checkIsValid", targetElementId, expectedSelector, description);
+ await extension.awaitMessage("checkIsValidDone");
+ };
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+ return {extension, tab};
+}
+
+// Tests that info.targetElementId is only available with the right permissions.
+add_task(async function required_permission() {
+ let {extension, tab} = await loadExtensionAndTab();
+
+ // Load another extension to verify that the permission from the first
+ // extension does not enable the "targetElementId" parameter.
+ function background() {
+ browser.contextMenus.onShown.addListener((info, tab) => {
+ browser.test.assertEq(undefined, info.targetElementId, "targetElementId requires permission");
+ browser.test.sendMessage("onShown");
+ });
+ browser.contextMenus.onClicked.addListener(async (info) => {
+ browser.test.assertEq(undefined, info.targetElementId, "targetElementId requires permission");
+ const code = `
+ browser.test.assertEq(undefined, browser.menus, "menus API requires permission in content script");
+ browser.test.assertEq(undefined, browser.contextMenus, "contextMenus API not available in content script.");
+ `;
+ await browser.tabs.executeScript({code});
+ browser.test.sendMessage("onClicked");
+ });
+ browser.contextMenus.create({title: "menu for page"}, () => {
+ browser.test.sendMessage("ready");
+ });
+ }
+ let extension2 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["contextMenus", "http://mochi.test/*"],
+ },
+ background,
+ });
+ await extension2.startup();
+ await extension2.awaitMessage("ready");
+
+ let menu = await openContextMenu();
+ await extension.awaitMessage("onShownMenu");
+ let menuItem = menu.getElementsByAttribute("label", "menu for page")[0];
+ await closeExtensionContextMenu(menuItem);
+
+ await extension2.awaitMessage("onShown");
+ await extension2.awaitMessage("onClicked");
+ await extension2.unload();
+
+ await extension.unload();
+ BrowserTestUtils.removeTab(tab);
+});
+
+// Tests that the basic functionality works as expected.
+add_task(async function getTargetElement_in_page() {
+ let {extension, tab} = await loadExtensionAndTab();
+
+ for (let selector of ["#img1", "#link1", "#password"]) {
+ let targetElementId = await extension.openAndCloseMenu(selector);
+ ok(Number.isInteger(targetElementId), `targetElementId (${targetElementId}) should be an integer for ${selector}`);
+
+ await extension.checkIsValid(targetElementId, selector, `Expected target to match ${selector}`);
+ }
+
+ await extension.unload();
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function getTargetElement_in_frame() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE);
+ gBrowser.selectedTab = tab;
+
+ async function background() {
+ let targetElementId;
+ browser.menus.onShown.addListener(async (info, tab) => {
+ browser.test.assertTrue(info.frameUrl.endsWith("context_frame.html"), `Expected frame ${info.frameUrl}`);
+ targetElementId = info.targetElementId;
+ let elem = browser.menus.getTargetElement(targetElementId);
+ browser.test.assertEq(null, elem, "should not find page element in extension's background");
+
+ await browser.tabs.executeScript(tab.id, {
+ code: `{
+ let elem = browser.menus.getTargetElement(${targetElementId});
+ browser.test.assertEq(null, elem, "should not find element from different frame");
+ }`,
+ });
+
+ await browser.tabs.executeScript(tab.id, {
+ frameId: info.frameId,
+ code: `{
+ let elem = browser.menus.getTargetElement(${targetElementId});
+ browser.test.assertEq(document.body, elem, "should find the target element in the frame");
+ }`,
+ });
+ browser.test.sendMessage("pageAndFrameChecked");
+ });
+
+ browser.menus.onClicked.addListener(info => {
+ browser.test.assertEq(targetElementId, info.targetElementId, "targetElementId in onClicked must match onShown.");
+ browser.test.sendMessage("onClickedChecked");
+ });
+
+ browser.menus.create({title: "menu for frame", contexts: ["frame"]}, () => {
+ browser.test.sendMessage("ready");
+ });
+ }
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["menus", "http://mochi.test/*"],
+ },
+ background,
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+
+ let menu = await openContextMenuInFrame("#frame");
+ await extension.awaitMessage("pageAndFrameChecked");
+ let menuItem = menu.getElementsByAttribute("label", "menu for frame")[0];
+ await closeExtensionContextMenu(menuItem);
+ await extension.awaitMessage("onClickedChecked");
+
+ await extension.unload();
+ BrowserTestUtils.removeTab(tab);
+});
+
+// Test that getTargetElement does not return a detached element.
+add_task(async function getTargetElement_after_removing_element() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE);
+
+ function background() {
+ function contentScript(targetElementId) {
+ let expectedElem = document.getElementById("edit-me");
+ let {nextElementSibling} = expectedElem;
+
+ let elem = browser.menus.getTargetElement(targetElementId);
+ browser.test.assertEq(expectedElem, elem, "Expected target element before element removal");
+
+ expectedElem.remove();
+ elem = browser.menus.getTargetElement(targetElementId);
+ browser.test.assertEq(null, elem, "Expected no target element after element removal.");
+
+ nextElementSibling.insertAdjacentElement("beforebegin", expectedElem);
+ elem = browser.menus.getTargetElement(targetElementId);
+ browser.test.assertEq(expectedElem, elem, "Expected target element after element restoration.");
+ }
+ browser.menus.onClicked.addListener(async (info, tab) => {
+ const code = `(${contentScript})(${info.targetElementId})`;
+ browser.test.log(code);
+ await browser.tabs.executeScript(tab.id, {code});
+ browser.test.sendMessage("checkedRemovedElement");
+ });
+ browser.menus.create({title: "some menu item"}, () => {
+ browser.test.sendMessage("ready");
+ });
+ }
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["menus", "http://mochi.test/*"],
+ },
+ background,
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+ let menu = await openContextMenu("#edit-me");
+ let menuItem = menu.getElementsByAttribute("label", "some menu item")[0];
+ await closeExtensionContextMenu(menuItem);
+ await extension.awaitMessage("checkedRemovedElement");
+ await extension.unload();
+ BrowserTestUtils.removeTab(tab);
+});
+
+// Tests whether targetElementId expires after opening a new menu.
+add_task(async function expireTargetElement() {
+ let {extension, tab} = await loadExtensionAndTab();
+
+ // Open the menu once to get the first element ID.
+ let targetElementId = await extension.openAndCloseMenu("#longtext");
+
+ // Open another menu. The previous ID should expire.
+ await extension.openAndCloseMenu("#longtext");
+ await extension.checkIsValid(targetElementId, null, `Expected initial target ID to expire after opening another menu`);
+
+ await extension.unload();
+ BrowserTestUtils.removeTab(tab);
+});
+
+// Tests whether targetElementId of different tabs are independent.
+add_task(async function independentMenusInDifferentTabs() {
+ let {extension, tab} = await loadExtensionAndTab();
+
+ let targetElementId = await extension.openAndCloseMenu("#longtext");
+
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE + "?");
+ gBrowser.selectedTab = tab2;
+
+ let targetElementId2 = await extension.openAndCloseMenu("#editabletext");
+
+ await extension.checkIsValid(targetElementId2, null,
+ "targetElementId from different tab should not resolve.");
+ await extension.checkIsValid(targetElementId, "#longtext",
+ "Expected getTargetElement to work after closing a menu in another tab.");
+
+ await extension.unload();
+ BrowserTestUtils.removeTab(tab);
+ BrowserTestUtils.removeTab(tab2);
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_menus_targetElement_extension.js
@@ -0,0 +1,165 @@
+/* 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";
+
+add_task(async function getTargetElement_in_extension_tab() {
+ async function background() {
+ browser.menus.onShown.addListener(info => {
+ let elem = browser.menus.getTargetElement(info.targetElementId);
+ browser.test.assertEq(null, elem, "should not get element of tab content in background");
+
+ // By using getViews() here, we verify that the targetElementId can
+ // synchronously be mapped to a valid element in a different tab
+ // during the onShown event.
+ let [tabGlobal] = browser.extension.getViews({type: "tab"});
+ elem = tabGlobal.browser.menus.getTargetElement(info.targetElementId);
+ browser.test.assertEq("BUTTON", elem.tagName, "should get element in tab content");
+ browser.test.sendMessage("elementChecked");
+ });
+
+ browser.tabs.create({url: "tab.html"});
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["menus"],
+ },
+ files: {
+ "tab.html": `<!DOCTYPE html><meta charset="utf-8"><button>Button in tab</button>`,
+ },
+ background,
+ });
+
+ 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 openContextMenu("button");
+ await extension.awaitMessage("elementChecked");
+ await closeContextMenu();
+
+ // Unloading the extension will automatically close the extension's tab.html
+ await extension.unload();
+});
+
+add_task(async function getTargetElement_in_extension_tab_on_click() {
+ // Similar to getTargetElement_in_extension_tab, except we check whether
+ // calling getTargetElement in onClicked results in the expected behavior.
+ async function background() {
+ browser.menus.onClicked.addListener(info => {
+ let [tabGlobal] = browser.extension.getViews({type: "tab"});
+ let elem = tabGlobal.browser.menus.getTargetElement(info.targetElementId);
+ browser.test.assertEq("BUTTON", elem.tagName, "should get element in tab content on click");
+ browser.test.sendMessage("elementClicked");
+ });
+
+ browser.menus.create({title: "click here"}, () => {
+ browser.tabs.create({url: "tab.html"});
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["menus"],
+ },
+ files: {
+ "tab.html": `<!DOCTYPE html><meta charset="utf-8"><button>Button in tab</button>`,
+ },
+ background,
+ });
+
+ let extensionTabPromise = BrowserTestUtils.waitForNewTab(gBrowser, null, true);
+ await extension.startup();
+ await extensionTabPromise;
+ let menu = await openContextMenu("button");
+ let menuItem = menu.getElementsByAttribute("label", "click here")[0];
+ await closeExtensionContextMenu(menuItem);
+ await extension.awaitMessage("elementClicked");
+
+ await extension.unload();
+});
+
+add_task(async function getTargetElement_in_browserAction_popup() {
+ async function background() {
+ browser.menus.onShown.addListener(info => {
+ let elem = browser.menus.getTargetElement(info.targetElementId);
+ browser.test.assertEq(null, elem, "should not get element of popup content in background");
+
+ let [popupGlobal] = browser.extension.getViews({type: "popup"});
+ elem = popupGlobal.browser.menus.getTargetElement(info.targetElementId);
+ browser.test.assertEq("BUTTON", elem.tagName, "should get element in popup content");
+ browser.test.sendMessage("popupChecked");
+ });
+
+ // Ensure that onShown is registered (workaround for bug 1300234):
+ await browser.menus.removeAll();
+ browser.test.sendMessage("ready");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["menus"],
+ browser_action: {
+ default_popup: "popup.html",
+ },
+ },
+ files: {
+ "popup.html": `<!DOCTYPE html><meta charset="utf-8"><button>Button in popup</button>`,
+ },
+ background,
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+
+ await clickBrowserAction(extension);
+ await openContextMenuInPopup(extension, "button");
+ await extension.awaitMessage("popupChecked");
+ await closeContextMenu();
+ await closeBrowserAction(extension);
+
+ await extension.unload();
+});
+
+add_task(async function getTargetElement_in_sidebar_panel() {
+ async function sidebarJs() {
+ browser.menus.onShown.addListener(info => {
+ let expected = document.querySelector("button");
+ let elem = browser.menus.getTargetElement(info.targetElementId);
+ browser.test.assertEq(expected, elem, "should get element in sidebar content");
+ browser.test.sendMessage("done");
+ });
+
+ // Ensure that onShown is registered (workaround for bug 1300234):
+ await browser.menus.removeAll();
+ browser.test.sendMessage("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": `
+ <!DOCTYPE html><meta charset="utf-8">
+ <button>Button in sidebar</button>
+ <script src="sidebar.js"></script>
+ `,
+ "sidebar.js": sidebarJs,
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+
+ let sidebarMenu = await openContextMenuInSidebar("button");
+ await extension.awaitMessage("done");
+ await closeContextMenu(sidebarMenu);
+
+ await extension.unload();
+});