Bug 1349944 - Add browser.theme.getCurrent() to query the selected theme. r=jaws, mixedpuppy draft
authorTim Nguyen <ntim.bugs@gmail.com>
Mon, 30 Oct 2017 18:51:26 +0000
changeset 688812 10bd1dd88e725e0e8c6d7108fc9b1f88c29f4385
parent 688183 c0eb1f08953b31362483a415465d2964a67a5f0c
child 688813 d4f8001369edab17c2dd81e97027c0188b9c3e52
child 688814 adc0e7ce43347fbf49185c15be12d01f719bfed9
push id86860
push userbmo:ntim.bugs@gmail.com
push dateMon, 30 Oct 2017 18:52:24 +0000
reviewersjaws, mixedpuppy
bugs1349944
milestone58.0a1
Bug 1349944 - Add browser.theme.getCurrent() to query the selected theme. r=jaws, mixedpuppy MozReview-Commit-ID: HzZjiyDDA6f
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();
+});