Bug 1390464 - Add windowId parameter in sidebarAction methods r=mixedpuppy
authorOriol Brufau <oriol-bugzilla@hotmail.com>
Sun, 28 Jan 2018 20:46:24 +0100
changeset 468789 958fff8c5f462ae7dc0c1ce7c0dd22faa48b12cf
parent 468788 f43cf565796e9aa7d2a9c463d8afd69c97941086
child 468790 7bbee469249d1dd85d8870b8499ca61beac6e0f2
push id9165
push userasasaki@mozilla.com
push dateThu, 26 Apr 2018 21:04:54 +0000
treeherdermozilla-beta@064c3804de2e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmixedpuppy
bugs1390464
milestone61.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 1390464 - Add windowId parameter in sidebarAction methods r=mixedpuppy MozReview-Commit-ID: eSJnVzpNvO
browser/components/extensions/parent/ext-sidebarAction.js
browser/components/extensions/schemas/sidebar_action.json
browser/components/extensions/test/browser/browser_ext_sidebarAction_context.js
--- a/browser/components/extensions/parent/ext-sidebarAction.js
+++ b/browser/components/extensions/parent/ext-sidebarAction.js
@@ -1,15 +1,19 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 ChromeUtils.import("resource://gre/modules/ExtensionParent.jsm");
 
 var {
+  ExtensionError,
+} = ExtensionUtils;
+
+var {
   IconDetails,
 } = ExtensionParent;
 
 var XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
 // WeakMap[Extension -> SidebarAction]
 let sidebarActionMap = new WeakMap();
 
@@ -45,18 +49,23 @@ this.sidebarAction = class extends Exten
     this.defaults = {
       enabled: true,
       title: options.default_title || extension.name,
       icon: IconDetails.normalize({path: options.default_icon}, extension),
       panel: options.default_panel || "",
     };
     this.globals = Object.create(this.defaults);
 
-    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);
 
     // We need to ensure our elements are available before session restore.
     this.windowOpenListener = (window) => {
       this.createMenuItem(window, this.globals);
     };
     windowTracker.addOpenListener(this.windowOpenListener);
 
     this.updateHeader = (event) => {
@@ -243,75 +252,102 @@ this.sidebarAction = class extends Exten
    */
   updateWindow(window) {
     let nativeTab = window.gBrowser.selectedTab;
     this.updateButton(window, this.tabContext.get(nativeTab));
   }
 
   /**
    * Update the broadcaster and menuitem when the extension changes the icon,
-   * title, url, etc. If it only changes a parameter for a single
-   * tab, `tab` will be that tab. Otherwise it will be null.
+   * 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|null} nativeTab
-   *        Browser tab, may be null.
+   * @param {XULElement|ChromeWindow|null} target
+   *        Browser tab or browser chrome window, may be null.
    */
-  updateOnChange(nativeTab) {
-    if (nativeTab) {
-      if (nativeTab.selected) {
-        this.updateWindow(nativeTab.ownerGlobal);
+  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);
       }
     }
   }
 
   /**
-   * Set a default or tab specific property.
+   * Gets the target object and its associated values corresponding to
+   * the `details` parameter of the various get* and set* API methods.
    *
-   * @param {XULElement|null} nativeTab
-   *        Webextension tab object, may be null.
+   * @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 and panel 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;
+    }
+    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 retrieve ["icon", "title", or "panel"].
+   *        String property to set ["icon", "title", or "panel"].
    * @param {string} value
    *        Value for property.
    */
-  setProperty(nativeTab, prop, value) {
-    let values;
-    if (nativeTab === null) {
-      values = this.globals;
-    } else {
-      values = this.tabContext.get(nativeTab);
-    }
+  setProperty(details, prop, value) {
+    let {target, values} = this.getContextData(details);
     if (value === null) {
       delete values[prop];
     } else {
       values[prop] = value;
     }
 
-    this.updateOnChange(nativeTab);
+    this.updateOnChange(target);
   }
 
   /**
-   * Retrieve a property from the tab or globals if tab is null.
+   * Retrieve the value of a global, window specific or tab specific property.
    *
-   * @param {XULElement|null} nativeTab
-   *        Browser tab object, may be null.
+   * @param {Object} details
+   *        An object with optional `tabId` or `windowId` properties.
    * @param {string} prop
    *        String property to retrieve ["icon", "title", or "panel"]
    * @returns {string} value
-   *          Value for prop.
+   *          Value of prop.
    */
-  getProperty(nativeTab, prop) {
-    if (nativeTab === null) {
-      return this.globals[prop];
-    }
-    return this.tabContext.get(nativeTab)[prop];
+  getProperty(details, prop) {
+    return this.getContextData(details).values[prop];
   }
 
   /**
    * Triggers this sidebar action for the given window, with the same effects as
    * if it were toggled via menu or toolbarbutton by a user.
    *
    * @param {ChromeWindow} window
    */
@@ -355,69 +391,51 @@ this.sidebarAction = class extends Exten
     let {SidebarUI} = window;
     return SidebarUI.isOpen && this.id == SidebarUI.currentID;
   }
 
   getAPI(context) {
     let {extension} = context;
     const sidebarAction = this;
 
-    function getTab(tabId) {
-      if (tabId !== null) {
-        return tabTracker.getTab(tabId);
-      }
-      return null;
-    }
-
     return {
       sidebarAction: {
         async setTitle(details) {
-          let nativeTab = getTab(details.tabId);
-          sidebarAction.setProperty(nativeTab, "title", details.title);
+          sidebarAction.setProperty(details, "title", details.title);
         },
 
         getTitle(details) {
-          let nativeTab = getTab(details.tabId);
-
-          let title = sidebarAction.getProperty(nativeTab, "title");
-          return Promise.resolve(title);
+          return sidebarAction.getProperty(details, "title");
         },
 
         async setIcon(details) {
-          let nativeTab = getTab(details.tabId);
-
           let icon = IconDetails.normalize(details, extension, context);
           if (!Object.keys(icon).length) {
             icon = null;
           }
-          sidebarAction.setProperty(nativeTab, "icon", icon);
+          sidebarAction.setProperty(details, "icon", icon);
         },
 
         async setPanel(details) {
-          let nativeTab = getTab(details.tabId);
-
           let url;
           // Clear the url when given null or empty string.
           if (!details.panel) {
             url = null;
           } else {
             url = context.uri.resolve(details.panel);
             if (!context.checkLoadURL(url)) {
               return Promise.reject({message: `Access denied for URL ${url}`});
             }
           }
 
-          sidebarAction.setProperty(nativeTab, "panel", url);
+          sidebarAction.setProperty(details, "panel", url);
         },
 
         getPanel(details) {
-          let nativeTab = getTab(details.tabId);
-
-          let panel = sidebarAction.getProperty(nativeTab, "panel");
-          return Promise.resolve(panel);
+          return sidebarAction.getProperty(details, "panel");
         },
 
         open() {
           let window = windowTracker.topWindow;
           sidebarAction.open(window);
         },
 
         close() {
--- a/browser/components/extensions/schemas/sidebar_action.json
+++ b/browser/components/extensions/schemas/sidebar_action.json
@@ -69,16 +69,22 @@
                   {"type": "null"}
                 ],
                 "description": "The string the sidebar action should display when moused over."
               },
               "tabId": {
                 "type": "integer",
                 "optional": true,
                 "description": "Sets the sidebar title for the tab specified by tabId. Automatically resets when the tab is closed."
+              },
+              "windowId": {
+                "type": "integer",
+                "optional": true,
+                "minimum": -2,
+                "description": "Sets the sidebar title for the window specified by windowId."
               }
             }
           }
         ]
       },
       {
         "name": "getTitle",
         "type": "function",
@@ -87,17 +93,23 @@
         "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."
+                "description": "Specify the tab to get the title from. If no tab nor window is specified, the global title is returned."
+              },
+              "windowId": {
+                "type": "integer",
+                "optional": true,
+                "minimum": -2,
+                "description": "Specify the window to get the title from. If no tab nor window is specified, the global title is returned."
               }
             }
           }
         ]
       },
       {
         "name": "setIcon",
         "type": "function",
@@ -132,16 +144,22 @@
                 ],
                 "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": "Sets the sidebar icon for the tab specified by tabId. Automatically resets when the tab is closed."
+              },
+              "windowId": {
+                "type": "integer",
+                "optional": true,
+                "minimum": -2,
+                "description": "Sets the sidebar icon for the window specified by windowId."
               }
             }
           }
         ]
       },
       {
         "name": "setPanel",
         "type": "function",
@@ -153,16 +171,22 @@
             "type": "object",
             "properties": {
               "tabId": {
                 "type": "integer",
                 "optional": true,
                 "minimum": 0,
                 "description": "Sets the sidebar url for the tab specified by tabId. Automatically resets when the tab is closed."
               },
+              "windowId": {
+                "type": "integer",
+                "optional": true,
+                "minimum": -2,
+                "description": "Sets the sidebar url for the window specified by windowId."
+              },
               "panel": {
                 "choices": [
                   {"type": "string"},
                   {"type": "null"}
                 ],
                 "description": "The url to the html file to show in a sidebar.  If set to the empty string (''), no sidebar is shown."
               }
             }
@@ -177,17 +201,23 @@
         "parameters": [
           {
             "name": "details",
             "type": "object",
             "properties": {
               "tabId": {
                 "type": "integer",
                 "optional": true,
-                "description": "Specify the tab to get the sidebar from. If no tab is specified, the non-tab-specific sidebar is returned."
+                "description": "Specify the tab to get the panel from. If no tab nor window is specified, the global panel is returned."
+              },
+              "windowId": {
+                "type": "integer",
+                "optional": true,
+                "minimum": -2,
+                "description": "Specify the window to get the panel from. If no tab nor window is specified, the global panel is returned."
               }
             }
           }
         ]
       },
       {
         "name": "open",
         "type": "function",
--- a/browser/components/extensions/test/browser/browser_ext_sidebarAction_context.js
+++ b/browser/components/extensions/test/browser/browser_ext_sidebarAction_context.js
@@ -6,32 +6,29 @@ ChromeUtils.import("resource://gre/modul
 
 SpecialPowers.pushPrefEnv({
   // Ignore toolbarbutton stuff, other test covers it.
   set: [["extensions.sidebar-button.shown", true]],
 });
 
 async function runTests(options) {
   async function background(getTests) {
-    async function checkDetails(expecting, tabId) {
-      let title = await browser.sidebarAction.getTitle({tabId});
+    async function checkDetails(expecting, details) {
+      let title = await browser.sidebarAction.getTitle(details);
       browser.test.assertEq(expecting.title, title,
-                            "expected value from getTitle");
+                            "expected value from getTitle in " + JSON.stringify(details));
 
-      let panel = await browser.sidebarAction.getPanel({tabId});
+      let panel = await browser.sidebarAction.getPanel(details);
       browser.test.assertEq(expecting.panel, panel,
-                            "expected value from getPanel");
+                            "expected value from getPanel in " + JSON.stringify(details));
     }
 
-    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.sidebarAction.setTitle({tabId, title: "foo"}),
         () => browser.sidebarAction.setIcon({tabId, path: "foo.png"}),
         () => browser.sidebarAction.setPanel({tabId, panel: "foo.html"}),
       ];
@@ -44,52 +41,59 @@ 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;
-    });
+    let [{id, windowId}] = await browser.tabs.query({active: true, currentWindow: true});
+    tabs.push(id);
+    windows.push(windowId);
   }
 
   let extension = ExtensionTestUtils.loadExtension({
     manifest: options.manifest,
     useAddonManager: "temporary",
 
     files: options.files || {},
 
     background: `(${background})(${options.getTests})`,
   });
 
   let sidebarActionId;
-  function checkDetails(details) {
+  function checkDetails(details, windowId) {
+    let {document} = Services.wm.getOuterWindowWithId(windowId);
     if (!sidebarActionId) {
       sidebarActionId = `${makeWidgetId(extension.id)}-sidebar-action`;
     }
 
     let command = document.getElementById(sidebarActionId);
     ok(command, "command exists");
 
     let menuId = `menu_${sidebarActionId}`;
@@ -98,18 +102,18 @@ async function runTests(options) {
 
     let title = details.title || options.manifest.name;
 
     is(getListStyleImage(menu), details.icon, "icon URL is correct");
     is(menu.getAttribute("label"), title, "image label is correct");
   }
 
   let awaitFinish = new Promise(resolve => {
-    extension.onMessage("nextTest", (expecting, testsRemaining) => {
-      checkDetails(expecting);
+    extension.onMessage("nextTest", (expecting, windowId, testsRemaining) => {
+      checkDetails(expecting, windowId);
 
       if (testsRemaining) {
         extension.sendMessage("runNextTest");
       } else {
         resolve();
       }
     });
   });
@@ -144,166 +148,149 @@ add_task(async function testTabSwitchCon
 
       "default_locale": "en",
 
       "permissions": ["tabs"],
     },
 
     "files": {
       "default.html": sidebar,
-      "default-2.html": sidebar,
+      "global.html": sidebar,
       "2.html": sidebar,
 
       "_locales/en/messages.json": {
         "panel": {
           "message": "default.html",
           "description": "Panel",
         },
 
         "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) {
+    getTests: function(tabs) {
       let details = [
         {"icon": browser.runtime.getURL("default.png"),
          "panel": browser.runtime.getURL("default.html"),
          "title": "Default Title",
         },
         {"icon": browser.runtime.getURL("1.png"),
-         "panel": browser.runtime.getURL("default.html"),
-         "title": "Default Title",
         },
         {"icon": browser.runtime.getURL("2.png"),
          "panel": browser.runtime.getURL("2.html"),
          "title": "Title 2",
         },
-        {"icon": browser.runtime.getURL("1.png"),
-         "panel": browser.runtime.getURL("default-2.html"),
-         "title": "Default Title 2",
-        },
-        {"icon": browser.runtime.getURL("1.png"),
-         "panel": browser.runtime.getURL("default-2.html"),
-         "title": "Default Title 2",
-        },
-        {"icon": browser.runtime.getURL("default-2.png"),
-         "panel": browser.runtime.getURL("default-2.html"),
-         "title": "Default Title 2",
+        {"icon": browser.runtime.getURL("global.png"),
+         "panel": browser.runtime.getURL("global.html"),
+         "title": "Global Title",
         },
         {"icon": browser.runtime.getURL("1.png"),
          "panel": browser.runtime.getURL("2.html"),
-         "title": "Default Title 2",
         },
       ];
 
       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.");
           await browser.sidebarAction.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);
 
-          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];
           await Promise.all([
             browser.sidebarAction.setIcon({tabId, path: "2.png"}),
             browser.sidebarAction.setPanel({tabId, panel: "2.html"}),
             browser.sidebarAction.setTitle({tabId, title: "Title 2"}),
           ]);
-          await expectDefaults(details[0]);
-          expect(details[2]);
+          expect(details[2], null, null, details[0]);
         },
         expect => {
           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(details[2]);
+              expect(details[2], null, null, details[0]);
             }
           });
 
           browser.tabs.update(tabs[1], {url: "about:blank?1"});
         },
         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.test.log("Change global values, expect those changes reflected.");
           await Promise.all([
-            browser.sidebarAction.setIcon({path: "default-2.png"}),
-            browser.sidebarAction.setPanel({panel: "default-2.html"}),
-            browser.sidebarAction.setTitle({title: "Default Title 2"}),
+            browser.sidebarAction.setIcon({path: "global.png"}),
+            browser.sidebarAction.setPanel({panel: "global.html"}),
+            browser.sidebarAction.setTitle({title: "Global Title"}),
           ]);
 
-          await expectDefaults(details[3]);
-          expect(details[3]);
+          expect(details[1], null, details[3], 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 step.");
           await browser.tabs.update(tabs[1], {active: true});
 
-          await expectDefaults(details[3]);
-          expect(details[2]);
+          expect(details[2], null, details[3], 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[3], 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[3], details[0]);
         },
         async expect => {
           browser.test.log("Delete tab.");
           await browser.tabs.remove(tabs[2]);
-          expect(details[4]);
+          expect(details[1], null, details[3], details[0]);
         },
         async expect => {
           browser.test.log("Change tab panel.");
           let tabId = tabs[0];
           await browser.sidebarAction.setPanel({tabId, panel: "2.html"});
-          expect(details[6]);
+          expect(details[4], null, details[3], details[0]);
         },
         async expect => {
           browser.test.log("Revert tab panel.");
           let tabId = tabs[0];
           await browser.sidebarAction.setPanel({tabId, panel: null});
-          expect(details[4]);
+          expect(details[1], null, details[3], details[0]);
         },
       ];
     },
   });
 });
 
 add_task(async function testDefaultTitle() {
   await runTests({
@@ -318,72 +305,62 @@ add_task(async function testDefaultTitle
       "permissions": ["tabs"],
     },
 
     files: {
       "sidebar.html": sidebar,
       "icon.png": imageBuffer,
     },
 
-    getTests: function(tabs, expectGlobals) {
+    getTests: function(tabs) {
       let details = [
         {"title": "Foo Extension",
          "panel": browser.runtime.getURL("sidebar.html"),
          "icon": browser.runtime.getURL("icon.png")},
-        {"title": "Foo Title",
-         "panel": browser.runtime.getURL("sidebar.html"),
-         "icon": browser.runtime.getURL("icon.png")},
-        {"title": "Bar Title",
-         "panel": browser.runtime.getURL("sidebar.html"),
-         "icon": browser.runtime.getURL("icon.png")},
+        {"title": "Foo Title"},
+        {"title": "Bar Title"},
       ];
 
       return [
         async expect => {
           browser.test.log("Initial state. Expect default extension title.");
 
-          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.sidebarAction.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.sidebarAction.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.sidebarAction.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.sidebarAction.setTitle({title: null});
 
-          await expectGlobals(details[0]);
-          expect(details[0]);
+          expect(null, null, null, details[0]);
         },
         async expect => {
           browser.test.assertRejects(
             browser.sidebarAction.setPanel({panel: "about:addons"}),
             /Access denied for URL about:addons/,
             "unable to set panel to about:addons");
 
-          await expectGlobals(details[0]);
-          expect(details[0]);
+          expect(null, null, null, details[0]);
         },
       ];
     },
   });
 });
 
 add_task(async function testPropertyRemoval() {
   await runTests({
@@ -396,98 +373,233 @@ add_task(async function testPropertyRemo
         "default_title": "Default Title",
       },
 
       "permissions": ["tabs"],
     },
 
     files: {
       "default.html": sidebar,
-      "p1.html": sidebar,
-      "p2.html": sidebar,
-      "p3.html": sidebar,
+      "global.html": sidebar,
+      "global2.html": sidebar,
+      "window.html": sidebar,
+      "tab.html": sidebar,
       "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"),
          "panel": browser.runtime.getURL("default.html"),
          "title": "Default Title"},
-        {"icon": browser.runtime.getURL("i1.png"),
-         "panel": browser.runtime.getURL("p1.html"),
-         "title": "t1"},
-        {"icon": browser.runtime.getURL("i2.png"),
-         "panel": browser.runtime.getURL("p2.html"),
-         "title": "t2"},
+        {"icon": browser.runtime.getURL("global.png"),
+         "panel": browser.runtime.getURL("global.html"),
+         "title": "global"},
+        {"icon": browser.runtime.getURL("window.png"),
+         "panel": browser.runtime.getURL("window.html"),
+         "title": "window"},
+        {"icon": browser.runtime.getURL("tab.png"),
+         "panel": browser.runtime.getURL("tab.html"),
+         "title": "tab"},
         {"icon": defaultIcon,
-         "panel": browser.runtime.getURL("p1.html"),
          "title": ""},
-        {"icon": browser.runtime.getURL("i3.png"),
-         "panel": browser.runtime.getURL("p3.html"),
-         "title": "t3"},
+        {"icon": browser.runtime.getURL("global2.png"),
+         "panel": browser.runtime.getURL("global2.html"),
+         "title": "global2"},
       ];
 
       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.sidebarAction.setIcon({path: "i1.png"});
-          browser.sidebarAction.setPanel({panel: "p1.html"});
-          browser.sidebarAction.setTitle({title: "t1"});
-          await expectGlobals(details[1]);
-          expect(details[1]);
+          browser.sidebarAction.setIcon({path: "global.png"});
+          browser.sidebarAction.setPanel({panel: "global.html"});
+          browser.sidebarAction.setTitle({title: "global"});
+          expect(null, null, details[1], details[0]);
+        },
+        async expect => {
+          browser.test.log("Set window values, expect the new values.");
+          let windowId = windows[0];
+          browser.sidebarAction.setIcon({windowId, path: "window.png"});
+          browser.sidebarAction.setPanel({windowId, panel: "window.html"});
+          browser.sidebarAction.setTitle({windowId, title: "window"});
+          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.sidebarAction.setIcon({tabId, path: "i2.png"});
-          browser.sidebarAction.setPanel({tabId, panel: "p2.html"});
-          browser.sidebarAction.setTitle({tabId, title: "t2"});
-          await expectGlobals(details[1]);
-          expect(details[2]);
+          browser.sidebarAction.setIcon({tabId, path: "tab.png"});
+          browser.sidebarAction.setPanel({tabId, panel: "tab.html"});
+          browser.sidebarAction.setTitle({tabId, title: "tab"});
+          expect(details[3], details[2], details[1], details[0]);
         },
         async expect => {
           browser.test.log("Set empty tab values.");
           let tabId = tabs[0];
           browser.sidebarAction.setIcon({tabId, path: ""});
           browser.sidebarAction.setPanel({tabId, panel: ""});
           browser.sidebarAction.setTitle({tabId, title: ""});
-          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.sidebarAction.setIcon({tabId, path: null});
           browser.sidebarAction.setPanel({tabId, panel: null});
           browser.sidebarAction.setTitle({tabId, title: 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.sidebarAction.setIcon({windowId, path: null});
+          browser.sidebarAction.setPanel({windowId, panel: null});
+          browser.sidebarAction.setTitle({windowId, title: null});
+          expect(null, null, details[1], details[0]);
         },
         async expect => {
           browser.test.log("Change global values, expect the new values.");
-          browser.sidebarAction.setIcon({path: "i3.png"});
-          browser.sidebarAction.setPanel({panel: "p3.html"});
-          browser.sidebarAction.setTitle({title: "t3"});
-          await expectGlobals(details[4]);
-          expect(details[4]);
+          browser.sidebarAction.setIcon({path: "global2.png"});
+          browser.sidebarAction.setPanel({panel: "global2.html"});
+          browser.sidebarAction.setTitle({title: "global2"});
+          expect(null, null, details[5], details[0]);
         },
         async expect => {
           browser.test.log("Remove global values, expect defaults.");
           browser.sidebarAction.setIcon({path: null});
           browser.sidebarAction.setPanel({panel: null});
           browser.sidebarAction.setTitle({title: null});
-          await expectGlobals(details[0]);
-          expect(details[0]);
+          expect(null, null, null, details[0]);
         },
       ];
     },
   });
 });
+
+add_task(async function testMultipleWindows() {
+  await runTests({
+    manifest: {
+      "name": "Foo Extension",
+
+      "sidebar_action": {
+        "default_icon": "default.png",
+        "default_panel": "default.html",
+        "default_title": "Default Title",
+      },
+
+      "permissions": ["tabs"],
+    },
+
+    files: {
+      "default.html": sidebar,
+      "window1.html": sidebar,
+      "window2.html": sidebar,
+      "default.png": imageBuffer,
+      "window1.png": imageBuffer,
+      "window2.png": imageBuffer,
+    },
+
+    getTests: function(tabs, windows) {
+      let details = [
+        {"icon": browser.runtime.getURL("default.png"),
+         "panel": browser.runtime.getURL("default.html"),
+         "title": "Default Title"},
+        {"icon": browser.runtime.getURL("window1.png"),
+         "panel": browser.runtime.getURL("window1.html"),
+         "title": "window1"},
+        {"icon": browser.runtime.getURL("window2.png"),
+         "panel": browser.runtime.getURL("window2.html"),
+         "title": "window2"},
+        {"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.sidebarAction.setIcon({windowId, path: "window1.png"});
+          browser.sidebarAction.setPanel({windowId, panel: "window1.html"});
+          browser.sidebarAction.setTitle({windowId, title: "window1"});
+          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.sidebarAction.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.sidebarAction.setIcon({windowId, path: "window2.png"});
+          browser.sidebarAction.setPanel({windowId, panel: "window2.html"});
+          browser.sidebarAction.setTitle({windowId, title: "window2"});
+          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"},
+            setPanel: {panel: "default.html"},
+            setTitle: {title: "Default Title"},
+            getPanel: {},
+            getTitle: {},
+          };
+          for (let [method, arg] of Object.entries(calls)) {
+            browser.test.assertThrows(
+              () => browser.sidebarAction[method]({...arg, windowId: -3}),
+              /-3 is too small \(must be at least -2\)/,
+              method + " with invalid windowId",
+            );
+            await browser.test.assertRejects(
+              browser.sidebarAction[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]);
+        },
+      ];
+    },
+  });
+});