Bug 1349944 - Add browser.theme.onUpdated to watch for theme updates. r=jaws,mixedpuppy
authorTim Nguyen <ntim.bugs@gmail.com>
Tue, 31 Oct 2017 01:13:19 +0000
changeset 389219 6eec7e78c673f240e57b8c28bd8e2df99b54333f
parent 389218 bf4fd832f591878b0c8838179f6f62e34fd573e3
child 389220 35b09d9ef35f9d504a5ed18ae84c1b8fdf8d6544
push id32779
push userebalazs@mozilla.com
push dateTue, 31 Oct 2017 10:45:04 +0000
treeherdermozilla-central@a16cc603d061 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjaws, mixedpuppy
bugs1349944
milestone58.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 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();
+});