Bug 1416839 - Add viewType/viewTypes to menus API r=mixedpuppy
authorRob Wu <rob@robwu.nl>
Mon, 01 Oct 2018 16:56:53 +0000
changeset 487393 6341cb6ae607587609d46353fee88a1d737a7fa2
parent 487392 77089282928991ef69a040b48b23cdff377598e9
child 487394 238ec26bef308495ac182b6b66dc5cb6803ea5b4
push id246
push userfmarier@mozilla.com
push dateSat, 13 Oct 2018 00:15:40 +0000
reviewersmixedpuppy
bugs1416839
milestone64.0a1
Bug 1416839 - Add viewType/viewTypes to menus API r=mixedpuppy - Support viewTypes property in menus.create / menus.update. - Add info.viewType to menus.onShown / menus.onClicked event. - This "viewType" reuses the existing extension.ViewType enum, which is a "tab", "popup" (pageAction/browserAction) or "sidebar". Differential Revision: https://phabricator.services.mozilla.com/D6205
browser/base/content/nsContextMenu.js
browser/components/extensions/parent/ext-menus.js
browser/components/extensions/schemas/menus.json
browser/components/extensions/test/browser/browser-common.ini
browser/components/extensions/test/browser/browser_ext_menus_events.js
browser/components/extensions/test/browser/browser_ext_menus_replace_menu.js
browser/components/extensions/test/browser/browser_ext_menus_replace_menu_context.js
browser/components/extensions/test/browser/browser_ext_menus_viewType.js
--- a/browser/base/content/nsContextMenu.js
+++ b/browser/base/content/nsContextMenu.js
@@ -124,16 +124,17 @@ nsContextMenu.prototype = {
         onPassword: this.onPassword,
         srcUrl: this.mediaURL,
         frameUrl: gContextMenuContentData ? gContextMenuContentData.docLocation : undefined,
         pageUrl: this.browser ? this.browser.currentURI.spec : undefined,
         linkText: this.linkTextStr,
         linkUrl: this.linkURL,
         selectionText: this.isTextSelected ? this.selectionInfo.fullText : undefined,
         frameId: this.frameOuterWindowID,
+        webExtBrowserType: this.webExtBrowserType,
         webExtContextData: gContextMenuContentData ? gContextMenuContentData.webExtContextData : undefined,
       };
       subject.wrappedJSObject = subject;
       Services.obs.notifyObservers(subject, "on-build-contextmenu");
     }
 
     this.viewFrameSourceElement =
          document.getElementById("context-viewframesource");
--- a/browser/components/extensions/parent/ext-menus.js
+++ b/browser/components/extensions/parent/ext-menus.js
@@ -63,33 +63,37 @@ var gMenuBuilder = {
     }
   },
 
   maybeOverrideContextData(contextData) {
     let {webExtContextData} = contextData;
     if (!webExtContextData || !webExtContextData.overrideContext) {
       return contextData;
     }
+    let contextDataBase = {
+      menu: contextData.menu,
+      // eslint-disable-next-line no-use-before-define
+      originalViewType: getContextViewType(contextData),
+      webExtContextData,
+    };
     if (webExtContextData.overrideContext === "bookmark") {
       return {
-        menu: contextData.menu,
+        ...contextDataBase,
         bookmarkId: webExtContextData.bookmarkId,
         onBookmark: true,
-        webExtContextData,
       };
     }
     if (webExtContextData.overrideContext === "tab") {
       // TODO: Handle invalid tabs more gracefully (instead of throwing).
       let tab = tabTracker.getTab(webExtContextData.tabId);
       return {
-        menu: contextData.menu,
+        ...contextDataBase,
         tab,
         pageUrl: tab.linkedBrowser.currentURI.spec,
         onTab: true,
-        webExtContextData,
       };
     }
     throw new Error(`Unexpected overrideContext: ${webExtContextData.overrideContext}`);
   },
 
   createAndInsertTopLevelElements(root, contextData, nextSibling) {
     let rootElements;
     if (contextData.onBrowserAction || contextData.onPageAction) {
@@ -545,17 +549,32 @@ const getMenuContexts = contextData => {
   // New non-content contexts supported in Firefox are not part of "all".
   if (!contextData.onBookmark && !contextData.onTab && !contextData.inToolsMenu) {
     contexts.add("all");
   }
 
   return contexts;
 };
 
+function getContextViewType(contextData) {
+  if ("originalViewType" in contextData) {
+    return contextData.originalViewType;
+  }
+  if (contextData.webExtBrowserType === "popup" ||
+      contextData.webExtBrowserType === "sidebar") {
+    return contextData.webExtBrowserType;
+  }
+  if (contextData.tab && contextData.menu.id === "contentAreaContextMenu") {
+    return "tab";
+  }
+  return undefined;
+}
+
 function addMenuEventInfo(info, contextData, extension, includeSensitiveData) {
+  info.viewType = getContextViewType(contextData);
   if (contextData.onVideo) {
     info.mediaType = "video";
   } else if (contextData.onAudio) {
     info.mediaType = "audio";
   } else if (contextData.onImage) {
     info.mediaType = "image";
   }
   if (contextData.frameId !== undefined) {
@@ -777,16 +796,20 @@ MenuItem.prototype = {
     if (!this.visible) {
       return false;
     }
     let contexts = getMenuContexts(contextData);
     if (!this.contexts.some(n => contexts.has(n))) {
       return false;
     }
 
+    if (this.viewTypes && !this.viewTypes.includes(getContextViewType(contextData))) {
+      return false;
+    }
+
     if (contextData.onBookmark) {
       return this.extension.hasPermission("bookmarks");
     }
 
     let docPattern = this.documentUrlMatchPattern;
     let pageURI = Services.io.newURI(contextData[contextData.inFrame ? "frameUrl" : "pageUrl"]);
     if (docPattern && !docPattern.matches(pageURI)) {
       return false;
--- a/browser/components/extensions/schemas/menus.json
+++ b/browser/components/extensions/schemas/menus.json
@@ -78,16 +78,21 @@
           "parentMenuItemId": {
             "choices": [
               { "type": "integer" },
               { "type": "string" }
             ],
             "optional": true,
             "description": "The parent ID, if any, for the item clicked."
           },
+          "viewType": {
+            "$ref": "extension.ViewType",
+            "optional": true,
+            "description": "The type of view where the menu is clicked. May be unset if the menu is not associated with a view."
+          },
           "mediaType": {
             "type": "string",
             "optional": true,
             "description": "One of 'image', 'video', or 'audio' if the context menu was activated on one of these types of elements."
           },
           "linkText": {
             "type": "string",
             "optional": true,
@@ -205,16 +210,25 @@
                 "type": "array",
                 "items": {
                   "$ref": "ContextType"
                 },
                 "minItems": 1,
                 "optional": true,
                 "description": "List of contexts this menu item will appear in. Defaults to ['page'] if not specified."
               },
+              "viewTypes": {
+                "type": "array",
+                "items": {
+                  "$ref": "extension.ViewType"
+                },
+                "minItems": 1,
+                "optional": true,
+                "description": "List of view types where the menu item will be shown. Defaults to any view, including those without a viewType."
+              },
               "visible": {
                 "type": "boolean",
                 "optional": true,
                 "description": "Whether the item is visible in the menu."
               },
               "onclick": {
                 "type": "function",
                 "optional": true,
@@ -314,16 +328,24 @@
               "contexts": {
                 "type": "array",
                 "items": {
                   "$ref": "ContextType"
                 },
                 "minItems": 1,
                 "optional": true
               },
+              "viewTypes": {
+                "type": "array",
+                "items": {
+                  "$ref": "extension.ViewType"
+                },
+                "minItems": 1,
+                "optional": true
+              },
               "visible": {
                 "type": "boolean",
                 "optional": true,
                 "description": "Whether the item is visible in the menu."
               },
               "onclick": {
                 "type": "function",
                 "optional": "omit-key-if-missing",
@@ -495,16 +517,20 @@
                   ]
                 }
               },
               "contexts": {
                 "description": "A list of all contexts that apply to the menu.",
                 "type": "array",
                 "items": {"$ref": "ContextType"}
               },
+              "viewType": {
+                "$ref": "extension.ViewType",
+                "optional": true
+              },
               "editable": {
                 "type": "boolean"
               },
               "mediaType": {
                 "type": "string",
                 "optional": true
               },
               "linkUrl": {
--- a/browser/components/extensions/test/browser/browser-common.ini
+++ b/browser/components/extensions/test/browser/browser-common.ini
@@ -118,16 +118,17 @@ skip-if = (verify && (os == 'linux' || o
 [browser_ext_menus_refresh.js]
 [browser_ext_menus_replace_menu.js]
 [browser_ext_menus_replace_menu_context.js]
 [browser_ext_menus_replace_menu_permissions.js]
 [browser_ext_menus_targetElement.js]
 [browser_ext_menus_targetElement_extension.js]
 [browser_ext_menus_targetElement_shadow.js]
 [browser_ext_menus_visible.js]
+[browser_ext_menus_viewType.js]
 [browser_ext_omnibox.js]
 [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]
 skip-if = (verify && !debug && (os == 'linux'))
--- a/browser/components/extensions/test/browser/browser_ext_menus_events.js
+++ b/browser/components/extensions/test/browser/browser_ext_menus_events.js
@@ -194,16 +194,17 @@ add_task(async function test_show_hide_w
   // Run another context menu test where onShown/onHidden will fire.
   await testShowHideEvent({
     menuCreateParams: {
       title: "any menu item",
       contexts: ["all"],
     },
     expectedShownEvent: {
       contexts: ["page", "all"],
+      viewType: "tab",
       editable: false,
       frameId: 0,
     },
     async doOpenMenu() {
       await openContextMenu("body");
     },
     async doCloseMenu() {
       await closeExtensionContextMenu();
@@ -216,35 +217,38 @@ add_task(async function test_show_hide_w
   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"],
+    viewType: "tab",
     editable: false,
     pageUrl: PAGE,
     frameId: 0,
   }, "expected onShown info from menuless extension");
   await extension.unload();
 });
 
 add_task(async function test_show_hide_pageAction() {
   await testShowHideEvent({
     menuCreateParams: {
       title: "pageAction item",
       contexts: ["page_action"],
     },
     expectedShownEvent: {
       contexts: ["page_action", "all"],
+      viewType: undefined,
       editable: false,
     },
     expectedShownEventWithPermissions: {
       contexts: ["page_action", "all"],
+      viewType: undefined,
       editable: false,
       pageUrl: PAGE,
     },
     async doOpenMenu(extension) {
       await openActionContextMenu(extension, "page");
     },
     async doCloseMenu() {
       await closeActionContextMenu(null, "page");
@@ -255,20 +259,22 @@ add_task(async function test_show_hide_p
 add_task(async function test_show_hide_browserAction() {
   await testShowHideEvent({
     menuCreateParams: {
       title: "browserAction item",
       contexts: ["browser_action"],
     },
     expectedShownEvent: {
       contexts: ["browser_action", "all"],
+      viewType: undefined,
       editable: false,
     },
     expectedShownEventWithPermissions: {
       contexts: ["browser_action", "all"],
+      viewType: undefined,
       editable: false,
       pageUrl: PAGE,
     },
     async doOpenMenu(extension) {
       await openActionContextMenu(extension, "browser");
     },
     async doCloseMenu() {
       await closeActionContextMenu();
@@ -280,23 +286,25 @@ add_task(async function test_show_hide_b
   let popupUrl;
   await testShowHideEvent({
     menuCreateParams: {
       title: "browserAction popup - TEST_EXPECT_NO_TAB",
       contexts: ["all", "browser_action"],
     },
     expectedShownEvent: {
       contexts: ["page", "all"],
+      viewType: "popup",
       frameId: 0,
       editable: false,
       get pageUrl() { return popupUrl; },
       targetElementId: EXPECT_TARGET_ELEMENT,
     },
     expectedShownEventWithPermissions: {
       contexts: ["page", "all"],
+      viewType: "popup",
       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);
@@ -312,20 +320,22 @@ add_task(async function test_show_hide_b
 add_task(async function test_show_hide_tab() {
   await testShowHideEvent({
     menuCreateParams: {
       title: "tab menu item",
       contexts: ["tab"],
     },
     expectedShownEvent: {
       contexts: ["tab"],
+      viewType: undefined,
       editable: false,
     },
     expectedShownEventWithPermissions: {
       contexts: ["tab"],
+      viewType: undefined,
       editable: false,
       pageUrl: PAGE,
     },
     async doOpenMenu() {
       await openTabContextMenu();
     },
     async doCloseMenu() {
       await closeTabContextMenu();
@@ -336,20 +346,22 @@ add_task(async function test_show_hide_t
 add_task(async function test_show_hide_tools_menu() {
   await testShowHideEvent({
     menuCreateParams: {
       title: "menu item",
       contexts: ["tools_menu"],
     },
     expectedShownEvent: {
       contexts: ["tools_menu"],
+      viewType: undefined,
       editable: false,
     },
     expectedShownEventWithPermissions: {
       contexts: ["tools_menu"],
+      viewType: undefined,
       editable: false,
       pageUrl: PAGE,
     },
     async doOpenMenu() {
       await openToolsMenu();
     },
     async doCloseMenu() {
       await closeToolsMenu();
@@ -360,21 +372,23 @@ add_task(async function test_show_hide_t
 add_task(async function test_show_hide_page() {
   await testShowHideEvent({
     menuCreateParams: {
       title: "page menu item",
       contexts: ["page"],
     },
     expectedShownEvent: {
       contexts: ["page", "all"],
+      viewType: "tab",
       editable: false,
       frameId: 0,
     },
     expectedShownEventWithPermissions: {
       contexts: ["page", "all"],
+      viewType: "tab",
       editable: false,
       pageUrl: PAGE,
       frameId: 0,
       targetElementId: EXPECT_TARGET_ELEMENT,
     },
     async doOpenMenu() {
       await openContextMenu("body");
     },
@@ -389,21 +403,23 @@ add_task(async function test_show_hide_f
   let frameId;
   await testShowHideEvent({
     menuCreateParams: {
       title: "subframe menu item",
       contexts: ["frame"],
     },
     expectedShownEvent: {
       contexts: ["frame", "all"],
+      viewType: "tab",
       editable: false,
       get frameId() { return frameId; },
     },
     expectedShownEventWithPermissions: {
       contexts: ["frame", "all"],
+      viewType: "tab",
       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() {
@@ -423,21 +439,23 @@ add_task(async function test_show_hide_f
 add_task(async function test_show_hide_password() {
   await testShowHideEvent({
     menuCreateParams: {
       title: "password item",
       contexts: ["password"],
     },
     expectedShownEvent: {
       contexts: ["editable", "password", "all"],
+      viewType: "tab",
       editable: true,
       frameId: 0,
     },
     expectedShownEventWithPermissions: {
       contexts: ["editable", "password", "all"],
+      viewType: "tab",
       editable: true,
       frameId: 0,
       pageUrl: PAGE,
       targetElementId: EXPECT_TARGET_ELEMENT,
     },
     async doOpenMenu() {
       await openContextMenu("#password");
     },
@@ -450,21 +468,23 @@ add_task(async function test_show_hide_p
 add_task(async function test_show_hide_link() {
   await testShowHideEvent({
     menuCreateParams: {
       title: "link item",
       contexts: ["link"],
     },
     expectedShownEvent: {
       contexts: ["link", "all"],
+      viewType: "tab",
       editable: false,
       frameId: 0,
     },
     expectedShownEventWithPermissions: {
       contexts: ["link", "all"],
+      viewType: "tab",
       editable: false,
       frameId: 0,
       linkText: "Some link",
       linkUrl: PAGE_BASE + "some-link",
       pageUrl: PAGE,
       targetElementId: EXPECT_TARGET_ELEMENT,
     },
     async doOpenMenu() {
@@ -479,22 +499,24 @@ add_task(async function test_show_hide_l
 add_task(async function test_show_hide_image_link() {
   await testShowHideEvent({
     menuCreateParams: {
       title: "image item",
       contexts: ["image"],
     },
     expectedShownEvent: {
       contexts: ["image", "link", "all"],
+      viewType: "tab",
       mediaType: "image",
       editable: false,
       frameId: 0,
     },
     expectedShownEventWithPermissions: {
       contexts: ["image", "link", "all"],
+      viewType: "tab",
       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,
@@ -513,21 +535,23 @@ add_task(async function test_show_hide_e
   let selectionText;
   await testShowHideEvent({
     menuCreateParams: {
       title: "editable item",
       contexts: ["editable"],
     },
     expectedShownEvent: {
       contexts: ["editable", "selection", "all"],
+      viewType: "tab",
       editable: true,
       frameId: 0,
     },
     expectedShownEventWithPermissions: {
       contexts: ["editable", "selection", "all"],
+      viewType: "tab",
       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.
@@ -550,22 +574,24 @@ add_task(async function test_show_hide_v
   const VIDEO_URL = "data:video/webm,xxx";
   await testShowHideEvent({
     menuCreateParams: {
       title: "video item",
       contexts: ["video"],
     },
     expectedShownEvent: {
       contexts: ["video", "all"],
+      viewType: "tab",
       mediaType: "video",
       editable: false,
       frameId: 0,
     },
     expectedShownEventWithPermissions: {
       contexts: ["video", "all"],
+      viewType: "tab",
       mediaType: "video",
       editable: false,
       frameId: 0,
       srcUrl: VIDEO_URL,
       pageUrl: PAGE,
       targetElementId: EXPECT_TARGET_ELEMENT,
     },
     async doOpenMenu() {
@@ -589,22 +615,24 @@ add_task(async function test_show_hide_a
   const AUDIO_URL = "data:audio/ogg,xxx";
   await testShowHideEvent({
     menuCreateParams: {
       title: "audio item",
       contexts: ["audio"],
     },
     expectedShownEvent: {
       contexts: ["audio", "all"],
+      viewType: "tab",
       mediaType: "audio",
       editable: false,
       frameId: 0,
     },
     expectedShownEventWithPermissions: {
       contexts: ["audio", "all"],
+      viewType: "tab",
       mediaType: "audio",
       editable: false,
       frameId: 0,
       srcUrl: AUDIO_URL,
       pageUrl: PAGE,
       targetElementId: EXPECT_TARGET_ELEMENT,
     },
     async doOpenMenu() {
--- a/browser/components/extensions/test/browser/browser_ext_menus_replace_menu.js
+++ b/browser/components/extensions/test/browser/browser_ext_menus_replace_menu.js
@@ -85,16 +85,17 @@ add_task(async function overrideContext_
       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");
         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"});
     },
   });
@@ -242,16 +243,17 @@ add_task(async function overrideContext_
       } 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) {
--- a/browser/components/extensions/test/browser/browser_ext_menus_replace_menu_context.js
+++ b/browser/components/extensions/test/browser/browser_ext_menus_replace_menu_context.js
@@ -35,24 +35,26 @@ add_task(async function overrideContext_
         browser.test.sendMessage("testTabAccessDone", "executeScript_ok");
         return;
       } catch (e) {
         browser.test.assertEq("Missing host permission for the tab", e.message, "Expected error message");
         browser.test.sendMessage("testTabAccessDone", "executeScript_failed");
       }
     });
     browser.menus.onShown.addListener((info, tab) => {
+      browser.test.assertEq("tab", info.viewType, "Expected viewType at onShown");
       browser.test.sendMessage("onShown", {
         menuIds: info.menuIds,
         contexts: info.contexts,
         bookmarkId: info.bookmarkId,
         tabId: tab && tab.id,
       });
     });
     browser.menus.onClicked.addListener((info, tab) => {
+      browser.test.assertEq("tab", info.viewType, "Expected viewType at onClicked");
       browser.test.sendMessage("onClicked", {
         menuItemId: info.menuItemId,
         bookmarkId: info.bookmarkId,
         tabId: tab && tab.id,
       });
     });
     browser.menus.create({id: "tab_context", title: "tab_context", contexts: ["tab"]});
     browser.menus.create({id: "bookmark_context", title: "bookmark_context", contexts: ["bookmark"]});
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_menus_viewType.js
@@ -0,0 +1,95 @@
+/* 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";
+
+// browser_ext_menus_events.js provides some coverage for viewTypes in normal
+// tabs and extension popups.
+// This test provides coverage for extension tabs and sidebars, as well as
+// using the viewTypes property in menus.create and menus.update.
+
+add_task(async function extension_tab_viewType() {
+  async function background() {
+    browser.menus.onShown.addListener(info => {
+      browser.test.assertEq("tabonly", info.menuIds.join(","), "Expected menu items");
+      browser.test.sendMessage("shown");
+    });
+    browser.menus.onClicked.addListener(info => {
+      browser.test.assertEq("tab", info.viewType, "Expected viewType");
+      browser.test.sendMessage("clicked");
+    });
+
+    browser.menus.create({id: "sidebaronly", title: "sidebar-only", viewTypes: ["sidebar"]});
+    browser.menus.create({id: "tabonly", title: "click here", viewTypes: ["tab"]}, () => {
+      browser.tabs.create({url: "tab.html"});
+    });
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      permissions: ["menus"],
+    },
+    files: {
+      "tab.html": `<!DOCTYPE html><meta charset="utf-8"><script src="tab.js"></script>`,
+      "tab.js": `browser.test.sendMessage("ready");`,
+    },
+    background,
+  });
+
+  let extensionTabPromise = BrowserTestUtils.waitForNewTab(gBrowser, null, true);
+  await extension.startup();
+  await extension.awaitMessage("ready");
+  await extensionTabPromise;
+  let menu = await openContextMenu();
+  await extension.awaitMessage("shown");
+
+  let menuItem = menu.getElementsByAttribute("label", "click here")[0];
+  await closeExtensionContextMenu(menuItem);
+  await extension.awaitMessage("clicked");
+
+  // Unloading the extension will automatically close the extension's tab.html
+  await extension.unload();
+});
+
+add_task(async function sidebar_panel_viewType() {
+  async function sidebarJs() {
+    browser.menus.onShown.addListener(info => {
+      browser.test.assertEq("sidebaronly", info.menuIds.join(","), "Expected menu items");
+      browser.test.assertEq("sidebar", info.viewType, "Expected viewType");
+      browser.test.sendMessage("shown");
+    });
+
+    // Create menus and change their viewTypes using menus.update.
+    browser.menus.create({id: "sidebaronly", title: "sidebaronly", viewTypes: ["tab"]});
+    browser.menus.create({id: "tabonly", title: "sidebaronly", viewTypes: ["sidebar"]});
+    await browser.menus.update("sidebaronly", {viewTypes: ["sidebar"]});
+    await browser.menus.update("tabonly", {viewTypes: ["tab"]});
+    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">
+        <script src="sidebar.js"></script>
+      `,
+      "sidebar.js": sidebarJs,
+    },
+  });
+
+  await extension.startup();
+  await extension.awaitMessage("ready");
+
+  let sidebarMenu = await openContextMenuInSidebar();
+  await extension.awaitMessage("shown");
+  await closeContextMenu(sidebarMenu);
+
+  await extension.unload();
+});