Bug 1419940 - Allow browserAction set* methods to accept a null value. draft
authorOriol Brufau <oriol-bugzilla@hotmail.com>
Wed, 29 Nov 2017 05:50:00 +0100
changeset 706579 223a3aa034d7bce3fe851ae85630a7fdbab5fabb
parent 704194 5b33b070378ae0806bed0b5e5e34de429a29e7db
child 742690 cae4fecf9b7fd9987cc1bd36671c93e1c06cd770
push id91836
push userbmo:oriol-bugzilla@hotmail.com
push dateSat, 02 Dec 2017 14:39:10 +0000
bugs1419940
milestone59.0a1
Bug 1419940 - Allow browserAction set* methods to accept a null value. MozReview-Commit-ID: H2UfUITBEMm
browser/components/extensions/ext-browserAction.js
browser/components/extensions/schemas/browser_action.json
browser/components/extensions/test/browser/browser_ext_browserAction_context.js
toolkit/components/extensions/ExtensionParent.jsm
toolkit/components/extensions/Schemas.jsm
--- a/browser/components/extensions/ext-browserAction.js
+++ b/browser/components/extensions/ext-browserAction.js
@@ -87,16 +87,17 @@ this.browserAction = class extends Exten
     this.defaults = {
       enabled: true,
       title: options.default_title || extension.name,
       badgeText: "",
       badgeBackgroundColor: null,
       popup: options.default_popup || "",
       area: browserAreas[options.default_area || "navbar"],
     };
+    this.globals = Object.create(this.defaults);
 
     this.browserStyle = options.browser_style || false;
     if (options.browser_style === null) {
       this.extension.logger.warn("Please specify whether you want browser_style " +
                                  "or not in your browser_action options.");
     }
 
     browserActionMap.set(extension, this);
@@ -110,17 +111,17 @@ this.browserAction = class extends Exten
       }, extension));
 
     this.iconData.set(
       this.defaults.icon,
       await StartupCache.get(
         extension, ["browserAction", "default_icon_data"],
         () => this.getIconData(this.defaults.icon)));
 
-    this.tabContext = new TabContext(tab => Object.create(this.defaults),
+    this.tabContext = new TabContext(tab => Object.create(this.globals),
                                      extension);
 
     // eslint-disable-next-line mozilla/balanced-listeners
     this.tabContext.on("location-change", this.handleLocationChange.bind(this));
 
     this.build();
   }
 
@@ -183,17 +184,17 @@ this.browserAction = class extends Exten
         node.classList.add("badged-button");
         node.classList.add("webextension-browser-action");
         node.setAttribute("constrain-size", "true");
 
         node.onmousedown = event => this.handleEvent(event);
         node.onmouseover = event => this.handleEvent(event);
         node.onmouseout = event => this.handleEvent(event);
 
-        this.updateButton(node, this.defaults, true);
+        this.updateButton(node, this.globals, true);
       },
 
       onViewShowing: async event => {
         TelemetryStopwatch.start(POPUP_OPEN_MS_HISTOGRAM, this);
         let document = event.target.ownerDocument;
         let tabbrowser = document.defaultView.gBrowser;
 
         let tab = tabbrowser.selectedTab;
@@ -543,32 +544,36 @@ this.browserAction = class extends Exten
         this.updateWindow(window);
       }
     }
   }
 
   // tab is allowed to be null.
   // prop should be one of "icon", "title", "badgeText", "popup", or "badgeBackgroundColor".
   setProperty(tab, prop, value) {
+    let values;
     if (tab == null) {
-      this.defaults[prop] = value;
-    } else if (value != null) {
-      this.tabContext.get(tab)[prop] = value;
+      values = this.globals;
     } else {
-      delete this.tabContext.get(tab)[prop];
+      values = this.tabContext.get(tab);
+    }
+    if (value == null) {
+      delete values[prop];
+    } else {
+      values[prop] = value;
     }
 
     this.updateOnChange(tab);
   }
 
   // tab is allowed to be null.
   // prop should be one of "title", "badgeText", "popup", or "badgeBackgroundColor".
   getProperty(tab, prop) {
     if (tab == null) {
-      return this.defaults[prop];
+      return this.globals[prop];
     }
     return this.tabContext.get(tab)[prop];
   }
 
   getAPI(context) {
     let {extension} = context;
     let {tabManager} = extension;
 
@@ -602,37 +607,35 @@ this.browserAction = class extends Exten
         disable: function(tabId) {
           let tab = getTab(tabId);
           browserAction.setProperty(tab, "enabled", false);
         },
 
         setTitle: function(details) {
           let tab = getTab(details.tabId);
 
-          let title = details.title;
-          // Clear the tab-specific title when given a null string.
-          if (tab && title == "") {
-            title = null;
-          }
-          browserAction.setProperty(tab, "title", title);
+          browserAction.setProperty(tab, "title", details.title);
         },
 
         getTitle: function(details) {
           let tab = getTab(details.tabId);
 
           let title = browserAction.getProperty(tab, "title");
           return Promise.resolve(title);
         },
 
         setIcon: function(details) {
           let tab = getTab(details.tabId);
 
           details.iconType = "browserAction";
 
           let icon = IconDetails.normalize(details, extension, context);
+          if (!Object.keys(icon).length) {
+            icon = null;
+          }
           browserAction.setProperty(tab, "icon", icon);
         },
 
         setBadgeText: function(details) {
           let tab = getTab(details.tabId);
 
           browserAction.setProperty(tab, "badgeText", details.text);
         },
--- a/browser/components/extensions/schemas/browser_action.json
+++ b/browser/components/extensions/schemas/browser_action.json
@@ -84,17 +84,20 @@
         "description": "Sets the title of the browser action. This shows up in the tooltip.",
         "async": "callback",
         "parameters": [
           {
             "name": "details",
             "type": "object",
             "properties": {
               "title": {
-                "type": "string",
+                "choices": [
+                  {"type": "string"},
+                  {"type": "null"}
+                ],
                 "description": "The string the browser action should display when moused over."
               },
               "tabId": {
                 "type": "integer",
                 "optional": true,
                 "description": "Limits the change to when a particular tab is selected. Automatically resets when the tab is closed."
               }
             }
@@ -199,17 +202,20 @@
             "properties": {
               "tabId": {
                 "type": "integer",
                 "optional": true,
                 "minimum": 0,
                 "description": "Limits the change to when a particular tab is selected. Automatically resets when the tab is closed."
               },
               "popup": {
-                "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."
               }
             }
           },
           {
             "type": "function",
             "name": "callback",
             "optional": true,
@@ -252,17 +258,20 @@
         "description": "Sets the badge text for the browser action. The badge is displayed on top of the icon.",
         "async": "callback",
         "parameters": [
           {
             "name": "details",
             "type": "object",
             "properties": {
               "text": {
-                "type": "string",
+                "choices": [
+                  {"type": "string"},
+                  {"type": "null"}
+                ],
                 "description": "Any number of characters can be passed, but only about four can fit in the space."
               },
               "tabId": {
                 "type": "integer",
                 "optional": true,
                 "description": "Limits the change to when a particular tab is selected. Automatically resets when the tab is closed."
               }
             }
@@ -313,17 +322,18 @@
           {
             "name": "details",
             "type": "object",
             "properties": {
               "color": {
                 "description": "An array of four integers in the range [0,255] that make up the RGBA color of the badge. For example, opaque red is <code>[255, 0, 0, 255]</code>. Can also be a string with a CSS value, with opaque red being <code>#FF0000</code> or <code>#F00</code>.",
                 "choices": [
                   {"type": "string"},
-                  {"$ref": "ColorArray"}
+                  {"$ref": "ColorArray"},
+                  {"type": "null"}
                 ]
               },
               "tabId": {
                 "type": "integer",
                 "optional": true,
                 "description": "Limits the change to when a particular tab is selected. Automatically resets when the tab is closed."
               }
             }
--- a/browser/components/extensions/test/browser/browser_ext_browserAction_context.js
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_context.js
@@ -332,17 +332,17 @@ add_task(async function testDefaultTitle
 
       "permissions": ["tabs"],
     },
 
     files: {
       "icon.png": imageBuffer,
     },
 
-    getTests: function(tabs, expectDefaults) {
+    getTests: function(tabs, expectGlobals) {
       const DEFAULT_BADGE_COLOR = [0xd9, 0, 0, 255];
 
       let details = [
         {"title": "Foo Extension",
          "popup": "",
          "badge": "",
          "badgeBackgroundColor": DEFAULT_BADGE_COLOR,
          "icon": browser.runtime.getURL("icon.png")},
@@ -351,66 +351,61 @@ add_task(async function testDefaultTitle
          "badge": "",
          "badgeBackgroundColor": DEFAULT_BADGE_COLOR,
          "icon": browser.runtime.getURL("icon.png")},
         {"title": "Bar Title",
          "popup": "",
          "badge": "",
          "badgeBackgroundColor": DEFAULT_BADGE_COLOR,
          "icon": browser.runtime.getURL("icon.png")},
-        {"title": "",
-         "popup": "",
-         "badge": "",
-         "badgeBackgroundColor": DEFAULT_BADGE_COLOR,
-         "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 title as global 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.browserAction.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.browserAction.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.browserAction.setTitle({tabId: tabs[0], title: ""});
+          browser.test.log("Clear the tab title. Expect new global title.");
+          browser.browserAction.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.browserAction.setTitle({title: ""});
+          browser.test.log("Clear the global title. Expect default title.");
+          browser.browserAction.setTitle({title: null});
 
-          await expectDefaults(details[3]);
-          expect(details[3]);
+          await expectGlobals(details[0]);
+          expect(details[0]);
         },
         async expect => {
           browser.test.assertRejects(
             browser.browserAction.setPopup({popup: "about:addons"}),
             /Access denied for URL about:addons/,
             "unable to set popup to about:addons");
 
-          await expectDefaults(details[3]);
-          expect(details[3]);
+          await expectGlobals(details[0]);
+          expect(details[0]);
         },
       ];
     },
   });
 });
 
 add_task(async function testBadgeColorPersistence() {
   const extension = ExtensionTestUtils.loadExtension({
@@ -450,8 +445,141 @@ add_task(async function testBadgeColorPe
 
   badge = getBadgeForWindow(win);
   is(badge.value, "hi", "badge text is set in new window");
   is(badge.style.backgroundColor, "rgb(0, 255, 0)", "badge color is set in new window");
 
   await BrowserTestUtils.closeWindow(win);
   await extension.unload();
 });
+
+add_task(async function testPropertyRemoval() {
+  await runTests({
+    manifest: {
+      "browser_action": {
+        "default_icon": "default.png",
+        "default_popup": "default.html",
+        "default_title": "Default Title",
+      },
+    },
+
+    "files": {
+      "default.png": imageBuffer,
+      "i1.png": imageBuffer,
+      "i2.png": imageBuffer,
+      "i3.png": imageBuffer,
+    },
+
+    getTests: function(tabs, expectGlobals) {
+      let contextUri = browser.runtime.getURL("_generated_background_page.html");
+      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,
+         "popup": "",
+         "title": "",
+         "badge": "",
+         "badgeBackgroundColor": [0x11, 0x11, 0x11, 0xFF]},
+        {"icon": contextUri,
+         "popup": "",
+         "title": "",
+         "badge": "",
+         "badgeBackgroundColor": [0x22, 0x22, 0x22, 0xFF]},
+        {"icon": browser.runtime.getURL("i3.png"),
+         "popup": browser.runtime.getURL("p3.html"),
+         "title": "t3",
+         "badge": "b3",
+         "badgeBackgroundColor": [0x33, 0x33, 0x33, 0xFF]},
+      ];
+
+      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.browserAction.setIcon({path: "i1.png"});
+          browser.browserAction.setPopup({popup: "p1.html"});
+          browser.browserAction.setTitle({title: "t1"});
+          browser.browserAction.setBadgeText({text: "b1"});
+          browser.browserAction.setBadgeBackgroundColor({color: "#111"});
+          await expectGlobals(details[1]);
+          expect(details[1]);
+        },
+        async expect => {
+          browser.test.log("Set tab values, expect the new values.");
+          let tabId = tabs[0];
+          browser.browserAction.setIcon({tabId, path: "i2.png"});
+          browser.browserAction.setPopup({tabId, popup: "p2.html"});
+          browser.browserAction.setTitle({tabId, title: "t2"});
+          browser.browserAction.setBadgeText({tabId, text: "b2"});
+          browser.browserAction.setBadgeBackgroundColor({tabId, color: "#222"});
+          await expectGlobals(details[1]);
+          expect(details[2]);
+        },
+        async expect => {
+          browser.test.log("Set empty tab values, expect empty values except for bgcolor.");
+          let tabId = tabs[0];
+          browser.browserAction.setIcon({tabId, path: ""});
+          browser.browserAction.setPopup({tabId, popup: ""});
+          browser.browserAction.setTitle({tabId, title: ""});
+          browser.browserAction.setBadgeText({tabId, text: ""});
+          browser.browserAction.setBadgeBackgroundColor({tabId, color: ""});
+          await expectGlobals(details[1]);
+          expect(details[3]);
+        },
+        async expect => {
+          browser.test.log("The invalid color removed tab bgcolor, restore previous tab bgcolor.");
+          let tabId = tabs[0];
+          browser.browserAction.setBadgeBackgroundColor({tabId, color: "#222"});
+          await expectGlobals(details[1]);
+          expect(details[4]);
+        },
+        async expect => {
+          browser.test.log("Remove tab values, expect global values.");
+          let tabId = tabs[0];
+          browser.browserAction.setIcon({tabId, path: null});
+          browser.browserAction.setPopup({tabId, popup: null});
+          browser.browserAction.setTitle({tabId, title: null});
+          browser.browserAction.setBadgeText({tabId, text: null});
+          browser.browserAction.setBadgeBackgroundColor({tabId, color: null});
+          await expectGlobals(details[1]);
+          expect(details[1]);
+        },
+        async expect => {
+          browser.test.log("Change global values, expect the new values.");
+          browser.browserAction.setIcon({path: "i3.png"});
+          browser.browserAction.setPopup({popup: "p3.html"});
+          browser.browserAction.setTitle({title: "t3"});
+          browser.browserAction.setBadgeText({text: "b3"});
+          browser.browserAction.setBadgeBackgroundColor({color: "#333"});
+          await expectGlobals(details[5]);
+          expect(details[5]);
+        },
+        async expect => {
+          browser.test.log("Remove global values, expect defaults.");
+          browser.browserAction.setIcon({path: null});
+          browser.browserAction.setPopup({popup: null});
+          browser.browserAction.setBadgeText({text: null});
+          browser.browserAction.setTitle({title: null});
+          browser.browserAction.setBadgeBackgroundColor({color: null});
+          await expectGlobals(details[0]);
+          expect(details[0]);
+        },
+      ];
+    },
+  });
+});
--- a/toolkit/components/extensions/ExtensionParent.jsm
+++ b/toolkit/components/extensions/ExtensionParent.jsm
@@ -1255,17 +1255,17 @@ let IconDetails = {
   //
   // If a context is specified (function is called from an extension):
   // Throws an error if an invalid icon size was provided or the
   // extension is not allowed to load the specified resources.
   //
   // If no context is specified, instead of throwing an error, this
   // function simply logs a warning message.
   normalize(details, extension, context = null) {
-    if (!details.imageData && details.path) {
+    if (!details.imageData && details.path != null) {
       // Pick a cache key for the icon paths. If the path is a string,
       // use it directly. Otherwise, stringify the path object.
       let key = details.path;
       if (typeof key !== "string") {
         key = uneval(key);
       }
 
       let icons = this.iconCache.get(extension)
@@ -1296,17 +1296,17 @@ let IconDetails = {
 
         for (let size of Object.keys(imageData)) {
           result[size] = imageData[size];
         }
       }
 
       let baseURI = context ? context.uri : extension.baseURI;
 
-      if (path) {
+      if (path != null) {
         if (typeof path != "object") {
           path = {"19": path};
         }
 
         for (let size of Object.keys(path)) {
           let url = baseURI.resolve(path[size]);
 
           // The Chrome documentation specifies these parameters as
--- a/toolkit/components/extensions/Schemas.jsm
+++ b/toolkit/components/extensions/Schemas.jsm
@@ -1442,16 +1442,26 @@ class StringType extends Type {
 
       return {
         descriptor: {value: obj},
       };
     }
   }
 }
 
+class NullType extends Type {
+  normalize(value, context) {
+    return this.normalizeBase("null", value, context);
+  }
+
+  checkBaseType(baseType) {
+    return baseType == "null";
+  }
+}
+
 let FunctionEntry;
 let Event;
 let SubModuleType;
 
 class ObjectType extends Type {
   static get EXTRA_PROPERTIES() {
     return ["properties", "patternProperties", ...super.EXTRA_PROPERTIES];
   }
@@ -2356,16 +2366,17 @@ Event = class Event extends CallEntry { 
 };
 
 const TYPES = Object.freeze(Object.assign(Object.create(null), {
   any: AnyType,
   array: ArrayType,
   boolean: BooleanType,
   function: FunctionType,
   integer: IntegerType,
+  null: NullType,
   number: NumberType,
   object: ObjectType,
   string: StringType,
 }));
 
 const LOADERS = {
   events: "loadEvent",
   functions: "loadFunction",