Bug 1349944 - Add browser.theme.getCurrent() to query the selected theme. r=jaws,mixedpuppy
authorTim Nguyen <ntim.bugs@gmail.com>
Tue, 31 Oct 2017 01:03:15 +0000
changeset 389218 bf4fd832f591878b0c8838179f6f62e34fd573e3
parent 389217 fa94f7205173d34f23975c6af9cb95237b28c8b8
child 389219 6eec7e78c673f240e57b8c28bd8e2df99b54333f
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.getCurrent() to query the selected theme. r=jaws,mixedpuppy MozReview-Commit-ID: Hzdm21riVlb
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_getCurrent.js
--- a/toolkit/components/extensions/ext-theme.js
+++ b/toolkit/components/extensions/ext-theme.js
@@ -23,34 +23,53 @@ class Theme {
    * Creates a theme instance.
    *
    * @param {string} baseURI The base URI of the extension, used to
    *   resolve relative filepaths.
    * @param {Object} logger  Reference to the (console) logger that will be used
    *   to show manifest warnings to the theme author.
    */
   constructor(baseURI, logger) {
-    // A dictionary of light weight theme styles.
-    this.lwtStyles = {
-      icons: {},
-    };
+    // The base theme applied to all windows.
+    this.baseProperties = {};
+
+    // Window-specific theme overrides.
+    this.windowOverrides = new WeakMap();
+
     this.baseURI = baseURI;
     this.logger = logger;
   }
 
   /**
+   * Gets the current theme for a specified window
+   *
+   * @param {Object} window
+   * @returns {Object} The theme of the specified window
+   */
+  getWindowTheme(window) {
+    if (this.windowOverrides.has(window)) {
+      return this.windowOverrides.get(window);
+    }
+    return this.baseProperties;
+  }
+
+  /**
    * Loads a theme by reading the properties from the extension's manifest.
    * This method will override any currently applied theme.
    *
    * @param {Object} details Theme part of the manifest. Supported
    *   properties can be found in the schema under ThemeType.
    * @param {Object} targetWindow The window to apply the theme to. Omitting
    *   this parameter will apply the theme globally.
    */
   load(details, targetWindow) {
+    this.lwtStyles = {
+      icons: {},
+    };
+
     if (targetWindow) {
       this.lwtStyles.window = getWinUtils(targetWindow).outerWindowID;
     }
 
     if (details.colors) {
       this.loadColors(details.colors);
     }
 
@@ -65,16 +84,21 @@ class Theme {
     if (details.properties) {
       this.loadProperties(details.properties);
     }
 
     // Lightweight themes require all properties to be defined.
     if (this.lwtStyles.headerURL &&
         this.lwtStyles.accentcolor &&
         this.lwtStyles.textcolor) {
+      if (!targetWindow) {
+        this.baseProperties = details;
+      } else {
+        this.windowOverrides.set(targetWindow, details);
+      }
       LightweightThemeManager.fallbackThemeData = this.lwtStyles;
       Services.obs.notifyObservers(null,
         "lightweight-theme-styling-update",
         JSON.stringify(this.lwtStyles));
     } else {
       this.logger.warn("Your theme doesn't include one of the following required " +
         "properties: 'headerURL', 'accentcolor' or 'textcolor'");
     }
@@ -240,37 +264,41 @@ class Theme {
     }
   }
 
   /**
    * Unloads the currently applied theme.
    * @param {Object} targetWindow The window the theme should be unloaded from
    */
   unload(targetWindow) {
-    let lwtStyles = {
+    this.lwtStyles = {
       headerURL: "",
       accentcolor: "",
       additionalBackgrounds: "",
       backgroundsAlignment: "",
       backgroundsTiling: "",
       textcolor: "",
       icons: {},
     };
 
     if (targetWindow) {
-      lwtStyles.window = getWinUtils(targetWindow).outerWindowID;
+      this.lwtStyles.window = getWinUtils(targetWindow).outerWindowID;
+      this.windowOverrides.set(targetWindow, {});
+    } else {
+      this.windowOverrides = new WeakMap();
+      this.baseProperties = {};
     }
 
     for (let icon of ICONS) {
-      lwtStyles.icons[`--${icon}--icon`] = "";
+      this.lwtStyles.icons[`--${icon}--icon`] = "";
     }
     LightweightThemeManager.fallbackThemeData = null;
     Services.obs.notifyObservers(null,
       "lightweight-theme-styling-update",
-      JSON.stringify(lwtStyles));
+      JSON.stringify(this.lwtStyles));
   }
 }
 
 this.theme = class extends ExtensionAPI {
   onManifestEntry(entryName) {
     if (!gThemesEnabled) {
       // Return early if themes are disabled.
       return;
@@ -294,16 +322,33 @@ this.theme = class extends ExtensionAPI 
     }
   }
 
   getAPI(context) {
     let {extension} = context;
 
     return {
       theme: {
+        getCurrent: (windowId) => {
+          // Return empty theme if none is applied.
+          if (!this.theme) {
+            return Promise.resolve({});
+          }
+
+          // Return theme applied on last focused window when no ID is supplied.
+          if (!windowId) {
+            return Promise.resolve(this.theme.getWindowTheme(windowTracker.topWindow));
+          }
+
+          const browserWindow = windowTracker.getWindow(windowId, context);
+          if (!browserWindow) {
+            return Promise.reject(`Invalid window ID: ${windowId}`);
+          }
+          return Promise.resolve(this.theme.getWindowTheme(browserWindow));
+        },
         update: (windowId, details) => {
           if (!gThemesEnabled) {
             // Return early if themes are disabled.
             return;
           }
 
           if (!this.theme) {
             // WebExtensions using the Theme API will not have a theme defined
--- a/toolkit/components/extensions/schemas/theme.json
+++ b/toolkit/components/extensions/schemas/theme.json
@@ -454,20 +454,34 @@
     ]
   },
   {
     "namespace": "theme",
     "description": "The theme API allows customizing of visual elements of the browser.",
     "permissions": ["theme"],
     "functions": [
       {
+        "name": "getCurrent",
+        "type": "function",
+        "async": true,
+        "description": "Returns the current theme for the specified window or the last focused window.",
+        "parameters": [
+          {
+            "type": "integer",
+            "name": "windowId",
+            "optional": true,
+            "description": "The window for which we want the theme."
+          }
+        ]
+      },
+      {
         "name": "update",
         "type": "function",
         "async": true,
-        "description": "Make complete or partial updates to the theme. Resolves when the update has completed.",
+        "description": "Make complete updates to the theme. Resolves when the update has completed.",
         "parameters": [
           {
             "type": "integer",
             "name": "windowId",
             "optional": true,
             "description": "The id of the window to update. No id updates all windows."
           },
           {
--- a/toolkit/components/extensions/test/browser/browser.ini
+++ b/toolkit/components/extensions/test/browser/browser.ini
@@ -1,12 +1,13 @@
 [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_updates.js]
 [browser_ext_themes_lwtsupport.js]
 [browser_ext_themes_multiple_backgrounds.js]
 [browser_ext_themes_persistence.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_getCurrent.js
@@ -0,0 +1,140 @@
+"use strict";
+
+// This test checks whether browser.theme.getCurrent() works correctly in different
+// configurations and with different parameter.
+
+// 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_get_current() {
+  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");
+      }
+
+      function testEmptyTheme(returnedTheme) {
+        browser.test.assertEq(0, Object.keys(returnedTheme).length, JSON.stringify(returnedTheme, null, 2));
+      }
+
+      browser.test.log("Testing getCurrent() with initial unthemed window");
+      const firstWin = await browser.windows.getCurrent();
+      testEmptyTheme(await browser.theme.getCurrent());
+      testEmptyTheme(await browser.theme.getCurrent(firstWin.id));
+
+      browser.test.log("Testing getCurrent() with after theme.update()");
+      await browser.theme.update(theme1);
+      testTheme1(await browser.theme.getCurrent());
+      testTheme1(await browser.theme.getCurrent(firstWin.id));
+
+      browser.test.log("Testing getCurrent() with after theme.update(windowId)");
+      const secondWin = await browser.windows.create();
+      await browser.theme.update(secondWin.id, theme2);
+      testTheme2(await browser.theme.getCurrent());
+      testTheme1(await browser.theme.getCurrent(firstWin.id));
+      testTheme2(await browser.theme.getCurrent(secondWin.id));
+
+      browser.test.log("Testing getCurrent() after window focus change");
+      await browser.windows.update(firstWin.id, {focused: true});
+      testTheme1(await browser.theme.getCurrent());
+      testTheme1(await browser.theme.getCurrent(firstWin.id));
+      testTheme2(await browser.theme.getCurrent(secondWin.id));
+
+      browser.test.log("Testing getCurrent() after another window focus change");
+      await browser.windows.update(secondWin.id, {focused: true});
+      testTheme2(await browser.theme.getCurrent());
+      testTheme1(await browser.theme.getCurrent(firstWin.id));
+      testTheme2(await browser.theme.getCurrent(secondWin.id));
+
+      browser.test.log("Testing getCurrent() after theme.reset(windowId)");
+      await browser.theme.reset(firstWin.id);
+      testTheme2(await browser.theme.getCurrent());
+      testEmptyTheme(await browser.theme.getCurrent(firstWin.id));
+      testTheme2(await browser.theme.getCurrent(secondWin.id));
+
+      browser.test.log("Testing getCurrent() after reset and window focus change");
+      await browser.windows.update(firstWin.id, {focused: true});
+      testEmptyTheme(await browser.theme.getCurrent());
+      testEmptyTheme(await browser.theme.getCurrent(firstWin.id));
+      testTheme2(await browser.theme.getCurrent(secondWin.id));
+
+      browser.test.log("Testing getCurrent() after theme.update(windowId)");
+      await browser.theme.update(firstWin.id, theme1);
+      testTheme1(await browser.theme.getCurrent());
+      testTheme1(await browser.theme.getCurrent(firstWin.id));
+      testTheme2(await browser.theme.getCurrent(secondWin.id));
+
+      browser.test.log("Testing getCurrent() after theme.reset()");
+      await browser.theme.reset();
+      testEmptyTheme(await browser.theme.getCurrent());
+      testEmptyTheme(await browser.theme.getCurrent(firstWin.id));
+      testEmptyTheme(await browser.theme.getCurrent(secondWin.id));
+
+      browser.test.log("Testing getCurrent() after closing a window");
+      await browser.windows.remove(secondWin.id);
+      testEmptyTheme(await browser.theme.getCurrent());
+      testEmptyTheme(await browser.theme.getCurrent(firstWin.id));
+
+      browser.test.log("Testing getCurrent() with invalid window ID");
+      await browser.test.assertRejects(
+        browser.theme.getCurrent(secondWin.id),
+        /Invalid window/,
+        "Invalid window should throw",
+      );
+      browser.test.notifyPass("get_current");
+    },
+    manifest: {
+      permissions: ["theme"],
+    },
+    files: {
+      "image1.png": BACKGROUND_1,
+      "image2.png": BACKGROUND_2,
+    },
+  });
+
+  await extension.startup();
+  await extension.awaitFinish("get_current");
+  await extension.unload();
+});