Bug 1246035 - Add support for _execute_page_action. r=kmag
☠☠ backed out by 3d37c7e5b8dd ☠ ☠
authorMatthew Wein <mwein@mozilla.com>
Mon, 14 Mar 2016 14:54:57 +0100
changeset 289342 70960616621c6d2fce52edc827a7600aeb2087a3
parent 289341 a3ed682cb1815f4e712aaee9e65080d9f7a40910
child 289343 74176e6545b138d1c84d4d6a2eb6a04c414d9f68
push id73798
push usercbook@mozilla.com
push dateFri, 18 Mar 2016 15:10:54 +0000
treeherdermozilla-inbound@5096e12520cd [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskmag
bugs1246035
milestone48.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 1246035 - Add support for _execute_page_action. r=kmag MozReview-Commit-ID: LPQAC7uJTkr
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/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;
 });
+
+