Bug 1349944 - Add browser.theme.onUpdated to watch for theme updates. r=jaws, mixedpuppy draft
authorTim Nguyen <ntim.bugs@gmail.com>
Tue, 31 Oct 2017 01:13:19 +0000
changeset 689109 ecf71122dc5ecc93d99c921c523fa65bd22302d8
parent 689108 9355b268b1c379979b0e631f94f42468cdba80d2
child 689110 bdd698bb8d1cedb304397a67389b56aac4f5642c
child 689112 6a2599000b666ef89af3408457fc8b8b5234e0a0
push id86908
push userbmo:ntim.bugs@gmail.com
push dateTue, 31 Oct 2017 01:13:41 +0000
reviewersjaws, mixedpuppy
bugs1349944
milestone58.0a1
Bug 1349944 - Add browser.theme.onUpdated to watch for theme updates. r=jaws, mixedpuppy MozReview-Commit-ID: JX4gRa3Qt0p
toolkit/components/extensions/ext-theme.js
toolkit/components/extensions/schemas/theme.json
toolkit/components/extensions/test/browser/browser.ini
toolkit/components/extensions/test/browser/browser_ext_themes_dynamic_onUpdated.js
toolkit/components/extensions/test/browser/browser_ext_themes_static_onUpdated.js
--- 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();
+});