Bug 1419893 - Add windowId parameter in browserAction methods draft
authorOriol Brufau <oriol-bugzilla@hotmail.com>
Fri, 06 Apr 2018 23:18:44 +0200
changeset 797321 8a104fa055b034cfc7bdbe188bd9a151f50ee4c1
parent 794547 df5dae2daa1b920a3c94e2647a7bd8c2b4a3d48e
push id110457
push userbmo:oriol-bugzilla@hotmail.com
push dateFri, 18 May 2018 21:38:49 +0000
bugs1419893
milestone62.0a1
Bug 1419893 - Add windowId parameter in browserAction methods MozReview-Commit-ID: FFb4I1wmTH
browser/components/extensions/parent/ext-browser.js
browser/components/extensions/parent/ext-browserAction.js
browser/components/extensions/schemas/browser_action.json
browser/components/extensions/test/browser/browser_ext_browserAction_context.js
--- a/browser/components/extensions/parent/ext-browser.js
+++ b/browser/components/extensions/parent/ext-browser.js
@@ -116,41 +116,64 @@ global.waitForTabLoaded = (tab, url) => 
 global.replaceUrlInTab = (gBrowser, tab, url) => {
   let loaded = waitForTabLoaded(tab, url);
   gBrowser.loadURI(url, {
     flags: Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY,
   });
   return loaded;
 };
 
-// Manages tab-specific context data, and dispatching tab select events
-// across all windows.
+/**
+ * Manages tab-specific and window-specific context data, and dispatches
+ * tab select events across all windows.
+ */
 global.TabContext = class extends EventEmitter {
+  /**
+   * @param {Function} getDefaults
+   *        Provides the context value for a tab or window when there is none.
+   *        Called with a XULElement or ChromeWindow argument.
+   *        The returned value is cached.
+   * @param {Object} extension
+   *        The extension object.
+   */
   constructor(getDefaults, extension) {
     super();
 
     this.extension = extension;
     this.getDefaults = getDefaults;
 
     this.tabData = new WeakMap();
 
     windowTracker.addListener("progress", this);
     windowTracker.addListener("TabSelect", this);
   }
 
-  get(nativeTab) {
-    if (!this.tabData.has(nativeTab)) {
-      this.tabData.set(nativeTab, this.getDefaults(nativeTab));
+  /**
+   * Returns the context data associated with `keyObject`.
+   *
+   * @param {XULElement|ChromeWindow} keyObject
+   *        Browser tab or browser chrome window.
+   * @returns {any}
+   */
+  get(keyObject) {
+    if (!this.tabData.has(keyObject)) {
+      this.tabData.set(keyObject, this.getDefaults(keyObject));
     }
 
-    return this.tabData.get(nativeTab);
+    return this.tabData.get(keyObject);
   }
 
-  clear(nativeTab) {
-    this.tabData.delete(nativeTab);
+  /**
+   * Clears the context data associated with `keyObject`.
+   *
+   * @param {XULElement|ChromeWindow} keyObject
+   *        Browser tab or browser chrome window.
+   */
+  clear(keyObject) {
+    this.tabData.delete(keyObject);
   }
 
   handleEvent(event) {
     if (event.type == "TabSelect") {
       let nativeTab = event.target;
       this.emit("tab-select", nativeTab);
       this.emit("location-change", nativeTab);
     }
@@ -159,16 +182,19 @@ global.TabContext = class extends EventE
   onLocationChange(browser, webProgress, request, locationURI, flags) {
     let gBrowser = browser.ownerGlobal.gBrowser;
     let tab = gBrowser.getTabForBrowser(browser);
     // fromBrowse will be false in case of e.g. a hash change or history.pushState
     let fromBrowse = !(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT);
     this.emit("location-change", tab, fromBrowse);
   }
 
+  /**
+   * Makes the TabContext instance stop emitting events.
+   */
   shutdown() {
     windowTracker.removeListener("progress", this);
     windowTracker.removeListener("TabSelect", this);
   }
 };
 
 
 class WindowTracker extends WindowTrackerBase {
--- a/browser/components/extensions/parent/ext-browserAction.js
+++ b/browser/components/extensions/parent/ext-browserAction.js
@@ -10,29 +10,26 @@ ChromeUtils.defineModuleGetter(this, "se
                                "resource://gre/modules/Timer.jsm");
 ChromeUtils.defineModuleGetter(this, "TelemetryStopwatch",
                                "resource://gre/modules/TelemetryStopwatch.jsm");
 ChromeUtils.defineModuleGetter(this, "ViewPopup",
                                "resource:///modules/ExtensionPopups.jsm");
 
 var {
   DefaultWeakMap,
+  ExtensionError,
 } = ExtensionUtils;
 
 ChromeUtils.import("resource://gre/modules/ExtensionParent.jsm");
 
 var {
   IconDetails,
   StartupCache,
 } = ExtensionParent;
 
-var {
-  ExtensionError,
-} = ExtensionUtils;
-
 Cu.importGlobalProperties(["InspectorUtils"]);
 
 const POPUP_PRELOAD_TIMEOUT_MS = 200;
 const POPUP_OPEN_MS_HISTOGRAM = "WEBEXT_BROWSERACTION_POPUP_OPEN_MS";
 const POPUP_RESULT_HISTOGRAM = "WEBEXT_BROWSERACTION_POPUP_PRELOAD_RESULT_COUNT";
 
 var XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
@@ -107,18 +104,23 @@ this.browserAction = class extends Exten
       }, extension));
 
     this.iconData.set(
       this.defaults.icon,
       await StartupCache.get(
         extension, ["browserAction", "default_icon_data"],
         () => this.getIconData(this.defaults.icon)));
 
-    this.tabContext = new TabContext(tab => Object.create(this.globals),
-                                     extension);
+    this.tabContext = new TabContext(target => {
+      let window = target.ownerGlobal;
+      if (target === window) {
+        return Object.create(this.globals);
+      }
+      return Object.create(this.tabContext.get(window));
+    }, extension);
 
     // eslint-disable-next-line mozilla/balanced-listeners
     this.tabContext.on("location-change", this.handleLocationChange.bind(this));
 
     this.build();
   }
 
   handleLocationChange(eventType, tab, fromBrowse) {
@@ -513,80 +515,128 @@ this.browserAction = class extends Exten
       ${getStyle("menupanel-image-2x", 64)}
       ${getStyle("toolbar-image", baseSize)}
       ${getStyle("toolbar-image-2x", baseSize * 2)}
     `;
 
     return {style, legacy};
   }
 
-  // Update the toolbar button for a given window.
+  /**
+   * Update the toolbar button for a given window.
+   *
+   * @param {ChromeWindow} window
+   *        Browser chrome window.
+   */
   updateWindow(window) {
     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.ownerGlobal);
+  /**
+   * Update the toolbar button when the extension changes the icon, title, url, etc.
+   * If it only changes a parameter for a single tab, `target` will be that tab.
+   * If it only changes a parameter for a single window, `target` will be that window.
+   * Otherwise `target` will be null.
+   *
+   * @param {XULElement|ChromeWindow|null} target
+   *        Browser tab or browser chrome window, may be null.
+   */
+  updateOnChange(target) {
+    if (target) {
+      let window = target.ownerGlobal;
+      if (target === window || target.selected) {
+        this.updateWindow(window);
       }
     } else {
       for (let window of windowTracker.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) {
-    let values;
-    if (tab == null) {
+  /**
+   * Gets the target object and its associated values corresponding to
+   * the `details` parameter of the various get* and set* API methods.
+   *
+   * @param {Object} details
+   *        An object with optional `tabId` or `windowId` properties.
+   * @throws if both `tabId` and `windowId` are specified, or if they are invalid.
+   * @returns {Object}
+   *        An object with two properties: `target` and `values`.
+   *        - If a `tabId` was specified, `target` will be the corresponding
+   *          XULElement tab. If a `windowId` was specified, `target` will be
+   *          the corresponding ChromeWindow. Otherwise it will be `null`.
+   *        - `values` will contain the icon, title, badge, etc. associated with
+   *          the target.
+   */
+  getContextData({tabId, windowId}) {
+    if (tabId != null && windowId != null) {
+      throw new ExtensionError("Only one of tabId and windowId can be specified.");
+    }
+    let target, values;
+    if (tabId != null) {
+      target = tabTracker.getTab(tabId);
+      values = this.tabContext.get(target);
+    } else if (windowId != null) {
+      target = windowTracker.getWindow(windowId);
+      values = this.tabContext.get(target);
+    } else {
+      target = null;
       values = this.globals;
-    } else {
-      values = this.tabContext.get(tab);
     }
-    if (value == null) {
+    return {target, values};
+  }
+
+  /**
+   * Set a global, window specific or tab specific property.
+   *
+   * @param {Object} details
+   *        An object with optional `tabId` or `windowId` properties.
+   * @param {string} prop
+   *        String property to set. Should should be one of "icon", "title",
+   *        "badgeText", "popup", "badgeBackgroundColor" or "enabled".
+   * @param {string} value
+   *        Value for prop.
+   */
+  setProperty(details, prop, value) {
+    let {target, values} = this.getContextData(details);
+    if (value === null) {
       delete values[prop];
     } else {
       values[prop] = value;
     }
 
-    this.updateOnChange(tab);
+    this.updateOnChange(target);
   }
 
-  // tab is allowed to be null.
-  // prop should be one of "title", "badgeText", "popup", or "badgeBackgroundColor".
-  getProperty(tab, prop) {
-    if (tab == null) {
-      return this.globals[prop];
-    }
-    return this.tabContext.get(tab)[prop];
+  /**
+   * Retrieve the value of a global, window specific or tab specific property.
+   *
+   * @param {Object} details
+   *        An object with optional `tabId` or `windowId` properties.
+   * @param {string} prop
+   *        String property to retrieve. Should should be one of "icon", "title",
+   *        "badgeText", "popup", "badgeBackgroundColor" or "enabled".
+   * @returns {string} value
+   *          Value of prop.
+   */
+  getProperty(details, prop) {
+    return this.getContextData(details).values[prop];
   }
 
   getAPI(context) {
     let {extension} = context;
     let {tabManager} = extension;
 
     let browserAction = this;
 
-    function getTab(tabId) {
-      if (tabId !== null) {
-        return tabTracker.getTab(tabId);
-      }
-      return null;
-    }
-
     return {
       browserAction: {
         onClicked: new EventManager({
           context,
           name: "browserAction.onClicked",
           inputHandling: true,
           register: fire => {
             let listener = (event, browser) => {
@@ -596,108 +646,85 @@ this.browserAction = class extends Exten
             browserAction.on("click", listener);
             return () => {
               browserAction.off("click", listener);
             };
           },
         }).api(),
 
         enable: function(tabId) {
-          let tab = getTab(tabId);
-          browserAction.setProperty(tab, "enabled", true);
+          browserAction.setProperty({tabId}, "enabled", true);
         },
 
         disable: function(tabId) {
-          let tab = getTab(tabId);
-          browserAction.setProperty(tab, "enabled", false);
+          browserAction.setProperty({tabId}, "enabled", false);
         },
 
         isEnabled: function(details) {
-          let tab = getTab(details.tabId);
-          return browserAction.getProperty(tab, "enabled");
+          return browserAction.getProperty(details, "enabled");
         },
 
         setTitle: function(details) {
-          let tab = getTab(details.tabId);
-
-          browserAction.setProperty(tab, "title", details.title);
+          browserAction.setProperty(details, "title", details.title);
         },
 
         getTitle: function(details) {
-          let tab = getTab(details.tabId);
-
-          let title = browserAction.getProperty(tab, "title");
-          return Promise.resolve(title);
+          return browserAction.getProperty(details, "title");
         },
 
         setIcon: function(details) {
-          let tab = getTab(details.tabId);
-
           details.iconType = "browserAction";
 
           let icon = IconDetails.normalize(details, extension, context);
           if (!Object.keys(icon).length) {
             icon = null;
           }
-          browserAction.setProperty(tab, "icon", icon);
+          browserAction.setProperty(details, "icon", icon);
         },
 
         setBadgeText: function(details) {
-          let tab = getTab(details.tabId);
-
-          browserAction.setProperty(tab, "badgeText", details.text);
+          browserAction.setProperty(details, "badgeText", details.text);
         },
 
         getBadgeText: function(details) {
-          let tab = getTab(details.tabId);
-
-          let text = browserAction.getProperty(tab, "badgeText");
-          return Promise.resolve(text);
+          return browserAction.getProperty(details, "badgeText");
         },
 
         setPopup: function(details) {
-          let tab = getTab(details.tabId);
-
           // 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);
           if (url && !context.checkLoadURL(url)) {
             return Promise.reject({message: `Access denied for URL ${url}`});
           }
-          browserAction.setProperty(tab, "popup", url);
+          browserAction.setProperty(details, "popup", url);
         },
 
         getPopup: function(details) {
-          let tab = getTab(details.tabId);
-
-          let popup = browserAction.getProperty(tab, "popup");
-          return Promise.resolve(popup);
+          return browserAction.getProperty(details, "popup");
         },
 
         setBadgeBackgroundColor: function(details) {
-          let tab = getTab(details.tabId);
           let color = details.color;
           if (typeof color == "string") {
             let col = InspectorUtils.colorToRGBA(color);
             if (!col) {
               throw new ExtensionError(`Invalid badge background color: "${color}"`);
             }
             color = col && [col.r, col.g, col.b, Math.round(col.a * 255)];
           }
-          browserAction.setProperty(tab, "badgeBackgroundColor", color);
+          browserAction.setProperty(details, "badgeBackgroundColor", color);
         },
 
         getBadgeBackgroundColor: function(details, callback) {
-          let tab = getTab(details.tabId);
-
-          let color = browserAction.getProperty(tab, "badgeBackgroundColor");
-          return Promise.resolve(color || [0xd9, 0, 0, 255]);
+          let color = browserAction.getProperty(details, "badgeBackgroundColor");
+          return color || [0xd9, 0, 0, 255];
         },
 
         openPopup: function() {
           let window = windowTracker.topWindow;
           browserAction.triggerAction(window);
         },
       },
     };
--- a/browser/components/extensions/schemas/browser_action.json
+++ b/browser/components/extensions/schemas/browser_action.json
@@ -53,16 +53,35 @@
     ]
   },
   {
     "namespace": "browserAction",
     "description": "Use browser actions to put icons in the main browser toolbar, to the right of the address bar. In addition to its icon, a browser action can also have a tooltip, a badge, and a popup.",
     "permissions": ["manifest:browser_action"],
     "types": [
       {
+        "id": "Details",
+        "type": "object",
+        "description": "Specifies to which tab or window the value should be set, or from which one it should be retrieved. If no tab nor window is specified, the global value is set or retrieved.",
+        "properties": {
+          "tabId": {
+            "type": "integer",
+            "optional": true,
+            "minimum": 0,
+            "description": "When setting a value, it will be specific to the specified tab, and will automatically reset when the tab navigates. When getting, specifies the tab to get the value from; if there is no tab-specific value, the window one will be inherited."
+          },
+          "windowId": {
+            "type": "integer",
+            "optional": true,
+            "minimum": -2,
+            "description": "When setting a value, it will be specific to the specified window. When getting, specifies the window to get the value from; if there is no window-specific value, the global one will be inherited."
+          }
+        }
+      },
+      {
         "id": "ColorArray",
         "type": "array",
         "items": {
           "type": "integer",
           "minimum": 0,
           "maximum": 255
         },
         "minItems": 4,
@@ -82,28 +101,24 @@
         "name": "setTitle",
         "type": "function",
         "description": "Sets the title of the browser action. This shows up in the tooltip.",
         "async": "callback",
         "parameters": [
           {
             "name": "details",
             "type": "object",
+            "$import": "Details",
             "properties": {
               "title": {
                 "choices": [
                   {"type": "string"},
                   {"type": "null"}
                 ],
                 "description": "The string the browser action should display when moused over."
-              },
-              "tabId": {
-                "type": "integer",
-                "optional": true,
-                "description": "Limits the change to when a particular tab is selected. Automatically resets when the tab is closed."
               }
             }
           },
           {
             "type": "function",
             "name": "callback",
             "optional": true,
             "parameters": []
@@ -113,24 +128,17 @@
       {
         "name": "getTitle",
         "type": "function",
         "description": "Gets the title of the browser action.",
         "async": "callback",
         "parameters": [
           {
             "name": "details",
-            "type": "object",
-            "properties": {
-              "tabId": {
-                "type": "integer",
-                "optional": true,
-                "description": "Specify the tab to get the title from. If no tab is specified, the non-tab-specific title is returned."
-              }
-            }
+            "$ref": "Details"
           },
           {
             "type": "function",
             "name": "callback",
             "parameters": [
               {
                 "name": "result",
                 "type": "string"
@@ -143,16 +151,17 @@
         "name": "setIcon",
         "type": "function",
         "description": "Sets the icon for the browser action. The icon can be specified either as the path to an image file or as the pixel data from a canvas element, or as dictionary of either one of those. Either the <b>path</b> or the <b>imageData</b> property must be specified.",
         "async": "callback",
         "parameters": [
           {
             "name": "details",
             "type": "object",
+            "$import": "Details",
             "properties": {
               "imageData": {
                 "choices": [
                   { "$ref": "ImageDataType" },
                   {
                     "type": "object",
                     "patternProperties": {
                       "^[1-9]\\d*$": {"$ref": "ImageDataType"}
@@ -169,21 +178,16 @@
                     "type": "object",
                     "patternProperties": {
                       "^[1-9]\\d*$": { "type": "string" }
                     }
                   }
                 ],
                 "optional": true,
                 "description": "Either a relative image path or a dictionary {size -> relative image path} pointing to icon to be set. If the icon is specified as a dictionary, the actual image to be used is chosen depending on screen's pixel density. If the number of image pixels that fit into one screen space unit equals <code>scale</code>, then image with size <code>scale</code> * 19 will be selected. Initially only scales 1 and 2 will be supported. At least one image must be specified. Note that 'details.path = foo' is equivalent to 'details.imageData = {'19': foo}'"
-              },
-              "tabId": {
-                "type": "integer",
-                "optional": true,
-                "description": "Limits the change to when a particular tab is selected. Automatically resets when the tab is closed."
               }
             }
           },
           {
             "type": "function",
             "name": "callback",
             "optional": true,
             "parameters": []
@@ -194,23 +198,18 @@
         "name": "setPopup",
         "type": "function",
         "description": "Sets the html document to be opened as a popup when the user clicks on the browser action's icon.",
         "async": "callback",
         "parameters": [
           {
             "name": "details",
             "type": "object",
+            "$import": "Details",
             "properties": {
-              "tabId": {
-                "type": "integer",
-                "optional": true,
-                "minimum": 0,
-                "description": "Limits the change to when a particular tab is selected. Automatically resets when the tab is closed."
-              },
               "popup": {
                 "choices": [
                   {"type": "string"},
                   {"type": "null"}
                 ],
                 "description": "The html file to show in a popup.  If set to the empty string (''), no popup is shown."
               }
             }
@@ -226,24 +225,17 @@
       {
         "name": "getPopup",
         "type": "function",
         "description": "Gets the html document set as the popup for this browser action.",
         "async": "callback",
         "parameters": [
           {
             "name": "details",
-            "type": "object",
-            "properties": {
-              "tabId": {
-                "type": "integer",
-                "optional": true,
-                "description": "Specify the tab to get the popup from. If no tab is specified, the non-tab-specific popup is returned."
-              }
-            }
+            "$ref": "Details"
           },
           {
             "type": "function",
             "name": "callback",
             "parameters": [
               {
                 "name": "result",
                 "type": "string"
@@ -256,55 +248,44 @@
         "name": "setBadgeText",
         "type": "function",
         "description": "Sets the badge text for the browser action. The badge is displayed on top of the icon.",
         "async": "callback",
         "parameters": [
           {
             "name": "details",
             "type": "object",
+            "$import": "Details",
             "properties": {
               "text": {
                 "choices": [
                   {"type": "string"},
                   {"type": "null"}
                 ],
                 "description": "Any number of characters can be passed, but only about four can fit in the space."
-              },
-              "tabId": {
-                "type": "integer",
-                "optional": true,
-                "description": "Limits the change to when a particular tab is selected. Automatically resets when the tab is closed."
               }
             }
           },
           {
             "type": "function",
             "name": "callback",
             "optional": true,
             "parameters": []
           }
         ]
       },
       {
         "name": "getBadgeText",
         "type": "function",
-        "description": "Gets the badge text of the browser action. If no tab is specified, the non-tab-specific badge text is returned.",
+        "description": "Gets the badge text of the browser action. If no tab nor window is specified is specified, the global badge text is returned.",
         "async": "callback",
         "parameters": [
           {
             "name": "details",
-            "type": "object",
-            "properties": {
-              "tabId": {
-                "type": "integer",
-                "optional": true,
-                "description": "Specify the tab to get the badge text from. If no tab is specified, the non-tab-specific badge text is returned."
-              }
-            }
+            "$ref": "Details"
           },
           {
             "type": "function",
             "name": "callback",
             "parameters": [
               {
                 "name": "result",
                 "type": "string"
@@ -317,29 +298,25 @@
         "name": "setBadgeBackgroundColor",
         "type": "function",
         "description": "Sets the background color for the badge.",
         "async": "callback",
         "parameters": [
           {
             "name": "details",
             "type": "object",
+            "$import": "Details",
             "properties": {
               "color": {
                 "description": "An array of four integers in the range [0,255] that make up the RGBA color of the badge. For example, opaque red is <code>[255, 0, 0, 255]</code>. Can also be a string with a CSS value, with opaque red being <code>#FF0000</code> or <code>#F00</code>.",
                 "choices": [
                   {"type": "string"},
                   {"$ref": "ColorArray"},
                   {"type": "null"}
                 ]
-              },
-              "tabId": {
-                "type": "integer",
-                "optional": true,
-                "description": "Limits the change to when a particular tab is selected. Automatically resets when the tab is closed."
               }
             }
           },
           {
             "type": "function",
             "name": "callback",
             "optional": true,
             "parameters": []
@@ -349,24 +326,17 @@
       {
         "name": "getBadgeBackgroundColor",
         "type": "function",
         "description": "Gets the background color of the browser action.",
         "async": "callback",
         "parameters": [
           {
             "name": "details",
-            "type": "object",
-            "properties": {
-              "tabId": {
-                "type": "integer",
-                "optional": true,
-                "description": "Specify the tab to get the badge background color from. If no tab is specified, the non-tab-specific badge background color is returned."
-              }
-            }
+            "$ref": "Details"
           },
           {
             "type": "function",
             "name": "callback",
             "parameters": [
               {
                 "name": "result",
                 "$ref": "ColorArray"
@@ -420,24 +390,17 @@
       {
         "name": "isEnabled",
         "type": "function",
         "description": "Checks whether the browser action is enabled.",
         "async": true,
         "parameters": [
           {
             "name": "details",
-            "type": "object",
-            "properties": {
-              "tabId": {
-                "type": "integer",
-                "optional": true,
-                "description": "Specify the tab to get the enabledness from. If no tab is specified, the non-tab-specific enabledness is returned."
-              }
-            }
+            "$ref": "Details"
           }
         ]
       },
       {
         "name": "openPopup",
         "type": "function",
         "requireUserInput": true,
         "description": "Opens the extension popup window in the active window.",
--- a/browser/components/extensions/test/browser/browser_ext_browserAction_context.js
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_context.js
@@ -1,43 +1,40 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 async function runTests(options) {
   async function background(getTests) {
-    async function checkDetails(expecting, tabId) {
-      let title = await browser.browserAction.getTitle({tabId});
+    async function checkDetails(expecting, details) {
+      let title = await browser.browserAction.getTitle(details);
       browser.test.assertEq(expecting.title, title,
                             "expected value from getTitle");
 
-      let popup = await browser.browserAction.getPopup({tabId});
+      let popup = await browser.browserAction.getPopup(details);
       browser.test.assertEq(expecting.popup, popup,
                             "expected value from getPopup");
 
-      let badge = await browser.browserAction.getBadgeText({tabId});
+      let badge = await browser.browserAction.getBadgeText(details);
       browser.test.assertEq(expecting.badge, badge,
                             "expected value from getBadge");
 
-      let badgeBackgroundColor = await browser.browserAction.getBadgeBackgroundColor({tabId});
+      let badgeBackgroundColor = await browser.browserAction.getBadgeBackgroundColor(details);
       browser.test.assertEq(String(expecting.badgeBackgroundColor),
                             String(badgeBackgroundColor),
                             "expected value from getBadgeBackgroundColor");
 
-      let enabled = await browser.browserAction.isEnabled({tabId});
-      browser.test.assertEq(!expecting.disabled, enabled,
+      let enabled = await browser.browserAction.isEnabled(details);
+      browser.test.assertEq(expecting.enabled, enabled,
                             "expected value from isEnabled");
     }
 
-    let expectDefaults = expecting => {
-      return checkDetails(expecting);
-    };
-
     let tabs = [];
-    let tests = getTests(tabs, expectDefaults);
+    let windows = [];
+    let tests = getTests(tabs, windows);
 
     {
       let tabId = 0xdeadbeef;
       let calls = [
         () => browser.browserAction.enable(tabId),
         () => browser.browserAction.disable(tabId),
         () => browser.browserAction.setTitle({tabId, title: "foo"}),
         () => browser.browserAction.setIcon({tabId, path: "foo.png"}),
@@ -54,68 +51,74 @@ async function runTests(options) {
       }
     }
 
     // Runs the next test in the `tests` array, checks the results,
     // and passes control back to the outer test scope.
     function nextTest() {
       let test = tests.shift();
 
-      test(async expecting => {
+      test(async (expectTab, expectWindow, expectGlobal, expectDefault) => {
+        expectGlobal = {...expectDefault, ...expectGlobal};
+        expectWindow = {...expectGlobal, ...expectWindow};
+        expectTab = {...expectWindow, ...expectTab};
+
         // Check that the API returns the expected values, and then
         // run the next test.
-        let tabs = await browser.tabs.query({active: true, currentWindow: true});
-        await checkDetails(expecting, tabs[0].id);
+        let [{windowId, id: tabId}] = await browser.tabs.query({active: true, currentWindow: true});
+        await checkDetails(expectTab, {tabId});
+        await checkDetails(expectWindow, {windowId});
+        await checkDetails(expectGlobal, {});
 
         // Check that the actual icon has the expected values, then
         // run the next test.
-        browser.test.sendMessage("nextTest", expecting, tests.length);
+        browser.test.sendMessage("nextTest", expectTab, windowId, tests.length);
       });
     }
 
     browser.test.onMessage.addListener((msg) => {
       if (msg != "runNextTest") {
         browser.test.fail("Expecting 'runNextTest' message");
       }
 
       nextTest();
     });
 
-    browser.tabs.query({active: true, currentWindow: true}, resultTabs => {
-      tabs[0] = resultTabs[0].id;
-
-      nextTest();
-    });
+    let [{id, windowId}] = await browser.tabs.query({active: true, currentWindow: true});
+    tabs.push(id);
+    windows.push(windowId);
+    nextTest();
   }
 
   let extension = ExtensionTestUtils.loadExtension({
     manifest: options.manifest,
 
     files: options.files || {},
 
     background: `(${background})(${options.getTests})`,
   });
 
   let browserActionId;
-  function checkDetails(details) {
+  function checkDetails(details, windowId) {
+    let {document} = Services.wm.getOuterWindowWithId(windowId);
     if (!browserActionId) {
       browserActionId = `${makeWidgetId(extension.id)}-browser-action`;
     }
 
     let button = document.getElementById(browserActionId);
 
     ok(button, "button exists");
 
     let title = details.title || options.manifest.name;
 
     is(getListStyleImage(button), details.icon, "icon URL is correct");
     is(button.getAttribute("tooltiptext"), title, "image title is correct");
     is(button.getAttribute("label"), title, "image label is correct");
     is(button.getAttribute("badge"), details.badge, "badge text is correct");
-    is(button.getAttribute("disabled") == "true", Boolean(details.disabled), "disabled state is correct");
+    is(button.getAttribute("disabled") == "true", !details.enabled, "disabled state is correct");
 
     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]})`;
@@ -123,20 +126,20 @@ async function runTests(options) {
       is(badgeColor, expectedColor, "badge color is correct");
     }
 
 
     // TODO: Popup URL.
   }
 
   let awaitFinish = new Promise(resolve => {
-    extension.onMessage("nextTest", async (expecting, testsRemaining) => {
+    extension.onMessage("nextTest", async (expecting, windowId, testsRemaining) => {
       await promiseAnimationFrame();
 
-      checkDetails(expecting);
+      checkDetails(expecting, windowId);
 
       if (testsRemaining) {
         extension.sendMessage("runNextTest");
       } else {
         resolve();
       }
     });
   });
@@ -171,64 +174,47 @@ add_task(async function testTabSwitchCon
 
         "title": {
           "message": "Title",
           "description": "Title",
         },
       },
 
       "default.png": imageBuffer,
-      "default-2.png": imageBuffer,
+      "global.png": imageBuffer,
       "1.png": imageBuffer,
       "2.png": imageBuffer,
     },
 
-    getTests: function(tabs, expectDefaults) {
-      const DEFAULT_BADGE_COLOR = [0xd9, 0, 0, 255];
-
+    getTests: function(tabs, windows) {
       let details = [
         {"icon": browser.runtime.getURL("default.png"),
          "popup": browser.runtime.getURL("default.html"),
          "title": "Default Title",
          "badge": "",
-         "badgeBackgroundColor": DEFAULT_BADGE_COLOR},
-        {"icon": browser.runtime.getURL("1.png"),
-         "popup": browser.runtime.getURL("default.html"),
-         "title": "Default Title",
-         "badge": "",
-         "badgeBackgroundColor": DEFAULT_BADGE_COLOR},
+         "badgeBackgroundColor": [0xd9, 0, 0, 255],
+         "enabled": true},
+        {"icon": browser.runtime.getURL("1.png")},
         {"icon": browser.runtime.getURL("2.png"),
          "popup": browser.runtime.getURL("2.html"),
          "title": "Title 2",
          "badge": "2",
          "badgeBackgroundColor": [0xff, 0, 0, 0xff],
-         "disabled": true},
-        {"icon": browser.runtime.getURL("1.png"),
-         "popup": browser.runtime.getURL("default-2.html"),
-         "title": "Default Title 2",
-         "badge": "d2",
-         "badgeBackgroundColor": [0, 0xff, 0, 0xff],
-         "disabled": true},
-        {"icon": browser.runtime.getURL("1.png"),
-         "popup": browser.runtime.getURL("default-2.html"),
-         "title": "Default Title 2",
-         "badge": "d2",
+         "enabled": false},
+        {"icon": browser.runtime.getURL("global.png"),
+         "popup": browser.runtime.getURL("global.html"),
+         "title": "Global Title",
+         "badge": "g",
          "badgeBackgroundColor": [0, 0xff, 0, 0xff],
-         "disabled": false},
-        {"icon": browser.runtime.getURL("default-2.png"),
-         "popup": browser.runtime.getURL("default-2.html"),
-         "title": "Default Title 2",
-         "badge": "d2",
+         "enabled": false},
+        {"icon": browser.runtime.getURL("global.png"),
+         "popup": browser.runtime.getURL("global.html"),
+         "title": "Global Title",
+         "badge": "g",
          "badgeBackgroundColor": [0, 0xff, 0, 0xff]},
-        {"icon": browser.runtime.getURL("default-2.png"),
-         "popup": browser.runtime.getURL("default-2.html"),
-         "title": "Default Title 2",
-         "badge": "d2",
-         "badgeBackgroundColor": [0, 0xff, 0, 0xff],
-         "disabled": false},
       ];
 
       let promiseTabLoad = details => {
         return new Promise(resolve => {
           browser.tabs.onUpdated.addListener(function listener(tabId, changed) {
             if (tabId == details.id && changed.url == details.url) {
               browser.tabs.onUpdated.removeListener(listener);
               resolve();
@@ -236,112 +222,102 @@ add_task(async function testTabSwitchCon
           });
         });
       };
 
       return [
         async expect => {
           browser.test.log("Initial state, expect default properties.");
 
-          await expectDefaults(details[0]);
-          expect(details[0]);
+          expect(null, null, null, details[0]);
         },
         async expect => {
           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"});
 
-          await expectDefaults(details[0]);
-          expect(details[1]);
+          expect(details[1], null, null, details[0]);
         },
         async expect => {
           browser.test.log("Create a new tab. Expect default properties.");
           let tab = await browser.tabs.create({active: true, url: "about:blank?0"});
           tabs.push(tab.id);
 
           browser.test.log("Await tab load.");
           let promise = promiseTabLoad({id: tabs[1], url: "about:blank?0"});
           let {url} = await browser.tabs.get(tabs[1]);
           if (url === "about:blank") {
             await promise;
           }
 
-          await expectDefaults(details[0]);
-          expect(details[0]);
+          expect(null, null, null, details[0]);
         },
         async expect => {
           browser.test.log("Change properties. Expect new properties.");
           let tabId = tabs[1];
           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: "#ff0000"});
           browser.browserAction.disable(tabId);
 
-          await expectDefaults(details[0]);
-          expect(details[2]);
+          expect(details[2], null, null, details[0]);
         },
         async expect => {
           browser.test.log("Switch back to the first tab. Expect previously set properties.");
           await browser.tabs.update(tabs[0], {active: true});
-          expect(details[1]);
+          expect(details[1], null, null, details[0]);
         },
         async expect => {
-          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.test.log("Change global values, expect those changes reflected.");
+          browser.browserAction.setIcon({path: "global.png"});
+          browser.browserAction.setPopup({popup: "global.html"});
+          browser.browserAction.setTitle({title: "Global Title"});
+          browser.browserAction.setBadgeText({text: "g"});
           browser.browserAction.setBadgeBackgroundColor({color: [0, 0xff, 0, 0xff]});
           browser.browserAction.disable();
 
-          await expectDefaults(details[3]);
-          expect(details[3]);
+          expect(details[1], null, details[3], details[0]);
         },
         async expect => {
-          browser.test.log("Re-enable by default. Expect enabled.");
+          browser.test.log("Re-enable globally. Expect enabled.");
           browser.browserAction.enable();
 
-          await expectDefaults(details[4]);
-          expect(details[4]);
+          expect(details[1], null, details[4], details[0]);
         },
         async expect => {
-          browser.test.log("Switch back to tab 2. Expect former value, unaffected by changes to defaults in previous step.");
+          browser.test.log("Switch back to tab 2. Expect former tab values, and new global values from previous steps.");
           await browser.tabs.update(tabs[1], {active: true});
 
-          await expectDefaults(details[4]);
-          expect(details[2]);
+          expect(details[2], null, details[4], details[0]);
         },
-        expect => {
-          browser.test.log("Navigate to a new page. Expect defaults.");
+        async expect => {
+          browser.test.log("Navigate to a new page. Expect tab-specific values to be cleared.");
 
-          browser.tabs.onUpdated.addListener(function listener(tabId, changed) {
-            if (tabId == tabs[1] && changed.url) {
-              browser.tabs.onUpdated.removeListener(listener);
-              expect(details[6]);
-            }
-          });
+          let promise = promiseTabLoad({id: tabs[1], url: "about:blank?1"});
+          browser.tabs.update(tabs[1], {url: "about:blank?1"});
+          await promise;
 
-          browser.tabs.update(tabs[1], {url: "about:blank?1"});
+          expect(null, null, details[4], details[0]);
         },
         async expect => {
           browser.test.log("Delete tab, switch back to tab 1. Expect previous results again.");
           await browser.tabs.remove(tabs[1]);
-          expect(details[4]);
+          expect(details[1], null, details[4], details[0]);
         },
         async expect => {
-          browser.test.log("Create a new tab. Expect new default properties.");
+          browser.test.log("Create a new tab. Expect new global properties.");
           let tab = await browser.tabs.create({active: true, url: "about:blank?2"});
           tabs.push(tab.id);
-          expect(details[5]);
+          expect(null, null, details[4], details[0]);
         },
         async expect => {
           browser.test.log("Delete tab.");
           await browser.tabs.remove(tabs[2]);
-          expect(details[4]);
+          expect(details[1], null, details[4], details[0]);
         },
       ];
     },
   });
 });
 
 add_task(async function testDefaultTitle() {
   await runTests({
@@ -354,80 +330,65 @@ add_task(async function testDefaultTitle
 
       "permissions": ["tabs"],
     },
 
     files: {
       "icon.png": imageBuffer,
     },
 
-    getTests: function(tabs, expectGlobals) {
-      const DEFAULT_BADGE_COLOR = [0xd9, 0, 0, 255];
-
+    getTests: function(tabs, windows) {
       let details = [
         {"title": "Foo Extension",
          "popup": "",
          "badge": "",
-         "badgeBackgroundColor": DEFAULT_BADGE_COLOR,
-         "icon": browser.runtime.getURL("icon.png")},
-        {"title": "Foo Title",
-         "popup": "",
-         "badge": "",
-         "badgeBackgroundColor": DEFAULT_BADGE_COLOR,
-         "icon": browser.runtime.getURL("icon.png")},
-        {"title": "Bar Title",
-         "popup": "",
-         "badge": "",
-         "badgeBackgroundColor": DEFAULT_BADGE_COLOR,
-         "icon": browser.runtime.getURL("icon.png")},
+         "badgeBackgroundColor": [0xd9, 0, 0, 255],
+         "icon": browser.runtime.getURL("icon.png"),
+         "enabled": true},
+        {"title": "Foo Title"},
+        {"title": "Bar Title"},
       ];
 
       return [
         async expect => {
-          browser.test.log("Initial state. Expect default title as global title.");
+          browser.test.log("Initial state. Expect default properties.");
 
-          await expectGlobals(details[0]);
-          expect(details[0]);
+          expect(null, null, null, details[0]);
         },
         async expect => {
           browser.test.log("Change the tab title. Expect new title.");
           browser.browserAction.setTitle({tabId: tabs[0], title: "Foo Title"});
 
-          await expectGlobals(details[0]);
-          expect(details[1]);
+          expect(details[1], null, null, details[0]);
         },
         async expect => {
           browser.test.log("Change the global title. Expect same properties.");
           browser.browserAction.setTitle({title: "Bar Title"});
 
-          await expectGlobals(details[2]);
-          expect(details[1]);
+          expect(details[1], null, details[2], details[0]);
         },
         async expect => {
           browser.test.log("Clear the tab title. Expect new global title.");
           browser.browserAction.setTitle({tabId: tabs[0], title: null});
 
-          await expectGlobals(details[2]);
-          expect(details[2]);
+          expect(null, null, details[2], details[0]);
         },
         async expect => {
           browser.test.log("Clear the global title. Expect default title.");
           browser.browserAction.setTitle({title: null});
 
-          await expectGlobals(details[0]);
-          expect(details[0]);
+          expect(null, null, null, details[0]);
         },
         async expect => {
           browser.test.assertRejects(
             browser.browserAction.setPopup({popup: "about:addons"}),
             /Access denied for URL about:addons/,
             "unable to set popup to about:addons");
 
-          await expectGlobals(details[0]);
-          expect(details[0]);
+          expect(null, null, null, details[0]);
         },
       ];
     },
   });
 });
 
 add_task(async function testBadgeColorPersistence() {
   const extension = ExtensionTestUtils.loadExtension({
@@ -480,123 +441,273 @@ add_task(async function testPropertyRemo
         "default_icon": "default.png",
         "default_popup": "default.html",
         "default_title": "Default Title",
       },
     },
 
     "files": {
       "default.png": imageBuffer,
-      "i1.png": imageBuffer,
-      "i2.png": imageBuffer,
-      "i3.png": imageBuffer,
+      "global.png": imageBuffer,
+      "global2.png": imageBuffer,
+      "window.png": imageBuffer,
+      "tab.png": imageBuffer,
     },
 
-    getTests: function(tabs, expectGlobals) {
+    getTests: function(tabs, windows) {
       let defaultIcon = "chrome://browser/content/extension.svg";
       let details = [
         {"icon": browser.runtime.getURL("default.png"),
          "popup": browser.runtime.getURL("default.html"),
          "title": "Default Title",
          "badge": "",
-         "badgeBackgroundColor": [0xd9, 0x00, 0x00, 0xFF]},
-        {"icon": browser.runtime.getURL("i1.png"),
-         "popup": browser.runtime.getURL("p1.html"),
-         "title": "t1",
-         "badge": "b1",
+         "badgeBackgroundColor": [0xd9, 0x00, 0x00, 0xFF],
+         "enabled": true},
+        {"icon": browser.runtime.getURL("global.png"),
+         "popup": browser.runtime.getURL("global.html"),
+         "title": "global",
+         "badge": "global",
          "badgeBackgroundColor": [0x11, 0x11, 0x11, 0xFF]},
-        {"icon": browser.runtime.getURL("i2.png"),
-         "popup": browser.runtime.getURL("p2.html"),
-         "title": "t2",
-         "badge": "b2",
+        {"icon": browser.runtime.getURL("window.png"),
+         "popup": browser.runtime.getURL("window.html"),
+         "title": "window",
+         "badge": "window",
          "badgeBackgroundColor": [0x22, 0x22, 0x22, 0xFF]},
+        {"icon": browser.runtime.getURL("tab.png"),
+         "popup": browser.runtime.getURL("tab.html"),
+         "title": "tab",
+         "badge": "tab",
+         "badgeBackgroundColor": [0x33, 0x33, 0x33, 0xFF]},
         {"icon": defaultIcon,
          "popup": "",
          "title": "",
          "badge": "",
-         "badgeBackgroundColor": [0x22, 0x22, 0x22, 0xFF]},
-        {"icon": browser.runtime.getURL("i3.png"),
-         "popup": browser.runtime.getURL("p3.html"),
-         "title": "t3",
-         "badge": "b3",
          "badgeBackgroundColor": [0x33, 0x33, 0x33, 0xFF]},
+        {"icon": browser.runtime.getURL("global2.png"),
+         "popup": browser.runtime.getURL("global2.html"),
+         "title": "global2",
+         "badge": "global2",
+         "badgeBackgroundColor": [0x44, 0x44, 0x44, 0xFF]},
       ];
 
       return [
         async expect => {
           browser.test.log("Initial state, expect default properties.");
-          await expectGlobals(details[0]);
-          expect(details[0]);
+          expect(null, null, null, details[0]);
         },
         async expect => {
           browser.test.log("Set global values, expect the new values.");
-          browser.browserAction.setIcon({path: "i1.png"});
-          browser.browserAction.setPopup({popup: "p1.html"});
-          browser.browserAction.setTitle({title: "t1"});
-          browser.browserAction.setBadgeText({text: "b1"});
+          browser.browserAction.setIcon({path: "global.png"});
+          browser.browserAction.setPopup({popup: "global.html"});
+          browser.browserAction.setTitle({title: "global"});
+          browser.browserAction.setBadgeText({text: "global"});
           browser.browserAction.setBadgeBackgroundColor({color: "#111"});
-          await expectGlobals(details[1]);
-          expect(details[1]);
+          expect(null, null, details[1], details[0]);
+        },
+        async expect => {
+          browser.test.log("Set window values, expect the new values.");
+          let windowId = windows[0];
+          browser.browserAction.setIcon({windowId, path: "window.png"});
+          browser.browserAction.setPopup({windowId, popup: "window.html"});
+          browser.browserAction.setTitle({windowId, title: "window"});
+          browser.browserAction.setBadgeText({windowId, text: "window"});
+          browser.browserAction.setBadgeBackgroundColor({windowId, color: "#222"});
+          expect(null, details[2], details[1], details[0]);
         },
         async expect => {
           browser.test.log("Set tab values, expect the new values.");
           let tabId = tabs[0];
-          browser.browserAction.setIcon({tabId, path: "i2.png"});
-          browser.browserAction.setPopup({tabId, popup: "p2.html"});
-          browser.browserAction.setTitle({tabId, title: "t2"});
-          browser.browserAction.setBadgeText({tabId, text: "b2"});
-          browser.browserAction.setBadgeBackgroundColor({tabId, color: "#222"});
-          await expectGlobals(details[1]);
-          expect(details[2]);
+          browser.browserAction.setIcon({tabId, path: "tab.png"});
+          browser.browserAction.setPopup({tabId, popup: "tab.html"});
+          browser.browserAction.setTitle({tabId, title: "tab"});
+          browser.browserAction.setBadgeText({tabId, text: "tab"});
+          browser.browserAction.setBadgeBackgroundColor({tabId, color: "#333"});
+          expect(details[3], details[2], details[1], details[0]);
         },
         async expect => {
           browser.test.log("Set empty tab values, expect empty values except for bgcolor.");
           let tabId = tabs[0];
           browser.browserAction.setIcon({tabId, path: ""});
           browser.browserAction.setPopup({tabId, popup: ""});
           browser.browserAction.setTitle({tabId, title: ""});
           browser.browserAction.setBadgeText({tabId, text: ""});
           await browser.test.assertRejects(
             browser.browserAction.setBadgeBackgroundColor({tabId, color: ""}),
             /^Invalid badge background color: ""$/,
             "Expected invalid badge background color error"
           );
-          await expectGlobals(details[1]);
-          expect(details[3]);
+          expect(details[4], details[2], details[1], details[0]);
         },
         async expect => {
-          browser.test.log("Remove tab values, expect global values.");
+          browser.test.log("Remove tab values, expect window values.");
           let tabId = tabs[0];
           browser.browserAction.setIcon({tabId, path: null});
           browser.browserAction.setPopup({tabId, popup: null});
           browser.browserAction.setTitle({tabId, title: null});
           browser.browserAction.setBadgeText({tabId, text: null});
           browser.browserAction.setBadgeBackgroundColor({tabId, color: null});
-          await expectGlobals(details[1]);
-          expect(details[1]);
+          expect(null, details[2], details[1], details[0]);
+        },
+        async expect => {
+          browser.test.log("Remove window values, expect global values.");
+          let windowId = windows[0];
+          browser.browserAction.setIcon({windowId, path: null});
+          browser.browserAction.setPopup({windowId, popup: null});
+          browser.browserAction.setTitle({windowId, title: null});
+          browser.browserAction.setBadgeText({windowId, text: null});
+          browser.browserAction.setBadgeBackgroundColor({windowId, color: null});
+          expect(null, null, details[1], details[0]);
         },
         async expect => {
           browser.test.log("Change global values, expect the new values.");
-          browser.browserAction.setIcon({path: "i3.png"});
-          browser.browserAction.setPopup({popup: "p3.html"});
-          browser.browserAction.setTitle({title: "t3"});
-          browser.browserAction.setBadgeText({text: "b3"});
-          browser.browserAction.setBadgeBackgroundColor({color: "#333"});
-          await expectGlobals(details[4]);
-          expect(details[4]);
+          browser.browserAction.setIcon({path: "global2.png"});
+          browser.browserAction.setPopup({popup: "global2.html"});
+          browser.browserAction.setTitle({title: "global2"});
+          browser.browserAction.setBadgeText({text: "global2"});
+          browser.browserAction.setBadgeBackgroundColor({color: "#444"});
+          expect(null, null, details[5], details[0]);
         },
         async expect => {
           browser.test.log("Remove global values, expect defaults.");
           browser.browserAction.setIcon({path: null});
           browser.browserAction.setPopup({popup: null});
           browser.browserAction.setBadgeText({text: null});
           browser.browserAction.setTitle({title: null});
           browser.browserAction.setBadgeBackgroundColor({color: null});
-          await expectGlobals(details[0]);
-          expect(details[0]);
+          expect(null, null, null, details[0]);
+        },
+      ];
+    },
+  });
+});
+
+add_task(async function testMultipleWindows() {
+  await runTests({
+    manifest: {
+      "browser_action": {
+        "default_icon": "default.png",
+        "default_popup": "default.html",
+        "default_title": "Default Title",
+      },
+    },
+
+    "files": {
+      "default.png": imageBuffer,
+      "window1.png": imageBuffer,
+      "window2.png": imageBuffer,
+    },
+
+    getTests: function(tabs, windows) {
+      let details = [
+        {"icon": browser.runtime.getURL("default.png"),
+         "popup": browser.runtime.getURL("default.html"),
+         "title": "Default Title",
+         "badge": "",
+         "badgeBackgroundColor": [0xd9, 0x00, 0x00, 0xFF],
+         "enabled": true},
+        {"icon": browser.runtime.getURL("window1.png"),
+         "popup": browser.runtime.getURL("window1.html"),
+         "title": "window1",
+         "badge": "w1",
+         "badgeBackgroundColor": [0x11, 0x11, 0x11, 0xFF]},
+        {"icon": browser.runtime.getURL("window2.png"),
+         "popup": browser.runtime.getURL("window2.html"),
+         "title": "window2",
+         "badge": "w2",
+         "badgeBackgroundColor": [0x22, 0x22, 0x22, 0xFF]},
+        {"title": "tab"},
+      ];
+
+      return [
+        async expect => {
+          browser.test.log("Initial state, expect default properties.");
+          expect(null, null, null, details[0]);
+        },
+        async expect => {
+          browser.test.log("Set window values, expect the new values.");
+          let windowId = windows[0];
+          browser.browserAction.setIcon({windowId, path: "window1.png"});
+          browser.browserAction.setPopup({windowId, popup: "window1.html"});
+          browser.browserAction.setTitle({windowId, title: "window1"});
+          browser.browserAction.setBadgeText({windowId, text: "w1"});
+          browser.browserAction.setBadgeBackgroundColor({windowId, color: "#111"});
+          expect(null, details[1], null, details[0]);
+        },
+        async expect => {
+          browser.test.log("Create a new tab, expect window values.");
+          let tab = await browser.tabs.create({active: true});
+          tabs.push(tab.id);
+          expect(null, details[1], null, details[0]);
+        },
+        async expect => {
+          browser.test.log("Set a tab title, expect it.");
+          await browser.browserAction.setTitle({tabId: tabs[1], title: "tab"});
+          expect(details[3], details[1], null, details[0]);
+        },
+        async expect => {
+          browser.test.log("Open a new window, expect default values.");
+          let {id} = await browser.windows.create();
+          windows.push(id);
+          expect(null, null, null, details[0]);
+        },
+        async expect => {
+          browser.test.log("Set window values, expect the new values.");
+          let windowId = windows[1];
+          browser.browserAction.setIcon({windowId, path: "window2.png"});
+          browser.browserAction.setPopup({windowId, popup: "window2.html"});
+          browser.browserAction.setTitle({windowId, title: "window2"});
+          browser.browserAction.setBadgeText({windowId, text: "w2"});
+          browser.browserAction.setBadgeBackgroundColor({windowId, color: "#222"});
+          expect(null, details[2], null, details[0]);
+        },
+        async expect => {
+          browser.test.log("Move tab from old window to the new one. Tab-specific data"
+            + " is cleared (bug 1451176) and inheritance is from the new window");
+          await browser.tabs.move(tabs[1], {windowId: windows[1], index: -1});
+          await browser.tabs.update(tabs[1], {active: true});
+          expect(null, details[2], null, details[0]);
+        },
+        async expect => {
+          browser.test.log("Close the tab, expect window values.");
+          await browser.tabs.remove(tabs[1]);
+          expect(null, details[2], null, details[0]);
+        },
+        async expect => {
+          browser.test.log("Close the new window and go back to the previous one.");
+          await browser.windows.remove(windows[1]);
+          expect(null, details[1], null, details[0]);
+        },
+        async expect => {
+          browser.test.log("Assert failures for bad parameters. Expect no change");
+
+          let calls = {
+            setIcon: {path: "default.png"},
+            setPopup: {popup: "default.html"},
+            setTitle: {title: "Default Title"},
+            setBadgeText: {text: ""},
+            setBadgeBackgroundColor: {color: [0xd9, 0x00, 0x00, 0xFF]},
+            getPopup: {},
+            getTitle: {},
+            getBadgeText: {},
+            getBadgeBackgroundColor: {},
+          };
+          for (let [method, arg] of Object.entries(calls)) {
+            browser.test.assertThrows(
+              () => browser.browserAction[method]({...arg, windowId: -3}),
+              /-3 is too small \(must be at least -2\)/,
+              method + " with invalid windowId",
+            );
+            await browser.test.assertRejects(
+              browser.browserAction[method]({...arg, tabId: tabs[0], windowId: windows[0]}),
+              /Only one of tabId and windowId can be specified/,
+              method + " with both tabId and windowId",
+            );
+          }
+
+          expect(null, details[1], null, details[0]);
         },
       ];
     },
   });
 });
 
 add_task(async function testNavigationClearsData() {
   let url = "http://example.com/";