Bug 1424538 - Allow pageAction and sidebarAction set* methods to accept a null value (desktop). r=mixedpuppy
authorOriol Brufau <oriol-bugzilla@hotmail.com>
Mon, 18 Dec 2017 20:41:10 +0100
changeset 400030 43b467005ab2c6dfe3a9b26d7e8739f270743c09
parent 400029 447b906be65b7ad5a6c6f2f9d6a00ebeeaa6cf94
child 400031 4a651569fe8d13e38e7cdf602430a899983ede0d
push id33288
push userrgurzau@mozilla.com
push dateSat, 20 Jan 2018 09:37:19 +0000
treeherdermozilla-central@b59b5e6e7070 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmixedpuppy
bugs1424538
milestone59.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 1424538 - Allow pageAction and sidebarAction set* methods to accept a null value (desktop). r=mixedpuppy 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();