Bug 1424538 - Allow pageAction and sidebarAction set* methods to accept a null value (desktop). draft
authorOriol Brufau <oriol-bugzilla@hotmail.com>
Mon, 18 Dec 2017 20:41:10 +0100
changeset 722673 e4b15b9f94c02770a08fa1ba1ae0600598d8514c
parent 722672 6ffbba9ce0ef9ec77a63445f068f2e218ed4830f
child 746653 a798f5ad66988061ef6f3414f0d24355a063b38e
push id96191
push userbmo:oriol-bugzilla@hotmail.com
push dateFri, 19 Jan 2018 14:03:28 +0000
bugs1424538
milestone59.0a1
Bug 1424538 - Allow pageAction and sidebarAction set* methods to accept a null value (desktop). MozReview-Commit-ID: FqcRrFDYqWp
browser/components/extensions/ext-pageAction.js
browser/components/extensions/ext-sidebarAction.js
browser/components/extensions/schemas/page_action.json
browser/components/extensions/schemas/sidebar_action.json
browser/components/extensions/test/browser/browser_ext_browserAction_context.js
browser/components/extensions/test/browser/browser_ext_pageAction_context.js
browser/components/extensions/test/browser/browser_ext_pageAction_title.js
browser/components/extensions/test/browser/browser_ext_sidebarAction.js
browser/components/extensions/test/browser/browser_ext_sidebarAction_context.js
toolkit/components/extensions/ExtensionParent.jsm
--- a/browser/components/extensions/ext-pageAction.js
+++ b/browser/components/extensions/ext-pageAction.js
@@ -274,32 +274,33 @@ this.pageAction = class extends Extensio
 
         isShown(details) {
           let tab = tabTracker.getTab(details.tabId);
           return pageAction.getProperty(tab, "show");
         },
 
         setTitle(details) {
           let tab = tabTracker.getTab(details.tabId);
-
-          // Clear the tab-specific title when given a null string.
-          pageAction.setProperty(tab, "title", details.title || null);
+          pageAction.setProperty(tab, "title", details.title);
         },
 
         getTitle(details) {
           let tab = tabTracker.getTab(details.tabId);
 
           let title = pageAction.getProperty(tab, "title");
           return Promise.resolve(title);
         },
 
         setIcon(details) {
           let tab = tabTracker.getTab(details.tabId);
 
           let icon = IconDetails.normalize(details, extension, context);
+          if (!Object.keys(icon).length) {
+            icon = null;
+          }
           pageAction.setProperty(tab, "icon", icon);
         },
 
         setPopup(details) {
           let tab = tabTracker.getTab(details.tabId);
 
           // Note: Chrome resolves arguments to setIcon relative to the calling
           // context, but resolves arguments to setPopup relative to the extension
--- a/browser/components/extensions/ext-sidebarAction.js
+++ b/browser/components/extensions/ext-sidebarAction.js
@@ -4,20 +4,16 @@
 
 // The ext-* files are imported into the same scopes.
 /* import-globals-from ext-browser.js */
 /* globals WINDOW_ID_CURRENT */
 
 Cu.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();
 
@@ -51,23 +47,24 @@ this.sidebarAction = class extends Exten
     this.browserStyle = options.browser_style || options.browser_style === null;
 
     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.defaults),
+    this.tabContext = new TabContext(tab => Object.create(this.globals),
                                      extension);
 
     // We need to ensure our elements are available before session restore.
     this.windowOpenListener = (window) => {
-      this.createMenuItem(window, this.defaults);
+      this.createMenuItem(window, this.globals);
     };
     windowTracker.addOpenListener(this.windowOpenListener);
 
     this.updateHeader = (event) => {
       let window = event.target.ownerGlobal;
       let details = this.tabContext.get(window.gBrowser.selectedTab);
       let header = window.document.getElementById("sidebar-switcher-target");
       if (window.SidebarUI.currentID === this.id) {
@@ -286,40 +283,44 @@ this.sidebarAction = class extends Exten
    * @param {XULElement|null} nativeTab
    *        Webextension tab object, may be null.
    * @param {string} prop
    *        String property to retrieve ["icon", "title", or "panel"].
    * @param {string} value
    *        Value for property.
    */
   setProperty(nativeTab, prop, value) {
+    let values;
     if (nativeTab === null) {
-      this.defaults[prop] = value;
-    } else if (value !== null) {
-      this.tabContext.get(nativeTab)[prop] = value;
+      values = this.globals;
     } else {
-      delete this.tabContext.get(nativeTab)[prop];
+      values = this.tabContext.get(nativeTab);
+    }
+    if (value === null) {
+      delete values[prop];
+    } else {
+      values[prop] = value;
     }
 
     this.updateOnChange(nativeTab);
   }
 
   /**
-   * Retrieve a property from the tab or defaults if tab is null.
+   * Retrieve a property from the tab or globals if tab is null.
    *
    * @param {XULElement|null} nativeTab
    *        Browser tab object, may be null.
    * @param {string} prop
    *        String property to retrieve ["icon", "title", or "panel"]
    * @returns {string} value
    *          Value for prop.
    */
   getProperty(nativeTab, prop) {
     if (nativeTab === null) {
-      return this.defaults[prop];
+      return this.globals[prop];
     }
     return this.tabContext.get(nativeTab)[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.
    *
@@ -376,53 +377,48 @@ this.sidebarAction = class extends Exten
       }
       return null;
     }
 
     return {
       sidebarAction: {
         async setTitle(details) {
           let nativeTab = getTab(details.tabId);
-
-          let title = details.title;
-          // Clear the tab-specific title when given a null string.
-          if (nativeTab && title === "") {
-            title = null;
-          }
-          sidebarAction.setProperty(nativeTab, "title", title);
+          sidebarAction.setProperty(nativeTab, "title", details.title);
         },
 
         getTitle(details) {
           let nativeTab = getTab(details.tabId);
 
           let title = sidebarAction.getProperty(nativeTab, "title");
           return Promise.resolve(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);
         },
 
         async setPanel(details) {
           let nativeTab = getTab(details.tabId);
 
           let url;
-          // Clear the tab-specific url when given a null string.
-          if (nativeTab && details.panel === "") {
+          // Clear the url when given null or empty string.
+          if (!details.panel) {
             url = null;
-          } else if (details.panel !== "") {
+          } else {
             url = context.uri.resolve(details.panel);
             if (!context.checkLoadURL(url)) {
               return Promise.reject({message: `Access denied for URL ${url}`});
             }
-          } else {
-            throw new ExtensionError("Invalid url for sidebar panel.");
           }
 
           sidebarAction.setProperty(nativeTab, "panel", url);
         },
 
         getPanel(details) {
           let nativeTab = getTab(details.tabId);
 
--- a/browser/components/extensions/schemas/page_action.json
+++ b/browser/components/extensions/schemas/page_action.json
@@ -119,17 +119,23 @@
         "type": "function",
         "description": "Sets the title of the page action. This is displayed in a tooltip over the page action.",
         "parameters": [
           {
             "name": "details",
             "type": "object",
             "properties": {
               "tabId": {"type": "integer", "minimum": 0, "description": "The id of the tab for which you want to modify the page action."},
-              "title": {"type": "string", "description": "The tooltip string."}
+              "title": {
+                "choices": [
+                  {"type": "string"},
+                  {"type": "null"}
+                ],
+                "description": "The tooltip string."
+              }
             }
           }
         ]
       },
       {
         "name": "getTitle",
         "type": "function",
         "description": "Gets the title of the page action.",
@@ -211,17 +217,20 @@
         "description": "Sets the html document to be opened as a popup when the user clicks on the page action's icon.",
         "parameters": [
           {
             "name": "details",
             "type": "object",
             "properties": {
               "tabId": {"type": "integer", "minimum": 0, "description": "The id of the tab for which you want to modify the page action."},
               "popup": {
-                "type": "string",
+                "choices": [
+                  {"type": "string"},
+                  {"type": "null"}
+                ],
                 "description": "The html file to show in a popup.  If set to the empty string (''), no popup is shown."
               }
             }
           }
         ]
       },
       {
         "name": "getPopup",
--- a/browser/components/extensions/schemas/sidebar_action.json
+++ b/browser/components/extensions/schemas/sidebar_action.json
@@ -59,17 +59,20 @@
         "description": "Sets the title of the sidebar action. This shows up in the tooltip.",
         "async": true,
         "parameters": [
           {
             "name": "details",
             "type": "object",
             "properties": {
               "title": {
-                "type": "string",
+                "choices": [
+                  {"type": "string"},
+                  {"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."
               }
             }
@@ -151,17 +154,20 @@
             "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."
               },
               "panel": {
-                "type": "string",
+                "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."
               }
             }
           }
         ]
       },
       {
         "name": "getPanel",
--- a/browser/components/extensions/test/browser/browser_ext_browserAction_context.js
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_context.js
@@ -468,34 +468,34 @@ add_task(async function testPropertyRemo
     "files": {
       "default.png": imageBuffer,
       "i1.png": imageBuffer,
       "i2.png": imageBuffer,
       "i3.png": imageBuffer,
     },
 
     getTests: function(tabs, expectGlobals) {
-      let contextUri = browser.runtime.getURL("_generated_background_page.html");
+      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": [0x11, 0x11, 0x11, 0xFF]},
         {"icon": browser.runtime.getURL("i2.png"),
          "popup": browser.runtime.getURL("p2.html"),
          "title": "t2",
          "badge": "b2",
          "badgeBackgroundColor": [0x22, 0x22, 0x22, 0xFF]},
-        {"icon": contextUri,
+        {"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",
--- a/browser/components/extensions/test/browser/browser_ext_pageAction_context.js
+++ b/browser/components/extensions/test/browser/browser_ext_pageAction_context.js
@@ -47,29 +47,30 @@ add_task(async function testTabSwitchCon
       },
 
       "default.png": imageBuffer,
       "1.png": imageBuffer,
       "2.png": imageBuffer,
     },
 
     getTests: function(tabs) {
+      let defaultIcon = "chrome://browser/content/extension.svg";
       let details = [
         {"icon": browser.runtime.getURL("default.png"),
          "popup": browser.runtime.getURL("default.html"),
          "title": "Default T\u00edtulo \u263a"},
         {"icon": browser.runtime.getURL("1.png"),
          "popup": browser.runtime.getURL("default.html"),
          "title": "Default T\u00edtulo \u263a"},
         {"icon": browser.runtime.getURL("2.png"),
          "popup": browser.runtime.getURL("2.html"),
          "title": "Title 2"},
-        {"icon": browser.runtime.getURL("2.png"),
-         "popup": browser.runtime.getURL("2.html"),
-         "title": "Default T\u00edtulo \u263a"},
+        {"icon": defaultIcon,
+         "popup": "",
+         "title": ""},
       ];
 
       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();
@@ -119,21 +120,31 @@ add_task(async function testTabSwitchCon
 
           let promise = promiseTabLoad({id: tabs[1], url: "about:blank?0#ref"});
           browser.tabs.update(tabs[1], {url: "about:blank?0#ref"});
           await promise;
 
           expect(details[2]);
         },
         expect => {
-          browser.test.log("Clear the title. Expect default title.");
+          browser.test.log("Set empty string values. Expect empty strings but default icon.");
+          browser.pageAction.setIcon({tabId: tabs[1], path: ""});
+          browser.pageAction.setPopup({tabId: tabs[1], popup: ""});
           browser.pageAction.setTitle({tabId: tabs[1], title: ""});
 
           expect(details[3]);
         },
+        expect => {
+          browser.test.log("Clear the values. Expect default ones.");
+          browser.pageAction.setIcon({tabId: tabs[1], path: null});
+          browser.pageAction.setPopup({tabId: tabs[1], popup: null});
+          browser.pageAction.setTitle({tabId: tabs[1], title: null});
+
+          expect(details[0]);
+        },
         async expect => {
           browser.test.log("Navigate to a new page. Expect icon hidden.");
 
           // TODO: This listener should not be necessary, but the |tabs.update|
           // callback currently fires too early in e10s windows.
           let promise = promiseTabLoad({id: tabs[1], url: "about:blank?1"});
 
           browser.tabs.update(tabs[1], {url: "about:blank?1"});
--- a/browser/components/extensions/test/browser/browser_ext_pageAction_title.js
+++ b/browser/components/extensions/test/browser/browser_ext_pageAction_title.js
@@ -59,16 +59,19 @@ add_task(async function testTabSwitchCon
         {"icon": browser.runtime.getURL("1.png"),
          "popup": browser.runtime.getURL("default.html"),
          "title": "Default T\u00edtulo \u263a"},
         {"icon": browser.runtime.getURL("2.png"),
          "popup": browser.runtime.getURL("2.html"),
          "title": "Title 2"},
         {"icon": browser.runtime.getURL("2.png"),
          "popup": browser.runtime.getURL("2.html"),
+         "title": ""},
+        {"icon": browser.runtime.getURL("2.png"),
+         "popup": browser.runtime.getURL("2.html"),
          "title": "Default T\u00edtulo \u263a"},
       ];
 
       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);
@@ -119,21 +122,27 @@ add_task(async function testTabSwitchCon
           let promise = promiseTabLoad({id: tabs[1], url: "about:blank?0#ref"});
 
           browser.tabs.update(tabs[1], {url: "about:blank?0#ref"});
 
           await promise;
           expect(details[2]);
         },
         expect => {
-          browser.test.log("Clear the title. Expect default title.");
+          browser.test.log("Set empty title. Expect empty title.");
           browser.pageAction.setTitle({tabId: tabs[1], title: ""});
 
           expect(details[3]);
         },
+        expect => {
+          browser.test.log("Clear the title. Expect default title.");
+          browser.pageAction.setTitle({tabId: tabs[1], title: null});
+
+          expect(details[4]);
+        },
         async expect => {
           browser.test.log("Navigate to a new page. Expect icon hidden.");
 
           // TODO: This listener should not be necessary, but the |tabs.update|
           // callback currently fires too early in e10s windows.
           let promise = promiseTabLoad({id: tabs[1], url: "about:blank?1"});
 
           browser.tabs.update(tabs[1], {url: "about:blank?1"});
@@ -191,16 +200,19 @@ add_task(async function testDefaultTitle
     getTests: function(tabs) {
       let details = [
         {"title": "Foo Extension",
          "popup": "",
          "icon": browser.runtime.getURL("icon.png")},
         {"title": "Foo Title",
          "popup": "",
          "icon": browser.runtime.getURL("icon.png")},
+        {"title": "",
+         "popup": "",
+         "icon": browser.runtime.getURL("icon.png")},
       ];
 
       return [
         expect => {
           browser.test.log("Initial state. No icon visible.");
           expect(null);
         },
         async expect => {
@@ -209,16 +221,21 @@ add_task(async function testDefaultTitle
           expect(details[0]);
         },
         expect => {
           browser.test.log("Change the title. Expect new title.");
           browser.pageAction.setTitle({tabId: tabs[0], title: "Foo Title"});
           expect(details[1]);
         },
         expect => {
+          browser.test.log("Set empty title. Expect empty title.");
+          browser.pageAction.setTitle({tabId: tabs[0], title: ""});
+          expect(details[2]);
+        },
+        expect => {
           browser.test.log("Clear the title. Expect extension title.");
-          browser.pageAction.setTitle({tabId: tabs[0], title: ""});
+          browser.pageAction.setTitle({tabId: tabs[0], title: null});
           expect(details[0]);
         },
       ];
     },
   });
 });
--- a/browser/components/extensions/test/browser/browser_ext_sidebarAction.js
+++ b/browser/components/extensions/test/browser/browser_ext_sidebarAction.js
@@ -29,21 +29,22 @@ let extData = {
         browser.test.sendMessage("sidebar");
       };
     },
   },
 
   background: function() {
     browser.test.onMessage.addListener(async ({msg, data}) => {
       if (msg === "set-panel") {
-        await browser.sidebarAction.setPanel({panel: ""}).then(() => {
-          browser.test.notifyFail("empty panel settable");
-        }).catch(() => {
-          browser.test.notifyPass("unable to set empty panel");
-        });
+        await browser.sidebarAction.setPanel({panel: null});
+        browser.test.assertEq(
+          await browser.sidebarAction.getPanel({}),
+          browser.runtime.getURL("sidebar.html"),
+          "Global panel can be reverted to the default."
+        );
       } else if (msg === "isOpen") {
         let {arg = {}, result} = data;
         let isOpen = await browser.sidebarAction.isOpen(arg);
         browser.test.assertEq(result, isOpen, "expected value from isOpen");
       }
       browser.test.sendMessage("done");
     });
   },
@@ -91,17 +92,16 @@ add_task(async function sidebar_two_side
 
 add_task(async function sidebar_empty_panel() {
   let extension = ExtensionTestUtils.loadExtension(extData);
   await extension.startup();
   // Test sidebar is opened on install
   await extension.awaitMessage("sidebar");
   ok(!document.getElementById("sidebar-box").hidden, "sidebar box is visible in first window");
   await sendMessage(extension, "set-panel");
-  await extension.awaitFinish();
   await extension.unload();
 });
 
 add_task(async function sidebar_isOpen() {
   info("Load extension1");
   let extension1 = ExtensionTestUtils.loadExtension(extData);
   await extension1.startup();
 
--- a/browser/components/extensions/test/browser/browser_ext_sidebarAction_context.js
+++ b/browser/components/extensions/test/browser/browser_ext_sidebarAction_context.js
@@ -293,17 +293,17 @@ add_task(async function testTabSwitchCon
           browser.test.log("Change tab panel.");
           let tabId = tabs[0];
           await browser.sidebarAction.setPanel({tabId, panel: "2.html"});
           expect(details[6]);
         },
         async expect => {
           browser.test.log("Revert tab panel.");
           let tabId = tabs[0];
-          await browser.sidebarAction.setPanel({tabId, panel: ""});
+          await browser.sidebarAction.setPanel({tabId, panel: null});
           expect(details[4]);
         },
       ];
     },
   });
 });
 
 add_task(async function testDefaultTitle() {
@@ -319,72 +319,176 @@ add_task(async function testDefaultTitle
       "permissions": ["tabs"],
     },
 
     files: {
       "sidebar.html": sidebar,
       "icon.png": imageBuffer,
     },
 
-    getTests: function(tabs, expectDefaults) {
+    getTests: function(tabs, expectGlobals) {
       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": "",
-         "panel": browser.runtime.getURL("sidebar.html"),
-         "icon": browser.runtime.getURL("icon.png")},
       ];
 
       return [
         async expect => {
-          browser.test.log("Initial state. Expect extension title as default title.");
+          browser.test.log("Initial state. Expect default extension title.");
 
-          await expectDefaults(details[0]);
+          await expectGlobals(details[0]);
           expect(details[0]);
         },
         async expect => {
-          browser.test.log("Change the title. Expect new title.");
+          browser.test.log("Change the tab title. Expect new title.");
           browser.sidebarAction.setTitle({tabId: tabs[0], title: "Foo Title"});
 
-          await expectDefaults(details[0]);
+          await expectGlobals(details[0]);
           expect(details[1]);
         },
         async expect => {
-          browser.test.log("Change the default. Expect same properties.");
+          browser.test.log("Change the global title. Expect same properties.");
           browser.sidebarAction.setTitle({title: "Bar Title"});
 
-          await expectDefaults(details[2]);
+          await expectGlobals(details[2]);
           expect(details[1]);
         },
         async expect => {
-          browser.test.log("Clear the title. Expect new default title.");
-          browser.sidebarAction.setTitle({tabId: tabs[0], title: ""});
+          browser.test.log("Clear the tab title. Expect new global title.");
+          browser.sidebarAction.setTitle({tabId: tabs[0], title: null});
 
-          await expectDefaults(details[2]);
+          await expectGlobals(details[2]);
           expect(details[2]);
         },
         async expect => {
-          browser.test.log("Set default title to null string. Expect null string from API, extension title in UI.");
-          browser.sidebarAction.setTitle({title: ""});
+          browser.test.log("Clear the global title. Expect default title.");
+          browser.sidebarAction.setTitle({title: null});
 
-          await expectDefaults(details[3]);
-          expect(details[3]);
+          await expectGlobals(details[0]);
+          expect(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 expectDefaults(details[3]);
-          expect(details[3]);
+          await expectGlobals(details[0]);
+          expect(details[0]);
         },
       ];
     },
   });
 });
+
+add_task(async function testPropertyRemoval() {
+  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,
+      "p1.html": sidebar,
+      "p2.html": sidebar,
+      "p3.html": sidebar,
+      "default.png": imageBuffer,
+      "i1.png": imageBuffer,
+      "i2.png": imageBuffer,
+      "i3.png": imageBuffer,
+    },
+
+    getTests: function(tabs, expectGlobals) {
+      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": defaultIcon,
+         "panel": browser.runtime.getURL("p1.html"),
+         "title": ""},
+        {"icon": browser.runtime.getURL("i3.png"),
+         "panel": browser.runtime.getURL("p3.html"),
+         "title": "t3"},
+      ];
+
+      return [
+        async expect => {
+          browser.test.log("Initial state, expect default properties.");
+          await expectGlobals(details[0]);
+          expect(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]);
+        },
+        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]);
+        },
+        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]);
+        },
+        async expect => {
+          browser.test.log("Remove tab values, expect global 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]);
+        },
+        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]);
+        },
+        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]);
+        },
+      ];
+    },
+  });
+});
--- a/toolkit/components/extensions/ExtensionParent.jsm
+++ b/toolkit/components/extensions/ExtensionParent.jsm
@@ -1294,24 +1294,26 @@ let IconDetails = {
       let baseURI = context ? context.uri : extension.baseURI;
 
       if (path != null) {
         if (typeof path != "object") {
           path = {"19": path};
         }
 
         for (let size of Object.keys(path)) {
-          let url = baseURI.resolve(path[size]);
+          let url = path[size];
+          if (url) {
+            url = baseURI.resolve(path[size]);
 
-          // The Chrome documentation specifies these parameters as
-          // relative paths. We currently accept absolute URLs as well,
-          // which means we need to check that the extension is allowed
-          // to load them. This will throw an error if it's not allowed.
-          this._checkURL(url, extension);
-
+            // The Chrome documentation specifies these parameters as
+            // relative paths. We currently accept absolute URLs as well,
+            // which means we need to check that the extension is allowed
+            // to load them. This will throw an error if it's not allowed.
+            this._checkURL(url, extension);
+          }
           result[size] = url;
         }
       }
 
       if (themeIcons) {
         themeIcons.forEach(({size, light, dark}) => {
           let lightURL = baseURI.resolve(light);
           let darkURL = baseURI.resolve(dark);
@@ -1363,17 +1365,17 @@ let IconDetails = {
       let sizes = Object.keys(icons)
                         .map(key => parseInt(key, 10))
                         .sort((a, b) => a - b);
 
       bestSize = sizes.find(candidate => candidate > size) || sizes.pop();
     }
 
     if (bestSize) {
-      return {size: bestSize, icon: icons[bestSize]};
+      return {size: bestSize, icon: icons[bestSize] || DEFAULT};
     }
 
     return {size, icon: DEFAULT};
   },
 
   convertImageURLToDataURL(imageURL, contentWindow, browserWindow, size = 18) {
     return new Promise((resolve, reject) => {
       let image = new contentWindow.Image();