--- 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",