Bug 1268020 - Implement "tools_menu" context r=kmag
authorTomislav Jovanovic <tomica@gmail.com>
Wed, 28 Jun 2017 16:19:24 -0700
changeset 419183 509be072f2ea0d9e6bef636e8ddcef1a15cf4a7a
parent 419182 955998eefbe8462e3fa9964962eb8ee6b05200dd
child 419184 ac0b38e566148e36369e7a7b987ba7b15b70e2ae
push id7566
push usermtabara@mozilla.com
push dateWed, 02 Aug 2017 08:25:16 +0000
treeherdermozilla-beta@86913f512c3c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskmag
bugs1268020
milestone56.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1268020 - Implement "tools_menu" context r=kmag MozReview-Commit-ID: KPUsBbqyQTC
browser/components/extensions/ext-menus.js
browser/components/extensions/schemas/menus.json
browser/components/extensions/test/browser/browser_ext_menus.js
browser/components/extensions/test/browser/head.js
--- a/browser/components/extensions/ext-menus.js
+++ b/browser/components/extensions/ext-menus.js
@@ -298,69 +298,47 @@ var gMenuBuilder = {
   itemsToCleanUp: new Set(),
 };
 
 // Called from pageAction or browserAction popup.
 global.actionContextMenu = function(contextData) {
   gMenuBuilder.buildActionContextMenu(contextData);
 };
 
+const contextsMap = {
+  onAudio: "audio",
+  onEditableArea: "editable",
+  inFrame: "frame",
+  onImage: "image",
+  onLink: "link",
+  onPassword: "password",
+  isTextSelected: "selection",
+  onVideo: "video",
+
+  onBrowserAction: "browser_action",
+  onPageAction: "page_action",
+  onTab: "tab",
+  inToolsMenu: "tools_menu",
+};
+
 const getMenuContexts = contextData => {
   let contexts = new Set();
 
-  if (contextData.inFrame) {
-    contexts.add("frame");
-  }
-
-  if (contextData.isTextSelected) {
-    contexts.add("selection");
-  }
-
-  if (contextData.onLink) {
-    contexts.add("link");
-  }
-
-  if (contextData.onEditableArea) {
-    contexts.add("editable");
-  }
-
-  if (contextData.onPassword) {
-    contexts.add("password");
-  }
-
-  if (contextData.onImage) {
-    contexts.add("image");
-  }
-
-  if (contextData.onVideo) {
-    contexts.add("video");
-  }
-
-  if (contextData.onAudio) {
-    contexts.add("audio");
-  }
-
-  if (contextData.onPageAction) {
-    contexts.add("page_action");
-  }
-
-  if (contextData.onBrowserAction) {
-    contexts.add("browser_action");
-  }
-
-  if (contextData.onTab) {
-    contexts.add("tab");
+  for (const [key, value] of Object.entries(contextsMap)) {
+    if (contextData[key]) {
+      contexts.add(value);
+    }
   }
 
   if (contexts.size === 0) {
     contexts.add("page");
   }
 
   // New non-content contexts supported in Firefox are not part of "all".
-  if (!contextData.onTab) {
+  if (!contextData.onTab && !contextData.inToolsMenu) {
     contexts.add("all");
   }
 
   return contexts;
 };
 
 function MenuItem(extension, createProperties, isRoot = false) {
   this.extension = extension;
@@ -577,47 +555,58 @@ MenuItem.prototype = {
       }
     }
 
     return true;
   },
 };
 
 // While any extensions are active, this Tracker registers to observe/listen
-// for contex-menu events from both content and chrome.
+// for menu events from both Tools and context menus, both content and chrome.
 const menuTracker = {
+  menuIds: ["menu_ToolsPopup", "tabContextMenu"],
+
   register() {
     Services.obs.addObserver(this, "on-build-contextmenu");
     for (const window of windowTracker.browserWindows()) {
       this.onWindowOpen(window);
     }
     windowTracker.addOpenListener(this.onWindowOpen);
   },
 
   unregister() {
     Services.obs.removeObserver(this, "on-build-contextmenu");
     for (const window of windowTracker.browserWindows()) {
-      const menu = window.document.getElementById("tabContextMenu");
-      menu.removeEventListener("popupshowing", this);
+      for (const id of this.menuIds) {
+        const menu = window.document.getElementById(id);
+        menu.removeEventListener("popupshowing", this);
+      }
     }
     windowTracker.removeOpenListener(this.onWindowOpen);
   },
 
   observe(subject, topic, data) {
     subject = subject.wrappedJSObject;
     gMenuBuilder.build(subject);
   },
 
   onWindowOpen(window) {
-    const menu = window.document.getElementById("tabContextMenu");
-    menu.addEventListener("popupshowing", menuTracker);
+    for (const id of this.menuIds) {
+      const menu = window.document.getElementById(id);
+      menu.addEventListener("popupshowing", menuTracker);
+    }
   },
 
   handleEvent(event) {
     const menu = event.target;
+    if (menu.id === "menu_ToolsPopup") {
+      const tab = tabTracker.activeTab;
+      const pageUrl = tab.linkedBrowser.currentURI.spec;
+      gMenuBuilder.build({menu, tab, pageUrl, inToolsMenu: true});
+    }
     if (menu.id === "tabContextMenu") {
       const trigger = menu.triggerNode;
       const tab = trigger.localName === "tab" ? trigger : tabTracker.activeTab;
       const pageUrl = tab.linkedBrowser.currentURI.spec;
       gMenuBuilder.build({menu, tab, pageUrl, onTab: true});
     }
   },
 };
--- a/browser/components/extensions/schemas/menus.json
+++ b/browser/components/extensions/schemas/menus.json
@@ -17,34 +17,42 @@
         }]
       }
     ]
   },
   {
     "namespace": "contextMenus",
     "permissions": ["contextMenus"],
     "description": "Use the browser.contextMenus API to add items to the browser's context menu. You can choose what types of objects your context menu additions apply to, such as images, hyperlinks, and pages.",
-    "$import": "menus"
+    "$import": "menus",
+    "types": [
+      {
+        "id": "ContextType",
+        "type": "string",
+        "enum": ["all", "page", "frame", "selection", "link", "editable", "password", "image", "video", "audio", "launcher", "browser_action", "page_action", "tab"],
+        "description": "The different contexts a menu can appear in. Specifying 'all' is equivalent to the combination of all other contexts except for 'tab' and 'tools_menu'."
+      }
+    ]
   },
   {
     "namespace": "menus",
     "permissions": ["menus"],
     "description": "Use the browser.menus API to add items to the browser's menus. You can choose what types of objects your context menu additions apply to, such as images, hyperlinks, and pages.",
     "properties": {
       "ACTION_MENU_TOP_LEVEL_LIMIT": {
         "value": 6,
         "description": "The maximum number of top level extension items that can be added to an extension action context menu. Any items beyond this limit will be ignored."
       }
     },
     "types": [
       {
         "id": "ContextType",
         "type": "string",
-        "enum": ["all", "page", "frame", "selection", "link", "editable", "password", "image", "video", "audio", "launcher", "browser_action", "page_action", "tab"],
-        "description": "The different contexts a menu can appear in. Specifying 'all' is equivalent to the combination of all other contexts except for 'tab'."
+        "enum": ["all", "page", "frame", "selection", "link", "editable", "password", "image", "video", "audio", "launcher", "browser_action", "page_action", "tab", "tools_menu"],
+        "description": "The different contexts a menu can appear in. Specifying 'all' is equivalent to the combination of all other contexts except for 'tab' and 'tools_menu'."
       },
       {
         "id": "ItemType",
         "type": "string",
         "enum": ["normal", "checkbox", "radio", "separator"],
         "description": "The type of menu item."
       },
       {
--- a/browser/components/extensions/test/browser/browser_ext_menus.js
+++ b/browser/components/extensions/test/browser/browser_ext_menus.js
@@ -250,8 +250,64 @@ add_task(async function test_multiple_co
   await closeExtensionContextMenu(popup.firstChild);
 
   const info = await extension.awaitMessage("click");
   is(info.menuItemId, "child", "onClicked the correct item");
 
   await BrowserTestUtils.removeTab(tab);
   await extension.unload();
 });
+
+add_task(async function test_tools_menu() {
+  const first = ExtensionTestUtils.loadExtension({
+    manifest: {
+      permissions: ["menus"],
+    },
+    async background() {
+      await browser.menus.create({title: "alpha", contexts: ["tools_menu"]});
+      await browser.menus.create({title: "beta", contexts: ["tools_menu"]});
+      browser.test.sendMessage("ready");
+    },
+  });
+
+  const second = ExtensionTestUtils.loadExtension({
+    manifest: {
+      permissions: ["menus"],
+    },
+    async background() {
+      await browser.menus.create({title: "gamma", contexts: ["tools_menu"]});
+      browser.menus.onClicked.addListener((info, tab) => {
+        browser.test.sendMessage("click", {info, tab});
+      });
+
+      const [tab] = await browser.tabs.query({active: true});
+      browser.test.sendMessage("ready", tab.id);
+    },
+  });
+
+  const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com/");
+  await first.startup();
+  await second.startup();
+
+  await first.awaitMessage("ready");
+  const tabId = await second.awaitMessage("ready");
+  const menu = await openToolsMenu();
+
+  const [separator, submenu, gamma] = Array.from(menu.children).slice(-3);
+  is(separator.tagName, "menuseparator", "Separator before first extension item");
+
+  is(submenu.tagName, "menu", "Correct submenu type");
+  is(submenu.getAttribute("label"), "Generated extension", "Correct submenu title");
+  is(submenu.firstChild.children.length, 2, "Correct number of submenu items");
+
+  is(gamma.tagName, "menuitem", "Third menu item type is correct");
+  is(gamma.getAttribute("label"), "gamma", "Third menu item label is correct");
+
+  closeToolsMenu(gamma);
+
+  const click = await second.awaitMessage("click");
+  is(click.info.pageUrl, "http://example.com/", "Click info pageUrl is correct");
+  is(click.tab.id, tabId, "Click event tab ID is correct");
+
+  await BrowserTestUtils.removeTab(tab);
+  await first.unload();
+  await second.unload();
+});
--- a/browser/components/extensions/test/browser/head.js
+++ b/browser/components/extensions/test/browser/head.js
@@ -8,16 +8,17 @@
  *          getBrowserActionPopup getPageActionPopup
  *          closeBrowserAction closePageAction
  *          promisePopupShown promisePopupHidden
  *          openContextMenu closeContextMenu
  *          openContextMenuInSidebar openContextMenuInPopup
  *          openExtensionContextMenu closeExtensionContextMenu
  *          openActionContextMenu openSubmenu closeActionContextMenu
  *          openTabContextMenu closeTabContextMenu
+ *          openToolsMenu closeToolsMenu
  *          imageBuffer imageBufferFromDataURI
  *          getListStyleImage getPanelForNode
  *          awaitExtensionPanel awaitPopupResize
  *          promiseContentDimensions alterContent
  *          promisePrefChangeObserved openContextMenuInFrame
  *          promiseAnimationFrame getCustomizableUIPanelID
  */
 
@@ -331,16 +332,45 @@ async function closeExtensionContextMenu
   let popupHiddenPromise = BrowserTestUtils.waitForEvent(contentAreaContextMenu, "popuphidden");
   EventUtils.synthesizeMouseAtCenter(itemToSelect, modifiers);
   await popupHiddenPromise;
 
   // Bug 1351638: parent menu fails to close intermittently, make sure it does.
   contentAreaContextMenu.hidePopup();
 }
 
+async function openToolsMenu(win = window) {
+  const node = win.document.getElementById("tools-menu");
+  const menu = win.document.getElementById("menu_ToolsPopup");
+  const shown = BrowserTestUtils.waitForEvent(menu, "popupshown");
+  if (AppConstants.platform === "macosx") {
+    // We can't open menubar items on OSX, so mocking instead.
+    menu.dispatchEvent(new MouseEvent("popupshowing"));
+    menu.dispatchEvent(new MouseEvent("popupshown"));
+  } else {
+    node.open = true;
+  }
+  await shown;
+  return menu;
+}
+
+function closeToolsMenu(itemToSelect, win = window) {
+  const menu = win.document.getElementById("menu_ToolsPopup");
+  const hidden = BrowserTestUtils.waitForEvent(menu, "popuphidden");
+  if (AppConstants.platform === "macosx") {
+    // Mocking on OSX, see above.
+    itemToSelect.doCommand();
+    menu.dispatchEvent(new MouseEvent("popuphiding"));
+    menu.dispatchEvent(new MouseEvent("popuphidden"));
+  } else {
+    EventUtils.synthesizeMouseAtCenter(itemToSelect, {}, win);
+  }
+  return hidden;
+}
+
 async function openChromeContextMenu(menuId, target, win = window) {
   const node = win.document.querySelector(target);
   const menu = win.document.getElementById(menuId);
   const shown = BrowserTestUtils.waitForEvent(menu, "popupshown");
   EventUtils.synthesizeMouseAtCenter(node, {type: "contextmenu"}, win);
   await shown;
   return menu;
 }