Bug 1246035 - Add support for _execute_page_action r=kmag
authorMatthew Wein <mwein@mozilla.com>
Mon, 14 Mar 2016 14:54:57 +0100
changeset 290708 3357cb502d0c5a987083a3d0ea196d3d77fa12bd
parent 290707 334c1b88d28a46e3ebefbd42a55190bee4b3fefc
child 290709 e1f49f739dfde973e064e2419e4a3dafd7f93d37
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
bugs1246035
milestone48.0a1
Bug 1246035 - Add support for _execute_page_action r=kmag MozReview-Commit-ID: LPQAC7uJTkr
browser/components/extensions/.eslintrc
browser/components/extensions/ext-browserAction.js
browser/components/extensions/ext-commands.js
browser/components/extensions/ext-pageAction.js
browser/components/extensions/test/browser/browser.ini
browser/components/extensions/test/browser/browser_ext_commands_execute_page_action.js
browser/components/extensions/test/browser/browser_ext_commands_onCommand.js
--- a/browser/components/extensions/.eslintrc
+++ b/browser/components/extensions/.eslintrc
@@ -2,16 +2,17 @@
   "extends": "../../../toolkit/components/extensions/.eslintrc",
 
   "globals": {
     "AllWindowEvents": true,
     "currentWindow": true,
     "EventEmitter": true,
     "IconDetails": true,
     "makeWidgetId": true,
+    "pageActionFor: true,
     "PanelPopup": true,
     "TabContext": true,
     "ViewPopup": true,
     "WindowEventManager": true,
     "WindowListManager": true,
     "WindowManager": true,
   },
 }
--- a/browser/components/extensions/ext-browserAction.js
+++ b/browser/components/extensions/ext-browserAction.js
@@ -12,20 +12,16 @@ var {
   EventManager,
 } = ExtensionUtils;
 
 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
 // WeakMap[Extension -> BrowserAction]
 var browserActionMap = new WeakMap();
 
-function browserActionOf(extension) {
-  return browserActionMap.get(extension);
-}
-
 // Responsible for the browser_action section of the manifest as well
 // as the associated popup.
 function BrowserAction(options, extension) {
   this.extension = extension;
 
   let widgetId = makeWidgetId(extension.id);
   this.id = `${widgetId}-browser-action`;
   this.viewId = `PanelUI-webext-${widgetId}-browser-action-view`;
@@ -198,16 +194,22 @@ BrowserAction.prototype = {
   },
 
   shutdown() {
     this.tabContext.shutdown();
     CustomizableUI.destroyWidget(this.id);
   },
 };
 
+BrowserAction.for = (extension) => {
+  return browserActionMap.get(extension);
+};
+
+global.browserActionFor = BrowserAction.for;
+
 /* eslint-disable mozilla/balanced-listeners */
 extensions.on("manifest_browser_action", (type, directive, extension, manifest) => {
   let browserAction = new BrowserAction(manifest.browser_action, extension);
   browserAction.build();
   browserActionMap.set(extension, browserAction);
 });
 
 extensions.on("shutdown", (type, extension) => {
@@ -221,89 +223,89 @@ extensions.on("shutdown", (type, extensi
 extensions.registerSchemaAPI("browserAction", null, (extension, context) => {
   return {
     browserAction: {
       onClicked: new EventManager(context, "browserAction.onClicked", fire => {
         let listener = () => {
           let tab = TabManager.activeTab;
           fire(TabManager.convert(extension, tab));
         };
-        browserActionOf(extension).on("click", listener);
+        BrowserAction.for(extension).on("click", listener);
         return () => {
-          browserActionOf(extension).off("click", listener);
+          BrowserAction.for(extension).off("click", listener);
         };
       }).api(),
 
       enable: function(tabId) {
         let tab = tabId !== null ? TabManager.getTab(tabId) : null;
-        browserActionOf(extension).setProperty(tab, "enabled", true);
+        BrowserAction.for(extension).setProperty(tab, "enabled", true);
       },
 
       disable: function(tabId) {
         let tab = tabId !== null ? TabManager.getTab(tabId) : null;
-        browserActionOf(extension).setProperty(tab, "enabled", false);
+        BrowserAction.for(extension).setProperty(tab, "enabled", false);
       },
 
       setTitle: function(details) {
         let tab = details.tabId !== null ? TabManager.getTab(details.tabId) : null;
 
         let title = details.title;
         // Clear the tab-specific title when given a null string.
         if (tab && title == "") {
           title = null;
         }
-        browserActionOf(extension).setProperty(tab, "title", title);
+        BrowserAction.for(extension).setProperty(tab, "title", title);
       },
 
       getTitle: function(details) {
         let tab = details.tabId !== null ? TabManager.getTab(details.tabId) : null;
-        let title = browserActionOf(extension).getProperty(tab, "title");
+        let title = BrowserAction.for(extension).getProperty(tab, "title");
         return Promise.resolve(title);
       },
 
       setIcon: function(details) {
         let tab = details.tabId !== null ? TabManager.getTab(details.tabId) : null;
         let icon = IconDetails.normalize(details, extension, context);
-        browserActionOf(extension).setProperty(tab, "icon", icon);
+        BrowserAction.for(extension).setProperty(tab, "icon", icon);
         return Promise.resolve();
       },
 
       setBadgeText: function(details) {
         let tab = details.tabId !== null ? TabManager.getTab(details.tabId) : null;
-        browserActionOf(extension).setProperty(tab, "badgeText", details.text);
+        BrowserAction.for(extension).setProperty(tab, "badgeText", details.text);
       },
 
       getBadgeText: function(details) {
         let tab = details.tabId !== null ? TabManager.getTab(details.tabId) : null;
-        let text = browserActionOf(extension).getProperty(tab, "badgeText");
+        let text = BrowserAction.for(extension).getProperty(tab, "badgeText");
         return Promise.resolve(text);
       },
 
       setPopup: function(details) {
         let tab = details.tabId !== null ? TabManager.getTab(details.tabId) : null;
         // Note: Chrome resolves arguments to setIcon relative to the calling
         // context, but resolves arguments to setPopup relative to the extension
         // root.
         // For internal consistency, we currently resolve both relative to the
         // calling context.
         let url = details.popup && context.uri.resolve(details.popup);
-        browserActionOf(extension).setProperty(tab, "popup", url);
+        BrowserAction.for(extension).setProperty(tab, "popup", url);
       },
 
       getPopup: function(details) {
         let tab = details.tabId !== null ? TabManager.getTab(details.tabId) : null;
-        let popup = browserActionOf(extension).getProperty(tab, "popup");
+        let popup = BrowserAction.for(extension).getProperty(tab, "popup");
         return Promise.resolve(popup);
       },
 
       setBadgeBackgroundColor: function(details) {
         let tab = details.tabId !== null ? TabManager.getTab(details.tabId) : null;
-        browserActionOf(extension).setProperty(tab, "badgeBackgroundColor", details.color);
+        BrowserAction.for(extension).setProperty(tab, "badgeBackgroundColor", details.color);
       },
 
       getBadgeBackgroundColor: function(details, callback) {
         let tab = details.tabId !== null ? TabManager.getTab(details.tabId) : null;
-        let color = browserActionOf(extension).getProperty(tab, "badgeBackgroundColor");
+        let color = BrowserAction.for(extension).getProperty(tab, "badgeBackgroundColor");
         return Promise.resolve(color);
       },
     },
   };
 });
--- a/browser/components/extensions/ext-commands.js
+++ b/browser/components/extensions/ext-commands.js
@@ -10,87 +10,97 @@ var {
   PlatformInfo,
 } = ExtensionUtils;
 
 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
 // WeakMap[Extension -> CommandList]
 var commandsMap = new WeakMap();
 
-function CommandList(commandsObj, extensionID) {
-  this.commands = this.loadCommandsFromManifest(commandsObj);
-  this.keysetID = `ext-keyset-id-${makeWidgetId(extensionID)}`;
+function CommandList(manifest, extension) {
+  this.extension = extension;
+  this.id = makeWidgetId(extension.id);
   this.windowOpenListener = null;
+
+  // Map[{String} commandName -> {Object} commandProperties]
+  this.commands = this.loadCommandsFromManifest(manifest);
+
+  // WeakMap[Window -> <xul:keyset>]
+  this.keysetsMap = new WeakMap();
+
   this.register();
   EventEmitter.decorate(this);
 }
 
 CommandList.prototype = {
   /**
    * Registers the commands to all open windows and to any which
    * are later created.
    */
   register() {
     for (let window of WindowListManager.browserWindows()) {
-      this.registerKeysToDocument(window.document);
+      this.registerKeysToDocument(window);
     }
 
     this.windowOpenListener = (window) => {
-      this.registerKeysToDocument(window.document);
+      if (!this.keysetsMap.has(window)) {
+        this.registerKeysToDocument(window);
+      }
     };
 
     WindowListManager.addOpenListener(this.windowOpenListener);
   },
 
   /**
    * Unregisters the commands from all open windows and stops commands
    * from being registered to windows which are later created.
    */
   unregister() {
     for (let window of WindowListManager.browserWindows()) {
-      let keyset = window.document.getElementById(this.keysetID);
-      if (keyset) {
-        keyset.remove();
+      if (this.keysetsMap.has(window)) {
+        this.keysetsMap.get(window).remove();
       }
     }
 
     WindowListManager.removeOpenListener(this.windowOpenListener);
   },
 
   /**
    * Creates a Map from commands for each command in the manifest.commands object.
-   * @param {Object} commandsObj The manifest.commands JSON object.
+   * @param {Object} manifest The manifest JSON object.
    */
-  loadCommandsFromManifest(commandsObj) {
+  loadCommandsFromManifest(manifest) {
     let commands = new Map();
     // For Windows, chrome.runtime expects 'win' while chrome.commands
     // expects 'windows'.  We can special case this for now.
     let os = PlatformInfo.os == "win" ? "windows" : PlatformInfo.os;
-    for (let name of Object.keys(commandsObj)) {
-      let command = commandsObj[name];
+    for (let name of Object.keys(manifest.commands)) {
+      let command = manifest.commands[name];
       commands.set(name, {
         description: command.description,
         shortcut: command.suggested_key[os] || command.suggested_key.default,
       });
     }
     return commands;
   },
 
   /**
    * Registers the commands to a document.
-   * @param {Document} doc The XUL document to insert the Keyset.
+   * @param {ChromeWindow} window The XUL window to insert the Keyset.
    */
-  registerKeysToDocument(doc) {
+  registerKeysToDocument(window) {
+    let doc = window.document;
     let keyset = doc.createElementNS(XUL_NS, "keyset");
-    keyset.id = this.keysetID;
+    keyset.id = `ext-keyset-id-${this.id}`;
     this.commands.forEach((command, name) => {
       let keyElement = this.buildKey(doc, name, command.shortcut);
       keyset.appendChild(keyElement);
     });
     doc.documentElement.appendChild(keyset);
+    this.keysetsMap.set(window, keyset);
   },
 
   /**
    * Builds a XUL Key element and attaches an onCommand listener which
    * emits a command event with the provided name when fired.
    *
    * @param {Document} doc The XUL document.
    * @param {String} name The name of the command.
@@ -105,17 +115,22 @@ CommandList.prototype = {
     // We need to have the attribute "oncommand" for the "command" listener to fire,
     // and it is currently ignored when set to the empty string.
     keyElement.setAttribute("oncommand", "//");
 
     /* eslint-disable mozilla/balanced-listeners */
     // We remove all references to the key elements when the extension is shutdown,
     // therefore the listeners for these elements will be garbage collected.
     keyElement.addEventListener("command", (event) => {
-      this.emit("command", name);
+      if (name == "_execute_page_action") {
+        let win = event.target.ownerDocument.defaultView;
+        pageActionFor(this.extension).triggerAction(win);
+      } else {
+        this.emit("command", name);
+      }
     });
     /* eslint-enable mozilla/balanced-listeners */
 
     return keyElement;
   },
 
   /**
    * Builds a XUL Key element from the provided shortcut.
@@ -190,17 +205,17 @@ CommandList.prototype = {
       return modifiersMap[modifier];
     }).join(" ");
   },
 };
 
 
 /* eslint-disable mozilla/balanced-listeners */
 extensions.on("manifest_commands", (type, directive, extension, manifest) => {
-  commandsMap.set(extension, new CommandList(manifest.commands, extension.id));
+  commandsMap.set(extension, new CommandList(manifest, extension));
 });
 
 extensions.on("shutdown", (type, extension) => {
   let commandsList = commandsMap.get(extension);
   if (commandsList) {
     commandsList.unregister();
     commandsMap.delete(extension);
   }
--- a/browser/components/extensions/ext-pageAction.js
+++ b/browser/components/extensions/ext-pageAction.js
@@ -1,21 +1,21 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
+Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 var {
   EventManager,
 } = ExtensionUtils;
 
 // WeakMap[Extension -> PageAction]
 var pageActionMap = new WeakMap();
 
-
 // Handles URL bar icons, including the |page_action| manifest entry
 // and associated API.
 function PageAction(options, extension) {
   this.extension = extension;
   this.id = makeWidgetId(extension.id) + "-page-action";
 
   this.tabManager = TabManager.for(extension);
 
@@ -118,16 +118,29 @@ PageAction.prototype = {
     if (!this.buttons.has(window)) {
       let button = this.addButton(window);
       this.buttons.set(window, button);
     }
 
     return this.buttons.get(window);
   },
 
+  /**
+   * Triggers this page action for the given window, with the same effects as
+   * if it were clicked by a user.
+   *
+   * This has no effect if the page action is hidden for the selected tab.
+   */
+  triggerAction(window) {
+    let pageAction = pageActionMap.get(this.extension);
+    if (pageAction.getProperty(window.gBrowser.selectedTab, "show")) {
+      pageAction.handleClick(window);
+    }
+  },
+
   // Handles a click event on the page action button for the given
   // window.
   // If the page action has a |popup| property, a panel is opened to
   // that URL. Otherwise, a "click" event is emitted, and dispatched to
   // the any click listeners in the add-on.
   handleClick(window) {
     let tab = window.gBrowser.selectedTab;
     let popupURL = this.tabContext.get(tab).popup;
@@ -158,35 +171,35 @@ PageAction.prototype = {
     for (let window of WindowListManager.browserWindows()) {
       if (this.buttons.has(window)) {
         this.buttons.get(window).remove();
       }
     }
   },
 };
 
-PageAction.for = extension => {
-  return pageActionMap.get(extension);
-};
-
-
 /* eslint-disable mozilla/balanced-listeners */
 extensions.on("manifest_page_action", (type, directive, extension, manifest) => {
   let pageAction = new PageAction(manifest.page_action, extension);
   pageActionMap.set(extension, pageAction);
 });
 
 extensions.on("shutdown", (type, extension) => {
   if (pageActionMap.has(extension)) {
     pageActionMap.get(extension).shutdown();
     pageActionMap.delete(extension);
   }
 });
 /* eslint-enable mozilla/balanced-listeners */
 
+PageAction.for = extension => {
+  return pageActionMap.get(extension);
+};
+
+global.pageActionFor = PageAction.for;
 
 extensions.registerSchemaAPI("pageAction", null, (extension, context) => {
   return {
     pageAction: {
       onClicked: new EventManager(context, "pageAction.onClicked", fire => {
         let listener = (evt, tab) => {
           fire(TabManager.convert(extension, tab));
         };
--- a/browser/components/extensions/test/browser/browser.ini
+++ b/browser/components/extensions/test/browser/browser.ini
@@ -19,16 +19,17 @@ support-files =
 [browser_ext_browserAction_context.js]
 [browser_ext_browserAction_disabled.js]
 [browser_ext_pageAction_simple.js]
 [browser_ext_pageAction_context.js]
 [browser_ext_pageAction_popup.js]
 [browser_ext_browserAction_popup.js]
 [browser_ext_popup_api_injection.js]
 [browser_ext_contextMenus.js]
+[browser_ext_commands_execute_page_action.js]
 [browser_ext_commands_getAll.js]
 [browser_ext_commands_onCommand.js]
 [browser_ext_getViews.js]
 [browser_ext_lastError.js]
 [browser_ext_runtime_openOptionsPage.js]
 [browser_ext_runtime_setUninstallURL.js]
 [browser_ext_tabs_audio.js]
 [browser_ext_tabs_captureVisibleTab.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_commands_execute_page_action.js
@@ -0,0 +1,135 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(function* test_execute_page_action_without_popup() {
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      "commands": {
+        "_execute_page_action": {
+          "suggested_key": {
+            "default": "Alt+Shift+J",
+          },
+        },
+        "send-keys-command": {
+          "suggested_key": {
+            "default": "Alt+Shift+3",
+          },
+        },
+      },
+      "page_action": {},
+    },
+
+    background: function() {
+      let isShown = false;
+
+      browser.commands.onCommand.addListener((commandName) => {
+        if (commandName == "_execute_page_action") {
+          browser.test.fail(`The onCommand listener should never fire for ${commandName}.`);
+        } else if (commandName == "send-keys-command") {
+          if (!isShown) {
+            isShown = true;
+            browser.tabs.query({currentWindow: true, active: true}, tabs => {
+              tabs.forEach(tab => {
+                browser.pageAction.show(tab.id);
+              });
+              browser.test.sendMessage("send-keys");
+            });
+          }
+        }
+      });
+
+      browser.pageAction.onClicked.addListener(() => {
+        browser.test.assertTrue(isShown, "The onClicked event should fire if the page action is shown.");
+        browser.test.notifyPass("page-action-without-popup");
+      });
+
+      browser.test.sendMessage("send-keys");
+    },
+  });
+
+  yield extension.startup();
+
+  extension.onMessage("send-keys", () => {
+    EventUtils.synthesizeKey("j", {altKey: true, shiftKey: true});
+    EventUtils.synthesizeKey("3", {altKey: true, shiftKey: true});
+  });
+
+  yield extension.awaitFinish("page-action-without-popup");
+  yield extension.unload();
+});
+
+add_task(function* test_execute_page_action_with_popup() {
+  let scriptPage = url => `<html><head><meta charset="utf-8"><script src="${url}"></script></head><body>Test Popup</body></html>`;
+
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      "commands": {
+        "_execute_page_action": {
+          "suggested_key": {
+            "default": "Alt+Shift+J",
+          },
+        },
+        "send-keys-command": {
+          "suggested_key": {
+            "default": "Alt+Shift+3",
+          },
+        },
+      },
+      "page_action": {
+        "default_popup": "popup.html",
+      },
+    },
+
+    files: {
+      "popup.html": scriptPage("popup.js"),
+      "popup.js": function() {
+        browser.runtime.sendMessage("popup-opened");
+      },
+    },
+
+    background: function() {
+      let isShown = false;
+
+      browser.commands.onCommand.addListener((message) => {
+        if (message == "_execute_page_action") {
+          browser.test.fail(`The onCommand listener should never fire for ${message}.`);
+        }
+
+        if (message == "send-keys-command") {
+          if (!isShown) {
+            isShown = true;
+            browser.tabs.query({currentWindow: true, active: true}, tabs => {
+              tabs.forEach(tab => {
+                browser.pageAction.show(tab.id);
+              });
+              browser.test.sendMessage("send-keys");
+            });
+          }
+        }
+      });
+
+      browser.pageAction.onClicked.addListener(() => {
+        browser.test.fail(`The onClicked listener should never fire when the pageAction has a popup.`);
+      });
+
+      browser.runtime.onMessage.addListener(msg => {
+        browser.test.assertEq(msg, "popup-opened", "expected popup opened");
+        browser.test.assertTrue(isShown, "The onClicked event should fire if the page action is shown.");
+        browser.test.notifyPass("page-action-with-popup");
+      });
+
+      browser.test.sendMessage("send-keys");
+    },
+  });
+
+  yield extension.startup();
+
+  extension.onMessage("send-keys", () => {
+    EventUtils.synthesizeKey("j", {altKey: true, shiftKey: true});
+    EventUtils.synthesizeKey("3", {altKey: true, shiftKey: true});
+  });
+
+  yield extension.awaitFinish("page-action-with-popup");
+  yield extension.unload();
+});
--- a/browser/components/extensions/test/browser/browser_ext_commands_onCommand.js
+++ b/browser/components/extensions/test/browser/browser_ext_commands_onCommand.js
@@ -1,39 +1,38 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
-add_task(function* () {
+add_task(function* test_user_defined_commands() {
   // Create a window before the extension is loaded.
   let win1 = yield BrowserTestUtils.openNewBrowserWindow();
   yield BrowserTestUtils.loadURI(win1.gBrowser.selectedBrowser, "about:robots");
   yield BrowserTestUtils.browserLoaded(win1.gBrowser.selectedBrowser);
 
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
-      "name": "Commands Extension",
       "commands": {
         "toggle-feature-using-alt-shift-3": {
           "suggested_key": {
             "default": "Alt+Shift+3",
           },
         },
         "toggle-feature-using-alt-shift-comma": {
           "suggested_key": {
             "default": "Alt+Shift+Comma",
           },
           "unrecognized_property": "with-a-random-value",
         },
       },
     },
 
     background: function() {
-      browser.commands.onCommand.addListener((message) => {
-        browser.test.sendMessage("oncommand", message);
+      browser.commands.onCommand.addListener((commandName) => {
+        browser.test.sendMessage("oncommand", commandName);
       });
       browser.test.sendMessage("ready");
     },
   });
 
 
   SimpleTest.waitForExplicitFinish();
   let waitForConsole = new Promise(resolve => {
@@ -48,20 +47,22 @@ add_task(function* () {
   // Create another window after the extension is loaded.
   let win2 = yield BrowserTestUtils.openNewBrowserWindow();
   yield BrowserTestUtils.loadURI(win2.gBrowser.selectedBrowser, "about:config");
   yield BrowserTestUtils.browserLoaded(win2.gBrowser.selectedBrowser);
 
   // Confirm the keysets have been added to both windows.
   let keysetID = `ext-keyset-id-${makeWidgetId(extension.id)}`;
   let keyset = win1.document.getElementById(keysetID);
-  is(keyset.childNodes.length, 2, "Expected keyset to exist and have 2 children");
+  ok(keyset != null, "Expected keyset to exist");
+  is(keyset.childNodes.length, 2, "Expected keyset to have 2 children");
 
   keyset = win2.document.getElementById(keysetID);
-  is(keyset.childNodes.length, 2, "Expected keyset to exist and have 2 children");
+  ok(keyset != null, "Expected keyset to exist");
+  is(keyset.childNodes.length, 2, "Expected keyset to have 2 children");
 
   // Confirm that the commands are registered to both windows.
   yield focusWindow(win1);
   EventUtils.synthesizeKey("3", {altKey: true, shiftKey: true});
   let message = yield extension.awaitMessage("oncommand");
   is(message, "toggle-feature-using-alt-shift-3", "Expected onCommand listener to fire with correct message");
 
   yield focusWindow(win2);
@@ -79,8 +80,10 @@ add_task(function* () {
   is(keyset, null, "Expected keyset to be removed from the window");
 
   yield BrowserTestUtils.closeWindow(win1);
   yield BrowserTestUtils.closeWindow(win2);
 
   SimpleTest.endMonitorConsole();
   yield waitForConsole;
 });
+
+