Bug 1457474 - Add 'Manage Extension' in browserAction context menu draft
authorOriol Brufau <oriol-bugzilla@hotmail.com>
Mon, 14 May 2018 22:23:21 +0200
changeset 797847 17d8785ef981b42395b5d63e245740ac9014ac57
parent 797516 f980d3e2fef7b4f94ea7e709982b0bbf35721150
push id110600
push userbmo:oriol-bugzilla@hotmail.com
push dateMon, 21 May 2018 20:34:39 +0000
bugs1457474
milestone62.0a1
Bug 1457474 - Add 'Manage Extension' in browserAction context menu MozReview-Commit-ID: 3iA7fCeXLVs
browser/base/content/browser.js
browser/base/content/browser.xul
browser/components/customizableui/content/panelUI.inc.xul
browser/components/extensions/parent/ext-browserAction.js
browser/components/extensions/test/browser/browser_ext_browserAction_contextMenu.js
browser/locales/en-US/chrome/browser/browser.dtd
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -6385,16 +6385,39 @@ function UpdateDownloadsAutoHide(popup) 
   }
 }
 
 function onDownloadsAutoHideChange(event) {
   let autoHide = event.target.getAttribute("checked") == "true";
   Services.prefs.setBoolPref("browser.download.autohideButton", autoHide);
 }
 
+function getUnwrappedTriggerNode(popup) {
+  // Toolbar buttons are wrapped in customize mode. Unwrap if necessary.
+  let {triggerNode} = popup;
+  if (triggerNode && gCustomizeMode.isWrappedToolbarItem(triggerNode)) {
+    return triggerNode.firstChild;
+  }
+  return triggerNode;
+}
+
+function UpdateManageExtension(popup) {
+  let checkbox = popup.querySelector(".customize-context-manageExtension");
+  let separator = checkbox.nextElementSibling;
+  let node = getUnwrappedTriggerNode(popup);
+  let isWebExt = node && node.hasAttribute("data-extensionid");
+  checkbox.hidden = separator.hidden = !isWebExt;
+}
+
+function openAboutAddonsForContextAction(popup) {
+  let id = getUnwrappedTriggerNode(popup).getAttribute("data-extensionid");
+  let viewID = "addons://detail/" + encodeURIComponent(id);
+  BrowserOpenAddonsMgr(viewID);
+}
+
 var gPageStyleMenu = {
   // This maps from a <browser> element (or, more specifically, a
   // browser's permanentKey) to an Object that contains the most recent
   // information about the browser content's stylesheets. That Object
   // is populated via the PageStyle:StyleSheets message from the content
   // process. The Object should have the following structure:
   //
   // filteredStyleSheets (Array):
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -328,17 +328,23 @@
                      oncommand="SidebarUI.reversePosition()"/>
       <toolbarseparator/>
       <toolbarbutton label="&sidebarMenuClose.label;"
                      class="subviewbutton"
                      oncommand="SidebarUI.hide()"/>
     </panel>
 
     <menupopup id="toolbar-context-menu"
-               onpopupshowing="onViewToolbarsPopupShowing(event, document.getElementById('viewToolbarsMenuSeparator')); UpdateDownloadsAutoHide(this)">
+               onpopupshowing="onViewToolbarsPopupShowing(event, document.getElementById('viewToolbarsMenuSeparator')); UpdateDownloadsAutoHide(this); UpdateManageExtension(this)">
+      <menuitem oncommand="openAboutAddonsForContextAction(this.parentElement)"
+                accesskey="&customizeMenu.manageExtension.accesskey;"
+                label="&customizeMenu.manageExtension.label;"
+                contexttype="toolbaritem"
+                class="customize-context-manageExtension"/>
+      <menuseparator/>
       <menuitem oncommand="gCustomizeMode.addToPanel(document.popupNode)"
                 accesskey="&customizeMenu.pinToOverflowMenu.accesskey;"
                 label="&customizeMenu.pinToOverflowMenu.label;"
                 contexttype="toolbaritem"
                 class="customize-context-moveToPanel"/>
       <menuitem oncommand="onDownloadsAutoHideChange(event)"
                 type="checkbox"
                 accesskey="&customizeMenu.autoHideDownloadsButton.accesskey;"
--- a/browser/components/customizableui/content/panelUI.inc.xul
+++ b/browser/components/customizableui/content/panelUI.inc.xul
@@ -24,17 +24,23 @@
                       accesskey="&overflowCustomizeToolbar.accesskey;"
                       label="&overflowCustomizeToolbar.label;"/>
     </panelview>
   </panelmultiview>
   <!-- This menu is here because not having it in the menu in which it's used flickers
        when hover styles overlap. See https://bugzilla.mozilla.org/show_bug.cgi?id=1378427 .
        -->
   <menupopup id="customizationPanelItemContextMenu"
-             onpopupshowing="gCustomizeMode.onPanelContextMenuShowing(event)">
+             onpopupshowing="gCustomizeMode.onPanelContextMenuShowing(event); UpdateManageExtension(this)">
+    <menuitem oncommand="openAboutAddonsForContextAction(this.parentElement)"
+              accesskey="&customizeMenu.manageExtension.accesskey;"
+              label="&customizeMenu.manageExtension.label;"
+              contexttype="toolbaritem"
+              class="customize-context-manageExtension"/>
+    <menuseparator/>
     <menuitem oncommand="gCustomizeMode.addToPanel(document.popupNode)"
               id="customizationPanelItemContextMenuPin"
               accesskey="&customizeMenu.pinToOverflowMenu.accesskey;"
               label="&customizeMenu.pinToOverflowMenu.label;"
               closemenu="single"
               class="customize-context-moveToPanel"/>
     <menuitem oncommand="gCustomizeMode.addToToolbar(document.popupNode)"
               id="customizationPanelItemContextMenuUnpin"
--- a/browser/components/extensions/parent/ext-browserAction.js
+++ b/browser/components/extensions/parent/ext-browserAction.js
@@ -175,16 +175,17 @@ this.browserAction = class extends Exten
           view.remove();
         }
       },
 
       onCreated: node => {
         node.classList.add("badged-button");
         node.classList.add("webextension-browser-action");
         node.setAttribute("constrain-size", "true");
+        node.setAttribute("data-extensionid", this.extension.id);
 
         node.onmousedown = event => this.handleEvent(event);
         node.onmouseover = event => this.handleEvent(event);
         node.onmouseout = event => this.handleEvent(event);
 
         this.updateButton(node, this.globals, true);
       },
 
--- a/browser/components/extensions/test/browser/browser_ext_browserAction_contextMenu.js
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_contextMenu.js
@@ -99,8 +99,151 @@ add_task(async function browseraction_po
   let item = contentAreaContextMenu.querySelector("#context-viewimageinfo");
   ok(!item.hidden);
   ok(item.disabled);
 
   await closeContextMenu(contentAreaContextMenu);
 
   await extension.unload();
 });
+
+add_task(async function browseraction_contextmenu_manage_extension() {
+  let id = "addon_id@example.com";
+  let buttonId = `${makeWidgetId(id)}-browser-action`;
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      "applications": {
+        "gecko": {id},
+      },
+      "browser_action": {},
+      "options_ui": {
+        "page": "options.html",
+      },
+    },
+    useAddonManager: "temporary",
+    files: {
+      "options.html": `<script src="options.js"></script>`,
+      "options.js": `browser.test.sendMessage("options-loaded");`,
+    },
+  });
+
+  function openContextMenu(menuId, targetId) {
+    return openChromeContextMenu(menuId, "#" + CSS.escape(targetId));
+  }
+
+  function checkVisibility(menu, visible) {
+    let manageExtension = menu.querySelector(".customize-context-manageExtension");
+    let separator = manageExtension.nextElementSibling;
+
+    info(`Check visibility`);
+    is(manageExtension.hidden, !visible, `Manage Extension should be ${visible ? "visible" : "hidden"}`);
+    is(separator.hidden, !visible, `Separator after Manage Extension should be ${visible ? "visible" : "hidden"}`);
+  }
+
+  async function testContextMenu(menuId, customizing) {
+    info(`Open browserAction context menu in ${menuId}`);
+    let menu = await openContextMenu(menuId, buttonId);
+    await checkVisibility(menu, true);
+
+    info(`Choosing 'Manage Extension' in ${menuId} should load options`);
+    let optionsLoaded = extension.awaitMessage("options-loaded");
+    let manageExtension = menu.querySelector(".customize-context-manageExtension");
+    await closeChromeContextMenu(menuId, manageExtension);
+    await optionsLoaded;
+
+    info(`Remove the opened tab, and await customize mode to be restored if necessary`);
+    let tab = gBrowser.selectedTab;
+    is(tab.linkedBrowser.currentURI.spec, "about:addons");
+    if (customizing) {
+      let customizationReady = BrowserTestUtils.waitForEvent(gNavToolbox, "customizationready");
+      gBrowser.removeTab(tab);
+      await customizationReady;
+    } else {
+      gBrowser.removeTab(tab);
+    }
+
+    return menu;
+  }
+
+  function waitForElementShown(element) {
+    let win = element.ownerGlobal;
+    let dwu = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+    return BrowserTestUtils.waitForCondition(() => {
+      info("Waiting for overflow button to have non-0 size");
+      let bounds = dwu.getBoundsWithoutFlushing(element);
+      return bounds.width > 0 && bounds.height > 0;
+    });
+  }
+
+  async function main(customizing) {
+    if (customizing) {
+      info("Enter customize mode");
+      let customizationReady = BrowserTestUtils.waitForEvent(gNavToolbox, "customizationready");
+      gCustomizeMode.enter();
+      await customizationReady;
+    }
+
+    info("Test toolbar context menu in browserAction");
+    let toolbarCtxMenu = await testContextMenu("toolbar-context-menu", customizing);
+
+    info("Check toolbar context menu in another button");
+    let otherButtonId = "home-button";
+    await openContextMenu(toolbarCtxMenu.id, otherButtonId);
+    checkVisibility(toolbarCtxMenu, false);
+    toolbarCtxMenu.hidePopup();
+
+    info("Check toolbar context menu without triggerNode");
+    toolbarCtxMenu.openPopup();
+    checkVisibility(toolbarCtxMenu, false);
+    toolbarCtxMenu.hidePopup();
+
+    info("Pin the browserAction and another button to the overflow menu");
+    CustomizableUI.addWidgetToArea(buttonId, CustomizableUI.AREA_FIXED_OVERFLOW_PANEL);
+    CustomizableUI.addWidgetToArea(otherButtonId, CustomizableUI.AREA_FIXED_OVERFLOW_PANEL);
+
+    info("Wait until the overflow menu is ready");
+    let overflowButton = document.getElementById("nav-bar-overflow-button");
+    let icon = document.getAnonymousElementByAttribute(overflowButton, "class", "toolbarbutton-icon");
+    await waitForElementShown(icon);
+
+    if (!customizing) {
+      info("Open overflow menu");
+      let menu = document.getElementById("widget-overflow");
+      let shown = BrowserTestUtils.waitForEvent(menu, "popupshown");
+      overflowButton.click();
+      await shown;
+    }
+
+    info("Check overflow menu context menu in another button");
+    let overflowMenuCtxMenu = await openContextMenu("customizationPanelItemContextMenu", otherButtonId);
+    checkVisibility(overflowMenuCtxMenu, false);
+    overflowMenuCtxMenu.hidePopup();
+
+    info("Test overflow menu context menu in browserAction");
+    await testContextMenu(overflowMenuCtxMenu.id, customizing);
+
+    info("Restore initial state");
+    CustomizableUI.addWidgetToArea(buttonId, CustomizableUI.AREA_NAVBAR);
+    CustomizableUI.addWidgetToArea(otherButtonId, CustomizableUI.AREA_NAVBAR);
+
+    if (customizing) {
+      info("Exit customize mode");
+      let afterCustomization = BrowserTestUtils.waitForEvent(gNavToolbox, "aftercustomization");
+      gCustomizeMode.exit();
+      await afterCustomization;
+    }
+  }
+
+  await extension.startup();
+
+  info("Add a dummy tab to prevent about:addons from being loaded in the initial about:blank tab");
+  let dummyTab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com", true, true);
+
+  info("Run tests in normal mode");
+  await main(false);
+
+  info("Run tests in customize mode");
+  await main(true);
+
+  info("Close the dummy tab and finish");
+  gBrowser.removeTab(dummyTab);
+  await extension.unload();
+});
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -423,16 +423,18 @@ These should match what Safari and other
 <!ENTITY customizeMenu.removeFromToolbar.label "Remove from Toolbar">
 <!ENTITY customizeMenu.removeFromToolbar.accesskey "R">
 <!ENTITY customizeMenu.removeFromMenu.label "Remove from Menu">
 <!ENTITY customizeMenu.removeFromMenu.accesskey "R">
 <!ENTITY customizeMenu.addMoreItems.label "Add More Items…">
 <!ENTITY customizeMenu.addMoreItems.accesskey "A">
 <!ENTITY customizeMenu.autoHideDownloadsButton.label "Auto-Hide in Toolbar">
 <!ENTITY customizeMenu.autoHideDownloadsButton.accesskey "A">
+<!ENTITY customizeMenu.manageExtension.label "Manage Extension">
+<!ENTITY customizeMenu.manageExtension.accesskey "E">
 
 <!-- LOCALIZATION NOTE (moreMenu.label) This label is used in the new Photon
     app (hamburger) menu. When clicked, it opens a subview that contains
     secondary commands. -->
 <!ENTITY moreMenu.label "More">
 
 <!ENTITY openCmd.commandkey           "l">
 <!ENTITY urlbar.placeholder2          "Search or enter address">