Bug 1250631 - Implement chrome.contextMenus.onClicked. r=kmag
authorMatthew Wein <mwein@mozilla.com>
Wed, 23 Mar 2016 02:39:10 -0700
changeset 291359 6934dc0c95b8657235da70eb0a37a511998944be
parent 291358 5851bac416b3be028afc426f6f8f7032c89aa2fb
child 291360 7de386dfe2bb8b3014a4ed51c8ee89de5659cbb9
push id19656
push usergwagner@mozilla.com
push dateMon, 04 Apr 2016 13:43:23 +0000
treeherderb2g-inbound@e99061fde28a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskmag
bugs1250631
milestone48.0a1
Bug 1250631 - Implement chrome.contextMenus.onClicked. r=kmag MozReview-Commit-ID: Ic7opjJtmRB
browser/components/extensions/ext-contextMenus.js
browser/components/extensions/test/browser/browser_ext_contextMenus.js
--- a/browser/components/extensions/ext-contextMenus.js
+++ b/browser/components/extensions/ext-contextMenus.js
@@ -15,19 +15,16 @@ var {
 // Map[Extension -> Map[ID -> MenuItem]]
 // Note: we want to enumerate all the menu items so
 // this cannot be a weak map.
 var gContextMenuMap = new Map();
 
 // Map[Extension -> MenuItem]
 var gRootItems = new Map();
 
-// Not really used yet, will be used for event pages.
-var gOnClickedCallbacksMap = new WeakMap();
-
 // If id is not specified for an item we use an integer.
 var gNextMenuItemID = 0;
 
 // Used to assign unique names to radio groups.
 var gNextRadioGroupID = 0;
 
 // The max length of a menu item's label.
 var gMaxLabelLength = 64;
@@ -163,33 +160,39 @@ var gMenuBuilder = {
       }
     }
 
     if (!item.enabled) {
       element.setAttribute("disabled", "true");
     }
 
     element.addEventListener("command", event => {  // eslint-disable-line mozilla/balanced-listeners
+      if (event.target !== event.currentTarget) {
+        return;
+      }
       if (item.type == "checkbox") {
         item.checked = !item.checked;
       } else if (item.type == "radio") {
         // Deselect all radio items in the current radio group.
         for (let child of item.parent.children) {
           if (child.type == "radio" && child.groupName == item.groupName) {
             child.checked = false;
           }
         }
         // Select the clicked radio item.
         item.checked = true;
       }
 
       item.tabManager.addActiveTabPermission();
+
+      let tab = item.tabManager.convert(contextData.tab);
+      let info = item.getClickInfo(contextData, event);
+      item.extension.emit("webext-contextmenu-menuitem-click", info, tab);
       if (item.onclick) {
-        let clickData = item.getClickData(contextData, event);
-        runSafe(item.extContext, item.onclick, clickData);
+        runSafe(item.extContext, item.onclick, info, tab);
       }
     });
 
     return element;
   },
 
   handleEvent: function(event) {
     if (this.xulMenu != event.target || event.type != "popuphidden") {
@@ -385,52 +388,48 @@ MenuItem.prototype = {
 
     let menuMap = gContextMenuMap.get(this.extension);
     menuMap.delete(this.id);
     if (this.root == this) {
       gRootItems.delete(this.extension);
     }
   },
 
-  getClickData(contextData, event) {
+  getClickInfo(contextData, event) {
     let mediaType;
     if (contextData.onVideo) {
       mediaType = "video";
     }
     if (contextData.onAudio) {
       mediaType = "audio";
     }
     if (contextData.onImage) {
       mediaType = "image";
     }
 
-    let clickData = {
+    let info = {
       menuItemId: this.id,
     };
 
     function setIfDefined(argName, value) {
       if (value) {
-        clickData[argName] = value;
+        info[argName] = value;
       }
     }
 
-    let tab = contextData.tab ? TabManager.convert(this.extension, contextData.tab)
-                              : undefined;
-
     setIfDefined("parentMenuItemId", this.parentId);
     setIfDefined("mediaType", mediaType);
     setIfDefined("linkUrl", contextData.linkUrl);
     setIfDefined("srcUrl", contextData.srcUrl);
     setIfDefined("pageUrl", contextData.pageUrl);
     setIfDefined("frameUrl", contextData.frameUrl);
     setIfDefined("selectionText", contextData.selectionText);
     setIfDefined("editable", contextData.onEditableArea);
-    setIfDefined("tab", tab);
 
-    return clickData;
+    return info;
   },
 
   enabledForContext(contextData) {
     let contexts = getContexts(contextData);
     if (!this.contexts.some(n => contexts.has(n))) {
       return false;
     }
 
@@ -502,22 +501,21 @@ extensions.registerSchemaAPI("contextMen
       removeAll: function() {
         let root = gRootItems.get(extension);
         if (root) {
           root.remove();
         }
         return Promise.resolve();
       },
 
-      // TODO: implement this once event pages are ready.
       onClicked: new EventManager(context, "contextMenus.onClicked", fire => {
-        let callback = menuItem => {
-          fire(menuItem.data);
+        let listener = (event, info, tab) => {
+          fire(info, tab);
         };
 
-        gOnClickedCallbacksMap.set(extension, callback);
+        extension.on("webext-contextmenu-menuitem-click", listener);
         return () => {
-          gOnClickedCallbacksMap.delete(extension);
+          extension.off("webext-contextmenu-menuitem-click", listener);
         };
       }).api(),
     },
   };
 });
--- a/browser/components/extensions/test/browser/browser_ext_contextMenus.js
+++ b/browser/components/extensions/test/browser/browser_ext_contextMenus.js
@@ -12,20 +12,24 @@ add_task(function* () {
 
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
       "permissions": ["contextMenus"],
     },
 
     background: function() {
       // A generic onclick callback function.
-      function genericOnClick(info) {
-        browser.test.sendMessage("menuItemClick", info);
+      function genericOnClick(info, tab) {
+        browser.test.sendMessage("onclick", {info, tab});
       }
 
+      browser.contextMenus.onClicked.addListener((info, tab) => {
+        browser.test.sendMessage("browser.contextMenus.onClicked", {info, tab});
+      });
+
       browser.contextMenus.create({
         contexts: ["all"],
         type: "separator",
       });
 
       let contexts = ["page", "selection", "image"];
       for (let i = 0; i < contexts.length; i++) {
         let context = contexts[i];
@@ -34,19 +38,19 @@ add_task(function* () {
           title: title,
           contexts: [context],
           id: "ext-" + context,
           onclick: genericOnClick,
         });
         if (context == "selection") {
           browser.contextMenus.update("ext-selection", {
             title: "selection is: '%s'",
-            onclick: (info) => {
+            onclick: (info, tab) => {
               browser.contextMenus.removeAll();
-              genericOnClick(info);
+              genericOnClick(info, tab);
             },
           });
         }
       }
 
       let parent = browser.contextMenus.create({
         title: "parent",
       });
@@ -75,60 +79,59 @@ add_task(function* () {
         onclick: genericOnClick,
       });
       browser.contextMenus.remove(parentToDel);
 
       browser.contextMenus.create({
         title: "radio-group-1",
         type: "radio",
         checked: true,
-        contexts: ["page"],
         onclick: genericOnClick,
       });
 
       browser.contextMenus.create({
         title: "Checkbox",
         type: "checkbox",
-        contexts: ["page"],
         onclick: genericOnClick,
       });
 
       browser.contextMenus.create({
         title: "radio-group-2",
         type: "radio",
-        contexts: ["page"],
         onclick: genericOnClick,
       });
 
       browser.contextMenus.create({
         title: "radio-group-2",
         type: "radio",
-        contexts: ["page"],
         onclick: genericOnClick,
       });
 
       browser.contextMenus.create({
         type: "separator",
       });
 
       browser.contextMenus.create({
         title: "Checkbox",
         type: "checkbox",
         checked: true,
-        contexts: ["page"],
         onclick: genericOnClick,
       });
 
       browser.contextMenus.create({
         title: "Checkbox",
         type: "checkbox",
-        contexts: ["page"],
         onclick: genericOnClick,
       });
 
+      browser.contextMenus.create({
+        title: "Without onclick property",
+        id: "ext-without-onclick",
+      });
+
       browser.contextMenus.update(parent, {parentId: child2}).then(
         () => {
           browser.test.notifyFail();
         },
         () => {
           browser.test.notifyPass();
         }
       );
@@ -157,30 +160,39 @@ add_task(function* () {
     }, gBrowser.selectedBrowser);
     yield popupShownPromise;
 
     popupShownPromise = BrowserTestUtils.waitForEvent(contentAreaContextMenu, "popupshown");
     EventUtils.synthesizeMouseAtCenter(getTop(), {});
     yield popupShownPromise;
   }
 
-  function* closeContextMenu(itemToSelect, expectedClickInfo) {
-    function checkClickInfo(info) {
+  function* closeContextMenu(itemToSelect, expectedClickInfo, hasOnclickProperty = true) {
+    function checkClickInfo(info, tab) {
       for (let i of Object.keys(expectedClickInfo)) {
         is(info[i], expectedClickInfo[i],
            "click info " + i + " expected to be: " + expectedClickInfo[i] + " but was: " + info[i]);
       }
-      is(expectedClickInfo.pageSrc, info.tab.url);
+      is(expectedClickInfo.pageSrc, tab.url);
     }
     let popupHiddenPromise = BrowserTestUtils.waitForEvent(contentAreaContextMenu, "popuphidden");
     EventUtils.synthesizeMouseAtCenter(itemToSelect, {});
-    let clickInfo = yield extension.awaitMessage("menuItemClick");
+
+    if (hasOnclickProperty) {
+      let {info, tab} = yield extension.awaitMessage("onclick");
+      if (expectedClickInfo) {
+        checkClickInfo(info, tab);
+      }
+    }
+
+    let {info, tab} = yield extension.awaitMessage("browser.contextMenus.onClicked");
     if (expectedClickInfo) {
-      checkClickInfo(clickInfo);
+      checkClickInfo(info, tab);
     }
+
     yield popupHiddenPromise;
   }
 
   function confirmRadioGroupStates(expectedStates) {
     let top = getTop();
 
     let radioItems = top.getElementsByAttribute("type", "radio");
     let radioGroup1 = top.getElementsByAttribute("label", "radio-group-1");
@@ -283,16 +295,29 @@ add_task(function* () {
     selection.addRange(range);
   });
 
   // Bring up context menu again
   yield openExtensionMenu();
 
   // Check some menu items
   top = getTop();
+  items = top.getElementsByAttribute("label", "Without onclick property");
+  is(items.length, 1, "contextMenu item was found (context=page)");
+
+  yield closeContextMenu(items[0], {
+    menuItemId: "ext-without-onclick",
+    pageUrl: "http://mochi.test:8888/browser/browser/components/extensions/test/browser/context.html",
+  }, false /* hasOnclickProperty */);
+
+  // Bring up context menu again
+  yield openExtensionMenu();
+
+  // Check some menu items
+  top = getTop();
   items = top.getElementsByAttribute("label", "selection is: 'just some text 123456789012345678901234567890...'");
   is(items.length, 1, "contextMenu item for selection was found (context=selection)");
   let selectionItem = items[0];
 
   items = top.getElementsByAttribute("label", "selection");
   is(items.length, 0, "contextMenu item label update worked (context=selection)");
 
   yield closeContextMenu(selectionItem, {