Bug 1390464 - Add windowId parameter in sidebarAction methods draft
authorOriol Brufau <oriol-bugzilla@hotmail.com>
Sun, 28 Jan 2018 20:46:24 +0100
changeset 781731 048c58d5035468c2308a8fd79cb12f00cd508861
parent 781489 da809ecceaf3a8ada0aa2d7115822d39d0439654
child 782073 2b901ccf3f7c565a5ba30745b41f26f4cbef02c0
push id106397
push userbmo:oriol-bugzilla@hotmail.com
push dateFri, 13 Apr 2018 14:57:55 +0000
bugs1390464
milestone61.0a1
Bug 1390464 - Add windowId parameter in sidebarAction methods 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]);
+        },
+      ];
+    },
+  });
+});