Bug 1197422 - Part 3: [webext] Update browserAction API to use the same context tracking code as pageAction. r=billm
authorKris Maglione <maglione.k@gmail.com>
Thu, 15 Oct 2015 15:14:49 -0700
changeset 303497 958f04b7804fa587506b35b30ec616f986b637ca
parent 303496 84df75fc0206c25ef794ffdebf7104c23b30d4de
child 303498 28a844f6cd11a1023f8bb062f52ed81d4b4d6d87
push id1001
push userraliiev@mozilla.com
push dateMon, 18 Jan 2016 19:06:03 +0000
treeherdermozilla-release@8b89261f3ac4 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbillm
bugs1197422
milestone44.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 1197422 - Part 3: [webext] Update browserAction API to use the same context tracking code as pageAction. r=billm
browser/components/extensions/ext-browserAction.js
browser/components/extensions/test/browser/browser.ini
browser/components/extensions/test/browser/browser_ext_browserAction_context.js
--- a/browser/components/extensions/ext-browserAction.js
+++ b/browser/components/extensions/ext-browserAction.js
@@ -28,173 +28,164 @@ var nextActionId = 0;
 // Responsible for the browser_action section of the manifest as well
 // as the associated popup.
 function BrowserAction(options, extension)
 {
   this.extension = extension;
   this.id = makeWidgetId(extension.id) + "-browser-action";
   this.widget = null;
 
-  this.title = new DefaultWeakMap(extension.localize(options.default_title));
-  this.badgeText = new DefaultWeakMap();
-  this.badgeBackgroundColor = new DefaultWeakMap();
-  this.icon = new DefaultWeakMap(IconDetails.normalize({path: options.default_icon}, extension));
-  this.popup = new DefaultWeakMap(options.default_popup);
+  let title = extension.localize(options.default_title || "");
+  let popup = extension.localize(options.default_popup || "");
+  if (popup) {
+    popup = extension.baseURI.resolve(popup);
+  }
+
+  this.defaults = {
+    title: title,
+    badgeText: "",
+    badgeBackgroundColor: null,
+    icon: IconDetails.normalize({ path: options.default_icon }, extension,
+                                null, true),
+    popup: popup,
+  };
+
+  this.tabContext = new TabContext(tab => Object.create(this.defaults),
+                                   extension);
+
+  EventEmitter.decorate(this);
 }
 
 BrowserAction.prototype = {
   build() {
     let widget = CustomizableUI.createWidget({
       id: this.id,
       type: "custom",
       removable: true,
       defaultArea: CustomizableUI.AREA_NAVBAR,
       onBuild: document => {
         let node = document.createElement("toolbarbutton");
         node.id = this.id;
         node.setAttribute("class", "toolbarbutton-1 chromeclass-toolbar-additional badged-button");
         node.setAttribute("constrain-size", "true");
 
-        this.updateTab(null, node);
+        this.updateButton(node, this.defaults);
 
         let tabbrowser = document.defaultView.gBrowser;
-        tabbrowser.tabContainer.addEventListener("TabSelect", this);
 
         node.addEventListener("command", event => {
           let tab = tabbrowser.selectedTab;
           let popup = this.getProperty(tab, "popup");
           if (popup) {
             this.togglePopup(node, popup);
           } else {
             this.emit("click");
           }
         });
 
         return node;
       },
     });
-    this.widget = widget;
-  },
 
-  handleEvent(event) {
-    if (event.type == "TabSelect") {
-      let window = event.target.ownerDocument.defaultView;
-      let tabbrowser = window.gBrowser;
-      let instance = CustomizableUI.getWidget(this.id).forWindow(window);
-      if (instance) {
-        this.updateTab(tabbrowser.selectedTab, instance.node);
-      }
-    }
+    this.tabContext.on("tab-select",
+                       (evt, tab) => { this.updateWindow(tab.ownerDocument.defaultView); })
+
+    this.widget = widget;
   },
 
   togglePopup(node, popupResource) {
     openPanel(node, popupResource, this.extension);
   },
 
-  // Initialize the toolbar icon and popup given that |tab| is the
-  // current tab and |node| is the CustomizableUI node. Note: |tab|
-  // will be null if we don't know the current tab yet (during
-  // initialization).
-  updateTab(tab, node) {
-    let window = node.ownerDocument.defaultView;
-
-    let title = this.getProperty(tab, "title");
-    if (title) {
-      node.setAttribute("tooltiptext", title);
-      node.setAttribute("label", title);
+  // Update the toolbar button |node| with the tab context data
+  // in |tabData|.
+  updateButton(node, tabData) {
+    if (tabData.title) {
+      node.setAttribute("tooltiptext", tabData.title);
+      node.setAttribute("label", tabData.title);
+      node.setAttribute("aria-label", tabData.title);
     } else {
       node.removeAttribute("tooltiptext");
       node.removeAttribute("label");
+      node.removeAttribute("aria-label");
     }
 
-    let badgeText = this.badgeText.get(tab);
-    if (badgeText) {
-      node.setAttribute("badge", badgeText);
+    if (tabData.badgeText) {
+      node.setAttribute("badge", tabData.badgeText);
     } else {
       node.removeAttribute("badge");
     }
 
-    function toHex(n) {
-      return Math.floor(n / 16).toString(16) + (n % 16).toString(16);
-    }
-
     let badgeNode = node.ownerDocument.getAnonymousElementByAttribute(node,
                                         'class', 'toolbarbutton-badge');
     if (badgeNode) {
-      let color = this.badgeBackgroundColor.get(tab);
+      let color = tabData.badgeBackgroundColor;
       if (Array.isArray(color)) {
         color = `rgb(${color[0]}, ${color[1]}, ${color[2]})`;
       }
       badgeNode.style.backgroundColor = color || "";
     }
 
-    let iconURL = this.getIcon(tab, node);
+    let iconURL = IconDetails.getURL(
+      tabData.icon, node.ownerDocument.defaultView, this.extension);
     node.setAttribute("image", iconURL);
   },
 
-  // Note: tab is allowed to be null here.
-  getIcon(tab, node) {
-    let icon = this.icon.get(tab);
-    return IconDetails.getURL(icon, node.ownerDocument.defaultView,
-                              this.extension);
-  },
-
   // Update the toolbar button for a given window.
   updateWindow(window) {
-    let tab = window.gBrowser ? window.gBrowser.selectedTab : null;
-    let node = CustomizableUI.getWidget(this.id).forWindow(window).node;
-    this.updateTab(tab, node);
+    let widget = this.widget.forWindow(window);
+    if (widget) {
+      let tab = window.gBrowser.selectedTab;
+      this.updateButton(widget.node, this.tabContext.get(tab));
+    }
   },
 
   // Update the toolbar button when the extension changes the icon,
   // title, badge, etc. If it only changes a parameter for a single
   // tab, |tab| will be that tab. Otherwise it will be null.
   updateOnChange(tab) {
     if (tab) {
       if (tab.selected) {
         this.updateWindow(tab.ownerDocument.defaultView);
       }
     } else {
-      let e = Services.wm.getEnumerator("navigator:browser");
-      while (e.hasMoreElements()) {
-        let window = e.getNext();
-        if (window.gBrowser) {
-          this.updateWindow(window);
-        }
+      for (let window of WindowListManager.browserWindows()) {
+        this.updateWindow(window);
       }
     }
   },
 
   // tab is allowed to be null.
   // prop should be one of "icon", "title", "badgeText", "popup", or "badgeBackgroundColor".
   setProperty(tab, prop, value) {
-    this[prop].set(tab, value);
+    if (tab == null) {
+      this.defaults[prop] = value;
+    } else {
+      this.tabContext.get(tab)[prop] = value;
+    }
+
     this.updateOnChange(tab);
   },
 
   // tab is allowed to be null.
   // prop should be one of "title", "badgeText", "popup", or "badgeBackgroundColor".
   getProperty(tab, prop) {
-    return this[prop].get(tab);
+    if (tab == null) {
+      return this.defaults[prop];
+    } else {
+      return this.tabContext.get(tab)[prop];
+    }
   },
 
   shutdown() {
-    let widget = CustomizableUI.getWidget(this.id);
-    for (let instance of widget.instances) {
-      let window = instance.node.ownerDocument.defaultView;
-      let tabbrowser = window.gBrowser;
-      tabbrowser.tabContainer.removeEventListener("TabSelect", this);
-    }
-
+    this.tabContext.shutdown();
     CustomizableUI.destroyWidget(this.id);
   },
 };
 
-EventEmitter.decorate(BrowserAction.prototype);
-
 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) => {
   if (browserActionMap.has(extension)) {
@@ -242,17 +233,23 @@ extensions.registerAPI((extension, conte
       getBadgeText: function(details, callback) {
         let tab = details.tabId ? TabManager.getTab(details.tabId) : null;
         let text = browserActionOf(extension).getProperty(tab, "badgeText");
         runSafe(context, callback, text);
       },
 
       setPopup: function(details) {
         let tab = details.tabId ? TabManager.getTab(details.tabId) : null;
-        browserActionOf(extension).setProperty(tab, "popup", details.popup);
+        // 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);
       },
 
       getPopup: function(details, callback) {
         let tab = details.tabId ? TabManager.getTab(details.tabId) : null;
         let popup = browserActionOf(extension).getProperty(tab, "popup");
         runSafe(context, callback, popup);
       },
 
--- a/browser/components/extensions/test/browser/browser.ini
+++ b/browser/components/extensions/test/browser/browser.ini
@@ -3,16 +3,17 @@ support-files =
   head.js
   context.html
   ctxmenu-image.png
 
 [browser_ext_simple.js]
 [browser_ext_currentWindow.js]
 [browser_ext_browserAction_simple.js]
 [browser_ext_browserAction_pageAction_icon.js]
+[browser_ext_browserAction_context.js]
 [browser_ext_pageAction_context.js]
 [browser_ext_pageAction_popup.js]
 [browser_ext_contextMenus.js]
 [browser_ext_getViews.js]
 [browser_ext_tabs_executeScript.js]
 [browser_ext_tabs_query.js]
 [browser_ext_tabs_update.js]
 [browser_ext_tabs_sendMessage.js]
copy from browser/components/extensions/test/browser/browser_ext_pageAction_context.js
copy to browser/components/extensions/test/browser/browser_ext_browserAction_context.js
--- a/browser/components/extensions/test/browser/browser_ext_pageAction_context.js
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_context.js
@@ -1,161 +1,187 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 add_task(function* testTabSwitchContext() {
 
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
-      "page_action": {
+      "browser_action": {
         "default_icon": "default.png",
         "default_popup": "default.html",
         "default_title": "Default Title",
       },
       "permissions": ["tabs"],
     },
 
     background: function () {
       var details = [
         { "icon": browser.runtime.getURL("default.png"),
           "popup": browser.runtime.getURL("default.html"),
-          "title": "Default Title" },
+          "title": "Default Title",
+          "badge": "",
+          "badgeBackgroundColor": null },
         { "icon": browser.runtime.getURL("1.png"),
           "popup": browser.runtime.getURL("default.html"),
-          "title": "Default Title" },
+          "title": "Default Title",
+          "badge": "",
+          "badgeBackgroundColor": null },
         { "icon": browser.runtime.getURL("2.png"),
           "popup": browser.runtime.getURL("2.html"),
-          "title": "Title 2" },
+          "title": "Title 2",
+          "badge": "2",
+          "badgeBackgroundColor": [0xff, 0, 0] },
+        { "icon": browser.runtime.getURL("1.png"),
+          "popup": browser.runtime.getURL("default-2.html"),
+          "title": "Default Title 2",
+          "badge": "d2",
+          "badgeBackgroundColor": [0, 0xff, 0] },
+        { "icon": browser.runtime.getURL("default-2.png"),
+          "popup": browser.runtime.getURL("default-2.html"),
+          "title": "Default Title 2",
+          "badge": "d2",
+          "badgeBackgroundColor": [0, 0xff, 0] },
       ];
 
       var tabs = [];
 
       var tests = [
         expect => {
-          browser.test.log("Initial state. No icon visible.");
-          expect(null);
-        },
-        expect => {
-          browser.test.log("Show the icon on the first tab, expect default properties.");
-          browser.pageAction.show(tabs[0]);
+          browser.test.log("Initial state, expect default properties.");
           expect(details[0]);
         },
         expect => {
-          browser.test.log("Change the icon. Expect default properties excluding the icon.");
-          browser.pageAction.setIcon({ tabId: tabs[0], path: "1.png" });
+          browser.test.log("Change the icon in the current tab. Expect default properties excluding the icon.");
+          browser.browserAction.setIcon({ tabId: tabs[0], path: "1.png" });
           expect(details[1]);
         },
         expect => {
-          browser.test.log("Create a new tab. No icon visible.");
+          browser.test.log("Create a new tab. Expect default properties.");
           browser.tabs.create({ active: true, url: "about:blank?0" }, tab => {
             tabs.push(tab.id);
-            expect(null);
+            expect(details[0]);
           });
         },
         expect => {
           browser.test.log("Change properties. Expect new properties.");
           var tabId = tabs[1];
-          browser.pageAction.show(tabId);
-          browser.pageAction.setIcon({ tabId, path: "2.png" });
-          browser.pageAction.setPopup({ tabId, popup: "2.html" });
-          browser.pageAction.setTitle({ tabId, title: "Title 2" });
+          browser.browserAction.setIcon({ tabId, path: "2.png" });
+          browser.browserAction.setPopup({ tabId, popup: "2.html" });
+          browser.browserAction.setTitle({ tabId, title: "Title 2" });
+          browser.browserAction.setBadgeText({ tabId, text: "2" });
+          browser.browserAction.setBadgeBackgroundColor({ tabId, color: [0xff, 0, 0] });
 
           expect(details[2]);
         },
         expect => {
-          browser.test.log("Navigate to a new page. Expect icon hidden.");
+          browser.test.log("Navigate to a new page. Expect no changes.");
 
           // TODO: This listener should not be necessary, but the |tabs.update|
           // callback currently fires too early in e10s windows.
           browser.tabs.onUpdated.addListener(function listener(tabId, changed) {
             if (tabId == tabs[1] && changed.url) {
               browser.tabs.onUpdated.removeListener(listener);
-              expect(null);
+              expect(details[2]);
             }
           });
 
           browser.tabs.update(tabs[1], { url: "about:blank?1" });
         },
         expect => {
-          browser.test.log("Show the icon. Expect default properties again.");
-          browser.pageAction.show(tabs[1]);
-          expect(details[0]);
-        },
-        expect => {
           browser.test.log("Switch back to the first tab. Expect previously set properties.");
           browser.tabs.update(tabs[0], { active: true }, () => {
             expect(details[1]);
           });
         },
         expect => {
-          browser.test.log("Hide the icon on tab 2. Switch back, expect hidden.");
-          browser.pageAction.hide(tabs[1]);
+          browser.test.log("Change default values, expect those changes reflected.");
+          browser.browserAction.setIcon({ path: "default-2.png" });
+          browser.browserAction.setPopup({ popup: "default-2.html" });
+          browser.browserAction.setTitle({ title: "Default Title 2" });
+          browser.browserAction.setBadgeText({ text: "d2" });
+          browser.browserAction.setBadgeBackgroundColor({ color: [0, 0xff, 0] });
+          expect(details[3]);
+        },
+        expect => {
+          browser.test.log("Switch back to tab 2. Expect former value, unaffected by changes to defaults in previous step.");
           browser.tabs.update(tabs[1], { active: true }, () => {
-            expect(null);
+            expect(details[2]);
           });
         },
         expect => {
-          browser.test.log("Switch back to tab 1. Expect previous results again.");
+          browser.test.log("Delete tab, switch back to tab 1. Expect previous results again.");
           browser.tabs.remove(tabs[1], () => {
-            expect(details[1]);
+            expect(details[3]);
           });
         },
         expect => {
-          browser.test.log("Hide the icon. Expect hidden.");
-          browser.pageAction.hide(tabs[0]);
-          expect(null);
+          browser.test.log("Create a new tab. Expect new default properties.");
+          browser.tabs.create({ active: true, url: "about:blank?2" }, tab => {
+            tabs.push(tab.id);
+            expect(details[4]);
+          });
+        },
+        expect => {
+          browser.test.log("Delete tab.");
+          browser.tabs.remove(tabs[2], () => {
+            expect(details[3]);
+          });
         },
       ];
 
-      // Gets the current details of the page action, and returns a
+      // Gets the current details of the browser action, and returns a
       // promise that resolves to an object containing them.
       function getDetails() {
         return new Promise(resolve => {
           return browser.tabs.query({ active: true, currentWindow: true }, resolve);
         }).then(tabs => {
           var tabId = tabs[0].id;
 
           return Promise.all([
-            new Promise(resolve => browser.pageAction.getTitle({tabId}, resolve)),
-            new Promise(resolve => browser.pageAction.getPopup({tabId}, resolve))])
+            new Promise(resolve => browser.browserAction.getTitle({tabId}, resolve)),
+            new Promise(resolve => browser.browserAction.getPopup({tabId}, resolve)),
+            new Promise(resolve => browser.browserAction.getBadgeText({tabId}, resolve)),
+            new Promise(resolve => browser.browserAction.getBadgeBackgroundColor({tabId}, resolve))])
         }).then(details => {
           return Promise.resolve({ title: details[0],
-                                   popup: details[1] });
+                                   popup: details[1],
+                                   badge: details[2],
+                                   badgeBackgroundColor: details[3] });
         });
       }
 
 
       // Runs the next test in the `tests` array, checks the results,
       // and passes control back to the outer test scope.
       function nextTest() {
         var test = tests.shift();
 
         test(expecting => {
-          function finish() {
+          // Check that the API returns the expected values, and then
+          // run the next test.
+          getDetails().then(details => {
+            browser.test.assertEq(expecting.title, details.title,
+                                  "expected value from getTitle");
+
+            browser.test.assertEq(expecting.popup, details.popup,
+                                  "expected value from getPopup");
+
+            browser.test.assertEq(expecting.badge, details.badge,
+                                  "expected value from getBadge");
+
+            browser.test.assertEq(String(expecting.badgeBackgroundColor),
+                                  String(details.badgeBackgroundColor),
+                                  "expected value from getBadgeBackgroundColor");
+
             // Check that the actual icon has the expected values, then
             // run the next test.
             browser.test.sendMessage("nextTest", expecting, tests.length);
-          }
-
-          if (expecting) {
-            // Check that the API returns the expected values, and then
-            // run the next test.
-            getDetails().then(details => {
-              browser.test.assertEq(expecting.title, details.title,
-                                    "expected value from getTitle");
-
-              browser.test.assertEq(expecting.popup, details.popup,
-                                    "expected value from getPopup");
-
-              finish();
-            });
-          } else {
-            finish();
-          }
+          });
         });
       }
 
       browser.test.onMessage.addListener((msg) => {
         if (msg != "runNextTest") {
           browser.test.fail("Expecting 'runNextTest' message");
         }
 
@@ -165,30 +191,42 @@ add_task(function* testTabSwitchContext(
       browser.tabs.query({ active: true, currentWindow: true }, resultTabs => {
         tabs[0] = resultTabs[0].id;
 
         nextTest();
       });
     },
   });
 
-  let pageActionId = makeWidgetId(extension.id) + "-page-action";
+  let browserActionId = makeWidgetId(extension.id) + "-browser-action";
 
   function checkDetails(details) {
-    let image = document.getElementById(pageActionId);
-    if (details == null) {
-      ok(image == null || image.hidden, "image is hidden");
-    } else {
-      ok(image, "image exists");
+    let button = document.getElementById(browserActionId);
+
+    ok(button, "button exists");
+
+    is(button.getAttribute("image"), details.icon, "icon URL is correct");
+    is(button.getAttribute("tooltiptext"), details.title, "image title is correct");
+    is(button.getAttribute("label"), details.title, "image label is correct");
+    is(button.getAttribute("aria-label"), details.title, "image aria-label is correct");
+    is(button.getAttribute("badge"), details.badge, "badge text is correct");
 
-      is(image.src, details.icon, "icon URL is correct");
-      is(image.getAttribute("tooltiptext"), details.title, "image title is correct");
-      is(image.getAttribute("aria-label"), details.title, "image aria-label is correct");
-      // TODO: Popup URL.
+    if (details.badge && details.badgeBackgroundColor) {
+      let badge = button.ownerDocument.getAnonymousElementByAttribute(
+        button, 'class', 'toolbarbutton-badge');
+
+      let badgeColor = window.getComputedStyle(badge).backgroundColor;
+      let color = details.badgeBackgroundColor;
+      let expectedColor = `rgb(${color[0]}, ${color[1]}, ${color[2]})`
+
+      is(badgeColor, expectedColor, "badge color is correct");
     }
+
+
+    // TODO: Popup URL.
   }
 
   let awaitFinish = new Promise(resolve => {
     extension.onMessage("nextTest", (expecting, testsRemaining) => {
       checkDetails(expecting);
 
       if (testsRemaining) {
         extension.sendMessage("runNextTest")
@@ -198,12 +236,9 @@ add_task(function* testTabSwitchContext(
     });
   });
 
   yield extension.startup();
 
   yield awaitFinish;
 
   yield extension.unload();
-
-  let node = document.getElementById(pageActionId);
-  is(node, undefined, "pageAction image removed from document");
 });