Bug 1268020 - Implement "tools_menu" context draft
authorTomislav Jovanovic <tomica@gmail.com>
Sun, 22 Jan 2017 19:05:30 +0100
changeset 493552 5f611fce3944c7cb9261adbf8bfa8196b1096467
parent 491167 ec8ac10801d6c054a3e74f89fd1857c0bdac638b
child 547893 70e09b31379fb35e8d035370edd01b406e760f70
push id47800
push userbmo:tomica@gmail.com
push dateSun, 05 Mar 2017 00:06:08 +0000
bugs1268020
milestone54.0a1
Bug 1268020 - Implement "tools_menu" context MozReview-Commit-ID: JRJSIYDR0JM
browser/components/extensions/ext-contextMenus.js
browser/components/extensions/schemas/context_menus.json
browser/components/extensions/test/browser/browser_ext_contextMenus_chrome.js
browser/components/extensions/test/browser/head.js
--- a/browser/components/extensions/ext-contextMenus.js
+++ b/browser/components/extensions/ext-contextMenus.js
@@ -306,23 +306,29 @@ function getContexts(contextData) {
   if (contextData.onPageAction) {
     contexts.add("page_action");
   }
 
   if (contextData.onBrowserAction) {
     contexts.add("browser_action");
   }
 
+  if (contextData.onTab) {
+    contexts.add("tab");
+  }
+
+  if (contextData.inToolsMenu) {
+    contexts.add("tools_menu");
+  }
+
   if (contexts.size === 0) {
     contexts.add("page");
   }
 
-  if (contextData.onTab) {
-    contexts.add("tab");
-  } else {
+  if (!contextData.onTab && !contextData.inToolsMenu) {
     contexts.add("all");
   }
 
   return contexts;
 }
 
 function MenuItem(extension, createProperties, isRoot = false) {
   this.extension = extension;
@@ -537,70 +543,81 @@ MenuItem.prototype = {
       }
     }
 
     return true;
   },
 };
 
 // While any extensions are active, this Tracker registers to observe/listen
-// for contex-menu events from both content and chrome.
-const contextMenuTracker = {
+// 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", false);
     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", contextMenuTracker);
+    for (const id of menuTracker.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});
     }
   },
 };
 
 var gExtensionCount = 0;
 /* eslint-disable mozilla/balanced-listeners */
 extensions.on("startup", (type, extension) => {
   gContextMenuMap.set(extension, new Map());
   if (++gExtensionCount == 1) {
-    contextMenuTracker.register();
+    menuTracker.register();
   }
 });
 
 extensions.on("shutdown", (type, extension) => {
   gContextMenuMap.delete(extension);
   gRootItems.delete(extension);
   if (--gExtensionCount == 0) {
-    contextMenuTracker.unregister();
+    menuTracker.unregister();
   }
 });
 /* eslint-enable mozilla/balanced-listeners */
 
 extensions.registerSchemaAPI("contextMenus", "addon_parent", context => {
   let {extension} = context;
   return {
     contextMenus: {
--- a/browser/components/extensions/schemas/context_menus.json
+++ b/browser/components/extensions/schemas/context_menus.json
@@ -26,18 +26,18 @@
         "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_contextMenus_chrome.js
+++ b/browser/components/extensions/test/browser/browser_ext_contextMenus_chrome.js
@@ -125,8 +125,64 @@ add_task(function* test_tabContextMenu()
   const click = yield first.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");
 
   yield BrowserTestUtils.removeTab(tab);
   yield first.unload();
   yield second.unload();
 });
+
+add_task(function* test_tools_menu() {
+  const first = ExtensionTestUtils.loadExtension({
+    manifest: {
+      permissions: ["contextMenus"],
+    },
+    async background() {
+      await browser.contextMenus.create({title: "alpha", contexts: ["tools_menu"]});
+      await browser.contextMenus.create({title: "beta", contexts: ["tools_menu"]});
+      browser.test.sendMessage("ready");
+    },
+  });
+
+  const second = ExtensionTestUtils.loadExtension({
+    manifest: {
+      permissions: ["contextMenus"],
+    },
+    async background() {
+      await browser.contextMenus.create({title: "gamma", contexts: ["tools_menu"]});
+      browser.contextMenus.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 = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com/");
+  yield first.startup();
+  yield second.startup();
+
+  yield first.awaitMessage("ready");
+  const tabId = yield second.awaitMessage("ready");
+  const menu = yield 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 = yield 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");
+
+  yield BrowserTestUtils.removeTab(tab);
+  yield first.unload();
+  yield second.unload();
+});
--- a/browser/components/extensions/test/browser/head.js
+++ b/browser/components/extensions/test/browser/head.js
@@ -7,16 +7,17 @@
  *          clickBrowserAction clickPageAction
  *          getBrowserActionPopup getPageActionPopup
  *          closeBrowserAction closePageAction
  *          promisePopupShown promisePopupHidden
  *          openContextMenu closeContextMenu
  *          openExtensionContextMenu closeExtensionContextMenu
  *          openActionContextMenu openSubmenu closeActionContextMenu
  *          openTabContextMenu closeTabContextMenu
+ *          openToolsMenu closeToolsMenu
  *          imageBuffer getListStyleImage getPanelForNode
  *          awaitExtensionPanel awaitPopupResize
  *          promiseContentDimensions alterContent
  */
 
 const {AppConstants} = Cu.import("resource://gre/modules/AppConstants.jsm", {});
 const {CustomizableUI} = Cu.import("resource:///modules/CustomizableUI.jsm", {});
 
@@ -258,16 +259,45 @@ function* openExtensionContextMenu(selec
 
 function* closeExtensionContextMenu(itemToSelect, modifiers = {}) {
   let contentAreaContextMenu = document.getElementById("contentAreaContextMenu");
   let popupHiddenPromise = BrowserTestUtils.waitForEvent(contentAreaContextMenu, "popuphidden");
   EventUtils.synthesizeMouseAtCenter(itemToSelect, modifiers);
   yield popupHiddenPromise;
 }
 
+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;
+}
+
 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);
   yield shown;
   return menu;
 }