Bug 1349944 - Add browser.theme.onUpdated to watch for theme updates. r=jaws, mixedpuppy
MozReview-Commit-ID: JX4gRa3Qt0p
--- a/toolkit/components/extensions/ext-theme.js
+++ b/toolkit/components/extensions/ext-theme.js
@@ -1,11 +1,11 @@
"use strict";
-/* global windowTracker */
+/* global windowTracker, EventManager, EventEmitter */
Cu.import("resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeManager",
"resource://gre/modules/LightweightThemeManager.jsm");
XPCOMUtils.defineLazyGetter(this, "gThemesEnabled", () => {
return Services.prefs.getBoolPref("extensions.webextensions.themes.enabled");
@@ -292,16 +292,18 @@ class Theme {
}
LightweightThemeManager.fallbackThemeData = null;
Services.obs.notifyObservers(null,
"lightweight-theme-styling-update",
JSON.stringify(this.lwtStyles));
}
}
+const onUpdatedEmitter = new EventEmitter();
+
this.theme = class extends ExtensionAPI {
onManifestEntry(entryName) {
if (!gThemesEnabled) {
// Return early if themes are disabled.
return;
}
let {extension} = this;
@@ -309,22 +311,24 @@ this.theme = class extends ExtensionAPI
if (!gThemesEnabled) {
// Return early if themes are disabled.
return;
}
this.theme = new Theme(extension.baseURI, extension.logger);
this.theme.load(manifest.theme);
+ onUpdatedEmitter.emit("theme-updated", manifest.theme);
}
onShutdown() {
if (this.theme) {
this.theme.unload();
}
+ onUpdatedEmitter.emit("theme-updated", {});
}
getAPI(context) {
let {extension} = context;
return {
theme: {
getCurrent: (windowId) => {
@@ -357,16 +361,17 @@ this.theme = class extends ExtensionAPI
this.theme = new Theme(extension.baseURI, extension.logger);
}
let browserWindow;
if (windowId !== null) {
browserWindow = windowTracker.getWindow(windowId, context);
}
this.theme.load(details, browserWindow);
+ onUpdatedEmitter.emit("theme-updated", details, windowId);
},
reset: (windowId) => {
if (!gThemesEnabled) {
// Return early if themes are disabled.
return;
}
if (!this.theme) {
@@ -375,13 +380,28 @@ this.theme = class extends ExtensionAPI
}
let browserWindow;
if (windowId !== null) {
browserWindow = windowTracker.getWindow(windowId, context);
}
this.theme.unload(browserWindow);
+ onUpdatedEmitter.emit("theme-updated", {}, windowId);
},
+ onUpdated: new EventManager(context, "theme.onUpdated", fire => {
+ let callback = (event, theme, windowId) => {
+ if (windowId) {
+ fire.async({theme, windowId});
+ } else {
+ fire.async({theme});
+ }
+ };
+
+ onUpdatedEmitter.on("theme-updated", callback);
+ return () => {
+ onUpdatedEmitter.off("theme-updated", callback);
+ };
+ }).api(),
},
};
}
};
--- a/toolkit/components/extensions/schemas/theme.json
+++ b/toolkit/components/extensions/schemas/theme.json
@@ -452,16 +452,48 @@
}
}
]
},
{
"namespace": "theme",
"description": "The theme API allows customizing of visual elements of the browser.",
"permissions": ["theme"],
+ "types": [
+ {
+ "id": "ThemeUpdateInfo",
+ "type": "object",
+ "description": "Info provided in the onUpdated listener.",
+ "properties": {
+ "theme": {
+ "type": "object",
+ "description": "The new theme after update"
+ },
+ "windowId": {
+ "type": "integer",
+ "description": "The id of the window the theme has been applied to",
+ "optional": true
+ }
+ }
+ }
+ ],
+ "events": [
+ {
+ "name": "onUpdated",
+ "type": "function",
+ "description": "Fired when a new theme has been applied",
+ "parameters": [
+ {
+ "$ref": "ThemeUpdateInfo",
+ "name": "updateInfo",
+ "description": "Details of the theme update"
+ }
+ ]
+ }
+ ],
"functions": [
{
"name": "getCurrent",
"type": "function",
"async": true,
"description": "Returns the current theme for the specified window or the last focused window.",
"parameters": [
{
--- a/toolkit/components/extensions/test/browser/browser.ini
+++ b/toolkit/components/extensions/test/browser/browser.ini
@@ -1,13 +1,15 @@
[DEFAULT]
support-files =
head.js
[browser_ext_management_themes.js]
[browser_ext_themes_chromeparity.js]
[browser_ext_themes_dynamic_getCurrent.js]
+[browser_ext_themes_dynamic_onUpdated.js]
[browser_ext_themes_dynamic_updates.js]
[browser_ext_themes_lwtsupport.js]
[browser_ext_themes_multiple_backgrounds.js]
[browser_ext_themes_persistence.js]
+[browser_ext_themes_static_onUpdated.js]
[browser_ext_themes_toolbar_fields.js]
[browser_ext_themes_toolbars.js]
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/browser/browser_ext_themes_dynamic_onUpdated.js
@@ -0,0 +1,116 @@
+"use strict";
+
+// This test checks whether browser.theme.onUpdated works correctly with different
+// types of dynamic theme updates.
+
+// PNG image data for a simple red dot.
+const BACKGROUND_1 = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==";
+// PNG image data for the Mozilla dino head.
+const BACKGROUND_2 = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==";
+
+add_task(async function test_on_updated() {
+ let extension = ExtensionTestUtils.loadExtension({
+ async background() {
+ const ACCENT_COLOR_1 = "#a14040";
+ const TEXT_COLOR_1 = "#fac96e";
+
+ const ACCENT_COLOR_2 = "#03fe03";
+ const TEXT_COLOR_2 = "#0ef325";
+
+ const theme1 = {
+ "images": {
+ "headerURL": "image1.png",
+ },
+ "colors": {
+ "accentcolor": ACCENT_COLOR_1,
+ "textcolor": TEXT_COLOR_1,
+ },
+ };
+
+ const theme2 = {
+ "images": {
+ "headerURL": "image2.png",
+ },
+ "colors": {
+ "accentcolor": ACCENT_COLOR_2,
+ "textcolor": TEXT_COLOR_2,
+ },
+ };
+
+ function testTheme1(returnedTheme) {
+ browser.test.assertTrue(returnedTheme.images.headerURL.includes("image1.png"),
+ "Theme 1 header URL should be applied");
+ browser.test.assertEq(ACCENT_COLOR_1, returnedTheme.colors.accentcolor,
+ "Theme 1 accent color should be applied");
+ browser.test.assertEq(TEXT_COLOR_1, returnedTheme.colors.textcolor,
+ "Theme 1 text color should be applied");
+ }
+
+ function testTheme2(returnedTheme) {
+ browser.test.assertTrue(returnedTheme.images.headerURL.includes("image2.png"),
+ "Theme 2 header URL should be applied");
+ browser.test.assertEq(ACCENT_COLOR_2, returnedTheme.colors.accentcolor,
+ "Theme 2 accent color should be applied");
+ browser.test.assertEq(TEXT_COLOR_2, returnedTheme.colors.textcolor,
+ "Theme 2 text color should be applied");
+ }
+
+ const firstWin = await browser.windows.getCurrent();
+ const secondWin = await browser.windows.create();
+
+ const onceThemeUpdated = () => new Promise(resolve => {
+ const listener = updateInfo => {
+ browser.theme.onUpdated.removeListener(listener);
+ resolve(updateInfo);
+ };
+ browser.theme.onUpdated.addListener(listener);
+ });
+
+ browser.test.log("Testing update with no windowId parameter");
+ let updateInfo1 = onceThemeUpdated();
+ await browser.theme.update(theme1);
+ updateInfo1 = await updateInfo1;
+ testTheme1(updateInfo1.theme);
+ browser.test.assertTrue(!updateInfo1.windowId, "No window id on first update");
+
+ browser.test.log("Testing update with windowId parameter");
+ let updateInfo2 = onceThemeUpdated();
+ await browser.theme.update(secondWin.id, theme2);
+ updateInfo2 = await updateInfo2;
+ testTheme2(updateInfo2.theme);
+ browser.test.assertEq(secondWin.id, updateInfo2.windowId,
+ "window id on second update");
+
+ browser.test.log("Testing reset with windowId parameter");
+ let updateInfo3 = onceThemeUpdated();
+ await browser.theme.reset(firstWin.id);
+ updateInfo3 = await updateInfo3;
+ browser.test.assertEq(0, Object.keys(updateInfo3.theme).length,
+ "Empty theme given on reset");
+ browser.test.assertEq(firstWin.id, updateInfo3.windowId,
+ "window id on third update");
+
+ browser.test.log("Testing reset with no windowId parameter");
+ let updateInfo4 = onceThemeUpdated();
+ await browser.theme.reset();
+ updateInfo4 = await updateInfo4;
+ browser.test.assertEq(0, Object.keys(updateInfo4.theme).length, "Empty theme given on reset");
+ browser.test.assertTrue(!updateInfo4.windowId, "no window id on fourth update");
+
+ browser.test.log("Cleaning up test");
+ await browser.windows.remove(secondWin.id);
+ browser.test.notifyPass("onUpdated");
+ },
+ manifest: {
+ permissions: ["theme"],
+ },
+ files: {
+ "image1.png": BACKGROUND_1,
+ "image2.png": BACKGROUND_2,
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("onUpdated");
+ await extension.unload();
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/browser/browser_ext_themes_static_onUpdated.js
@@ -0,0 +1,58 @@
+"use strict";
+
+// This test checks whether browser.theme.onUpdated works
+// when a static theme is applied
+
+add_task(async function test_on_updated() {
+ const theme = ExtensionTestUtils.loadExtension({
+ manifest: {
+ "theme": {
+ "images": {
+ "headerURL": "image1.png",
+ },
+ "colors": {
+ "accentcolor": ACCENT_COLOR,
+ "textcolor": TEXT_COLOR,
+ },
+ },
+ },
+ files: {
+ "image1.png": BACKGROUND,
+ },
+ });
+
+ const extension = ExtensionTestUtils.loadExtension({
+ background() {
+ browser.theme.onUpdated.addListener(updateInfo => {
+ browser.test.sendMessage("theme-updated", updateInfo);
+ });
+ },
+ manifest: {
+ permissions: ["theme"],
+ },
+ });
+
+ await extension.startup();
+
+ info("Testing update event on static theme startup");
+ let updatedPromise = extension.awaitMessage("theme-updated");
+ await theme.startup();
+ const {theme: receivedTheme, windowId} = await updatedPromise;
+ Assert.ok(!windowId, "No window id in static theme update event");
+ Assert.ok(receivedTheme.images.headerURL.includes("image1.png"),
+ "Theme header URL should be applied");
+ Assert.equal(receivedTheme.colors.accentcolor, ACCENT_COLOR,
+ "Theme accent color should be applied");
+ Assert.equal(receivedTheme.colors.textcolor, TEXT_COLOR,
+ "Theme text color should be applied");
+
+ info("Testing update event on static theme unload");
+ updatedPromise = extension.awaitMessage("theme-updated");
+ await theme.unload();
+ const updateInfo = await updatedPromise;
+ Assert.ok(!windowId, "No window id in static theme update event on unload");
+ Assert.equal(Object.keys(updateInfo.theme), 0,
+ "unloading theme sends empty theme in update event");
+
+ await extension.unload();
+});