Bug 1347207 - Implement theme_experiment manifest field. r=jaws
authorTim Nguyen <ntim.bugs@gmail.com>
Mon, 23 Jul 2018 18:46:40 +0100
changeset 485890 20d0116ece9a1b88eceaf1876aabd2a162ca5496
parent 485889 e6005ad883098b389cfacaf2ac85d85ced0bbbc1
child 485891 1a03fc28195e1a29978a2cd25ca6e5900f2d59da
push id9719
push userffxbld-merge
push dateFri, 24 Aug 2018 17:49:46 +0000
treeherdermozilla-beta@719ec98fba77 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjaws
bugs1347207
milestone63.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 1347207 - Implement theme_experiment manifest field. r=jaws MozReview-Commit-ID: DuUiVAMcti2
toolkit/components/extensions/parent/ext-theme.js
toolkit/components/extensions/schemas/theme.json
toolkit/components/extensions/test/browser/browser.ini
toolkit/components/extensions/test/browser/browser_ext_themes_experiment.js
toolkit/modules/LightweightThemeConsumer.jsm
--- a/toolkit/components/extensions/parent/ext-theme.js
+++ b/toolkit/components/extensions/parent/ext-theme.js
@@ -1,11 +1,11 @@
 "use strict";
 
-/* global windowTracker, EventManager, EventEmitter */
+/* global windowTracker, EventManager, EventEmitter, AddonManager */
 
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 
 ChromeUtils.defineModuleGetter(this, "LightweightThemeManager",
                                "resource://gre/modules/LightweightThemeManager.jsm");
 
 var {
   getWinUtils,
@@ -31,25 +31,45 @@ let windowOverrides = new Map();
  */
 class Theme {
   /**
    * Creates a theme instance.
    *
    * @param {string} extension Extension that created the theme.
    * @param {Integer} windowId The windowId where the theme is applied.
    */
-  constructor({extension, details, windowId}) {
+  constructor({extension, details, windowId, experiment}) {
     this.extension = extension;
     this.details = details;
     this.windowId = windowId;
 
     this.lwtStyles = {
       icons: {},
     };
 
+    if (experiment) {
+      const canRunExperiment = AppConstants.MOZ_ALLOW_LEGACY_EXTENSIONS &&
+        Services.prefs.getBoolPref("extensions.legacy.enabled");
+      if (canRunExperiment) {
+        this.lwtStyles.experimental = {
+          colors: {},
+          images: {},
+          properties: {},
+        };
+        const {baseURI} = this.extension;
+        if (experiment.stylesheet) {
+          experiment.stylesheet = baseURI.resolve(experiment.stylesheet);
+        }
+        this.experiment = experiment;
+      } else {
+        const {logger} = this.extension;
+        logger.warn("This extension is not allowed to run theme experiments");
+        return;
+      }
+    }
     this.load();
   }
 
   /**
    * 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
@@ -83,16 +103,19 @@ class Theme {
         getWinUtils(windowTracker.getWindow(this.windowId)).outerWindowID;
       windowOverrides.set(this.windowId, this);
     } else {
       windowOverrides.clear();
       defaultTheme = this;
     }
     onUpdatedEmitter.emit("theme-updated", this.details, this.windowId);
 
+    if (this.experiment) {
+      lwtData.experiment = this.experiment;
+    }
     LightweightThemeManager.fallbackThemeData = this.lwtStyles;
     Services.obs.notifyObservers(null,
                                  "lightweight-theme-styling-update",
                                  JSON.stringify(lwtData));
   }
 
   /**
    * Helper method for loading colors found in the extension's manifest.
@@ -158,16 +181,24 @@ class Theme {
         case "popup_text":
         case "popup_border":
         case "popup_highlight":
         case "popup_highlight_text":
         case "ntp_background":
         case "ntp_text":
           this.lwtStyles[color] = cssColor;
           break;
+        default:
+          if (this.experiment && this.experiment.colors && color in this.experiment.colors) {
+            this.lwtStyles.experimental.colors[color] = cssColor;
+          } else {
+            const {logger} = this.extension;
+            logger.warn(`Unrecognized theme property found: colors.${color}`);
+          }
+          break;
       }
     }
   }
 
   /**
    * Helper method for loading images found in the extension's manifest.
    *
    * @param {Object} images Dictionary mapping image properties to values.
@@ -189,16 +220,25 @@ class Theme {
           break;
         }
         case "headerURL":
         case "theme_frame": {
           let resolvedURL = baseURI.resolve(val);
           this.lwtStyles.headerURL = resolvedURL;
           break;
         }
+        default: {
+          if (this.experiment && this.experiment.images && image in this.experiment.images) {
+            this.lwtStyles.experimental.images[image] = baseURI.resolve(val);
+          } else {
+            const {logger} = this.extension;
+            logger.warn(`Unrecognized theme property found: images.${image}`);
+          }
+          break;
+        }
       }
     }
   }
 
   /**
    * Helper method for loading icons found in the extension's manifest.
    *
    * @param {Object} icons Dictionary mapping icon properties to extension URLs.
@@ -280,16 +320,25 @@ class Theme {
             tiling.push("no-repeat");
           }
           for (let i = 0, l = this.lwtStyles.additionalBackgrounds.length; i < l; ++i) {
             tiling.push(val[i] || "no-repeat");
           }
           this.lwtStyles.backgroundsTiling = tiling.join(",");
           break;
         }
+        default: {
+          if (this.experiment && this.experiment.properties && property in this.experiment.properties) {
+            this.lwtStyles.experimental.properties[property] = val;
+          } else {
+            const {logger} = this.extension;
+            logger.warn(`Unrecognized theme property found: properties.${property}`);
+          }
+          break;
+        }
       }
     }
   }
 
   static unload(windowId) {
     let lwtData = {
       theme: null,
     };
@@ -309,20 +358,22 @@ class Theme {
                                  JSON.stringify(lwtData));
   }
 }
 
 this.theme = class extends ExtensionAPI {
   onManifestEntry(entryName) {
     let {extension} = this;
     let {manifest} = extension;
+    let {theme, theme_experiment} = manifest;
 
     defaultTheme = new Theme({
       extension,
-      details: manifest.theme,
+      details: theme,
+      experiment: theme_experiment,
     });
   }
 
   onShutdown(reason) {
     if (reason === "APP_SHUTDOWN") {
       return;
     }
 
@@ -361,16 +412,17 @@ this.theme = class extends ExtensionAPI 
               return Promise.reject(`Invalid window ID: ${windowId}`);
             }
           }
 
           new Theme({
             extension,
             details,
             windowId,
+            experiment: this.extension.manifest.theme_experiment,
           });
         },
         reset: (windowId) => {
           if (windowId) {
             const browserWindow = windowTracker.getWindow(windowId, context);
             if (!browserWindow) {
               return Promise.reject(`Invalid window ID: ${windowId}`);
             }
--- a/toolkit/components/extensions/schemas/theme.json
+++ b/toolkit/components/extensions/schemas/theme.json
@@ -37,16 +37,47 @@
             "maxItems": 4,
             "items": {
               "type": "number"
             }
           }
         ]
       },
       {
+        "id": "ThemeExperiment",
+        "type": "object",
+        "properties": {
+          "stylesheet": {
+            "optional": true,
+            "$ref": "ExtensionURL"
+          },
+          "images": {
+            "type": "object",
+            "optional": true,
+            "additionalProperties": {
+              "type": "string"
+            }
+          },
+          "colors": {
+            "type": "object",
+            "optional": true,
+            "additionalProperties": {
+              "type": "string"
+            }
+          },
+          "properties": {
+            "type": "object",
+            "optional": true,
+            "additionalProperties": {
+              "type": "string"
+            }
+          }
+        }
+      },
+      {
         "id": "ThemeType",
         "type": "object",
         "properties": {
           "images": {
             "type": "object",
             "optional": true,
             "properties": {
               "additional_backgrounds": {
@@ -59,17 +90,17 @@
                 "$ref": "ImageDataOrExtensionURL",
                 "optional": true
               },
               "theme_frame": {
                 "$ref": "ImageDataOrExtensionURL",
                 "optional": true
               }
             },
-            "additionalProperties": { "$ref": "UnrecognizedProperty" }
+            "additionalProperties": { "$ref": "ImageDataOrExtensionURL" }
           },
           "colors": {
             "type": "object",
             "optional": true,
             "properties": {
               "tab_selected": {
                 "$ref": "ThemeColor",
                 "optional": true
@@ -202,17 +233,17 @@
                 "$ref": "ThemeColor",
                 "optional": true
               },
               "ntp_text": {
                 "$ref": "ThemeColor",
                 "optional": true
               }
             },
-            "additionalProperties": { "$ref": "UnrecognizedProperty" }
+            "additionalProperties": { "$ref": "ThemeColor" }
           },
           "icons": {
             "type": "object",
             "optional": true,
             "properties": {
               "back": {
                 "$ref": "ExtensionURL",
                 "optional": true
@@ -559,42 +590,55 @@
                 "items": {
                   "type": "string",
                   "enum": ["no-repeat", "repeat", "repeat-x", "repeat-y"]
                 },
                 "maxItems": 15,
                 "optional": true
               }
             },
-            "additionalProperties": { "$ref": "UnrecognizedProperty" }
+            "additionalProperties": { "type": "string" }
           }
         },
         "additionalProperties": { "$ref": "UnrecognizedProperty" }
       },
       {
         "id": "ThemeManifest",
         "type": "object",
         "description": "Contents of manifest.json for a static theme",
         "$import": "manifest.ManifestBase",
         "properties": {
           "theme": {
             "$ref": "ThemeType"
           },
           "default_locale": {
             "type": "string",
-            "optional": "true"
+            "optional": true
+          },
+          "theme_experiment": {
+            "$ref": "ThemeExperiment",
+            "optional": true
           },
           "icons": {
             "type": "object",
             "optional": true,
             "patternProperties": {
               "^[1-9]\\d*$": { "type": "string" }
             }
           }
         }
+      },
+      {
+        "$extend": "WebExtensionManifest",
+        "properties": {
+          "theme_experiment": {
+            "$ref": "ThemeExperiment",
+            "optional": true
+          }
+        }
       }
     ]
   },
   {
     "namespace": "theme",
     "description": "The theme API allows customizing of visual elements of the browser.",
     "types": [
       {
--- a/toolkit/components/extensions/test/browser/browser.ini
+++ b/toolkit/components/extensions/test/browser/browser.ini
@@ -4,16 +4,17 @@ support-files =
 
 [browser_ext_management_themes.js]
 skip-if = verify
 [browser_ext_themes_alpha_accentcolor.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_experiment.js]
 [browser_ext_themes_getCurrent_differentExt.js]
 [browser_ext_themes_lwtsupport.js]
 [browser_ext_themes_multiple_backgrounds.js]
 [browser_ext_themes_ntp_colors.js]
 [browser_ext_themes_ntp_colors_perwindow.js]
 [browser_ext_themes_persistence.js]
 [browser_ext_themes_separators.js]
 [browser_ext_themes_static_onUpdated.js]
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/browser/browser_ext_themes_experiment.js
@@ -0,0 +1,258 @@
+"use strict";
+
+// This test checks whether the theme experiments work
+
+add_task(async function setup() {
+  await SpecialPowers.pushPrefEnv({
+    set: [["extensions.legacy.enabled", true]],
+  });
+});
+
+add_task(async function test_experiment_static_theme() {
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      theme: {
+        colors: {
+          some_color_property: "#ff00ff",
+        },
+        images: {
+          some_image_property: "background.jpg",
+        },
+        properties: {
+          some_random_property: "no-repeat",
+        },
+      },
+      theme_experiment: {
+        colors: {
+          some_color_property: "--some-color-property",
+        },
+        images: {
+          some_image_property: "--some-image-property",
+        },
+        properties: {
+          some_random_property: "--some-random-property",
+        },
+      },
+    },
+  });
+
+  const root = window.document.documentElement;
+
+  is(root.style.getPropertyValue("--some-color-property"), "",
+     "Color property should be unset");
+  is(root.style.getPropertyValue("--some-image-property"), "",
+     "Image property should be unset");
+  is(root.style.getPropertyValue("--some-random-property"), "",
+     "Generic Property should be unset.");
+
+  await extension.startup();
+
+  if (AppConstants.MOZ_ALLOW_LEGACY_EXTENSIONS) {
+    is(root.style.getPropertyValue("--some-color-property"), hexToCSS("#ff00ff"),
+       "Color property should be parsed and set.");
+    ok(root.style.getPropertyValue("--some-image-property").startsWith("url("),
+       "Image property should be parsed.");
+    ok(root.style.getPropertyValue("--some-image-property").endsWith("background.jpg)"),
+       "Image property should be set.");
+    is(root.style.getPropertyValue("--some-random-property"), "no-repeat",
+       "Generic Property should be set.");
+  } else {
+    is(root.style.getPropertyValue("--some-color-property"), "",
+       "Color property should be unset");
+    is(root.style.getPropertyValue("--some-image-property"), "",
+       "Image property should be unset");
+    is(root.style.getPropertyValue("--some-random-property"), "",
+       "Generic Property should be unset.");
+  }
+
+  await extension.unload();
+
+  is(root.style.getPropertyValue("--some-color-property"), "",
+     "Color property should be unset");
+  is(root.style.getPropertyValue("--some-image-property"), "",
+     "Image property should be unset");
+  is(root.style.getPropertyValue("--some-random-property"), "",
+     "Generic Property should be unset.");
+});
+
+add_task(async function test_experiment_dynamic_theme() {
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      permissions: ["theme"],
+      theme_experiment: {
+        colors: {
+          some_color_property: "--some-color-property",
+        },
+        images: {
+          some_image_property: "--some-image-property",
+        },
+        properties: {
+          some_random_property: "--some-random-property",
+        },
+      },
+    },
+    background() {
+      const theme = {
+        colors: {
+          some_color_property: "#ff00ff",
+        },
+        images: {
+          some_image_property: "background.jpg",
+        },
+        properties: {
+          some_random_property: "no-repeat",
+        },
+      };
+      browser.test.onMessage.addListener((msg) => {
+        if (msg === "update-theme") {
+          browser.theme.update(theme).then(() => {
+            browser.test.sendMessage("theme-updated");
+          });
+        } else {
+          browser.theme.reset().then(() => {
+            browser.test.sendMessage("theme-reset");
+          });
+        }
+      });
+    },
+  });
+
+  await extension.startup();
+
+  const root = window.document.documentElement;
+
+  is(root.style.getPropertyValue("--some-color-property"), "",
+     "Color property should be unset");
+  is(root.style.getPropertyValue("--some-image-property"), "",
+     "Image property should be unset");
+  is(root.style.getPropertyValue("--some-random-property"), "",
+     "Generic Property should be unset.");
+
+  extension.sendMessage("update-theme");
+  await extension.awaitMessage("theme-updated");
+
+  if (AppConstants.MOZ_ALLOW_LEGACY_EXTENSIONS) {
+    is(root.style.getPropertyValue("--some-color-property"), hexToCSS("#ff00ff"),
+       "Color property should be parsed and set.");
+    ok(root.style.getPropertyValue("--some-image-property").startsWith("url("),
+       "Image property should be parsed.");
+    ok(root.style.getPropertyValue("--some-image-property").endsWith("background.jpg)"),
+       "Image property should be set.");
+    is(root.style.getPropertyValue("--some-random-property"), "no-repeat",
+       "Generic Property should be set.");
+  } else {
+    is(root.style.getPropertyValue("--some-color-property"), "",
+       "Color property should be unset");
+    is(root.style.getPropertyValue("--some-image-property"), "",
+       "Image property should be unset");
+    is(root.style.getPropertyValue("--some-random-property"), "",
+       "Generic Property should be unset.");
+  }
+
+  extension.sendMessage("reset-theme");
+  await extension.awaitMessage("theme-reset");
+
+  is(root.style.getPropertyValue("--some-color-property"), "",
+     "Color property should be unset");
+  is(root.style.getPropertyValue("--some-image-property"), "",
+     "Image property should be unset");
+  is(root.style.getPropertyValue("--some-random-property"), "",
+     "Generic Property should be unset.");
+
+  extension.sendMessage("update-theme");
+  await extension.awaitMessage("theme-updated");
+
+  if (AppConstants.MOZ_ALLOW_LEGACY_EXTENSIONS) {
+    is(root.style.getPropertyValue("--some-color-property"), hexToCSS("#ff00ff"),
+       "Color property should be parsed and set.");
+    ok(root.style.getPropertyValue("--some-image-property").startsWith("url("),
+       "Image property should be parsed.");
+    ok(root.style.getPropertyValue("--some-image-property").endsWith("background.jpg)"),
+       "Image property should be set.");
+    is(root.style.getPropertyValue("--some-random-property"), "no-repeat",
+       "Generic Property should be set.");
+  } else {
+    is(root.style.getPropertyValue("--some-color-property"), "",
+       "Color property should be unset");
+    is(root.style.getPropertyValue("--some-image-property"), "",
+       "Image property should be unset");
+    is(root.style.getPropertyValue("--some-random-property"), "",
+       "Generic Property should be unset.");
+  }
+
+  await extension.unload();
+
+  is(root.style.getPropertyValue("--some-color-property"), "",
+     "Color property should be unset");
+  is(root.style.getPropertyValue("--some-image-property"), "",
+     "Image property should be unset");
+  is(root.style.getPropertyValue("--some-random-property"), "",
+     "Generic Property should be unset.");
+});
+
+add_task(async function test_experiment_stylesheet() {
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      theme: {
+        colors: {
+          menu_button_background: "#ff00ff",
+        },
+      },
+      theme_experiment: {
+        stylesheet: "experiment.css",
+        colors: {
+          menu_button_background: "--menu-button-background",
+        },
+      },
+    },
+    files: {
+      "experiment.css": `#PanelUI-menu-button {
+        background-color: var(--menu-button-background);
+        fill: white;
+      }`,
+    },
+  });
+
+  const root = window.document.documentElement;
+  const menuButton = document.getElementById("PanelUI-menu-button");
+  const computedStyle = window.getComputedStyle(menuButton);
+  const expectedColor = hexToCSS("#ff00ff");
+  const expectedFill = hexToCSS("#ffffff");
+
+  is(root.style.getPropertyValue("--menu-button-background"), "",
+     "Variable should be unset");
+  isnot(computedStyle.backgroundColor, expectedColor,
+        "Menu button should not have custom background");
+  isnot(computedStyle.fill, expectedFill,
+        "Menu button should not have stylesheet fill");
+
+  await extension.startup();
+
+  if (AppConstants.MOZ_ALLOW_LEGACY_EXTENSIONS) {
+    // Wait for stylesheet load.
+    await BrowserTestUtils.waitForCondition(() => computedStyle.fill === expectedFill);
+
+    is(root.style.getPropertyValue("--menu-button-background"), expectedColor,
+       "Variable should be parsed and set.");
+    is(computedStyle.backgroundColor, expectedColor,
+       "Menu button should be have correct background");
+    is(computedStyle.fill, expectedFill,
+       "Menu button should be have correct fill");
+  } else {
+    is(root.style.getPropertyValue("--menu-button-background"), "",
+       "Variable should be unset");
+    isnot(computedStyle.backgroundColor, expectedColor,
+          "Menu button should not have custom background");
+    isnot(computedStyle.fill, expectedFill,
+          "Menu button should not have stylesheet fill");
+  }
+
+  await extension.unload();
+
+  is(root.style.getPropertyValue("--menu-button-background"), "",
+     "Variable should be unset");
+  isnot(computedStyle.backgroundColor, expectedColor,
+        "Menu button should not have custom background");
+  isnot(computedStyle.fill, expectedFill,
+        "Menu button should not have stylesheet fill");
+});
--- a/toolkit/modules/LightweightThemeConsumer.jsm
+++ b/toolkit/modules/LightweightThemeConsumer.jsm
@@ -132,24 +132,24 @@ LightweightThemeConsumer.prototype = {
   _active: false,
 
   observe(aSubject, aTopic, aData) {
     if (aTopic != "lightweight-theme-styling-update")
       return;
 
     let parsedData = JSON.parse(aData);
     if (!parsedData) {
-      parsedData = { theme: null };
+      parsedData = { theme: null, experiment: null };
     }
 
     if (parsedData.window && parsedData.window !== this._winId) {
       return;
     }
 
-    this._update(parsedData.theme);
+    this._update(parsedData.theme, parsedData.experiment);
   },
 
   handleEvent(aEvent) {
     switch (aEvent.type) {
       case "resolutionchange":
         if (this._active) {
           this._update(this._lastData);
         }
@@ -158,67 +158,120 @@ LightweightThemeConsumer.prototype = {
         Services.obs.removeObserver(this, "lightweight-theme-styling-update");
         Services.ppmm.sharedData.delete(`theme/${this._winId}`);
         this._win.removeEventListener("resolutionchange", this);
         this._win = this._doc = null;
         break;
     }
   },
 
-  _update(aData) {
-    this._lastData = aData;
-    if (aData) {
-      aData = LightweightThemeImageOptimizer.optimize(aData, this._win.screen);
+  _update(theme, experiment) {
+    this._lastData = theme;
+    if (theme) {
+      theme = LightweightThemeImageOptimizer.optimize(theme, this._win.screen);
     }
 
-    let active = this._active = aData && aData.id !== DEFAULT_THEME_ID;
+    let active = this._active = theme && theme.id !== DEFAULT_THEME_ID;
 
-    if (!aData) {
-      aData = {};
+    if (!theme) {
+      theme = {};
     }
 
     let root = this._doc.documentElement;
 
-    if (active && aData.headerURL) {
+    if (active && theme.headerURL) {
       root.setAttribute("lwtheme-image", "true");
     } else {
       root.removeAttribute("lwtheme-image");
     }
 
-    if (active && aData.icons) {
-      let activeIcons = Object.keys(aData.icons).join(" ");
+    if (active && theme.icons) {
+      let activeIcons = Object.keys(theme.icons).join(" ");
       root.setAttribute("lwthemeicons", activeIcons);
     } else {
       root.removeAttribute("lwthemeicons");
     }
 
     for (let icon of ICONS) {
-      let value = aData.icons ? aData.icons[`--${icon}-icon`] : null;
+      let value = theme.icons ? theme.icons[`--${icon}-icon`] : null;
       _setImage(root, active, `--${icon}-icon`, value);
     }
 
-    _setImage(root, active, "--lwt-header-image", aData.headerURL);
-    _setImage(root, active, "--lwt-footer-image", aData.footerURL);
-    _setImage(root, active, "--lwt-additional-images", aData.additionalBackgrounds);
-    _setProperties(root, active, aData);
+    this._setExperiment(active, experiment, theme.experimental);
+    _setImage(root, active, "--lwt-header-image", theme.headerURL);
+    _setImage(root, active, "--lwt-footer-image", theme.footerURL);
+    _setImage(root, active, "--lwt-additional-images", theme.additionalBackgrounds);
+    _setProperties(root, active, theme);
 
     if (active) {
       root.setAttribute("lwtheme", "true");
     } else {
       root.removeAttribute("lwtheme");
       root.removeAttribute("lwthemetextcolor");
     }
 
-    if (active && aData.footerURL)
+    if (active && theme.footerURL)
       root.setAttribute("lwthemefooter", "true");
     else
       root.removeAttribute("lwthemefooter");
 
-    let contentThemeData = _getContentProperties(this._doc, active, aData);
+    let contentThemeData = _getContentProperties(this._doc, active, theme);
     Services.ppmm.sharedData.set(`theme/${this._winId}`, contentThemeData);
+  },
+
+  _setExperiment(active, experiment, properties) {
+    const root = this._doc.documentElement;
+    if (this._lastExperimentData) {
+      const { stylesheet, usedVariables } = this._lastExperimentData;
+      if (stylesheet) {
+        stylesheet.remove();
+      }
+      if (usedVariables) {
+        for (const variable of usedVariables) {
+          _setProperty(root, false, variable);
+        }
+      }
+    }
+    if (active && experiment) {
+      this._lastExperimentData = {};
+      if (experiment.stylesheet) {
+        /* Stylesheet URLs are validated using WebExtension schemas */
+        let stylesheetAttr = `href="${experiment.stylesheet}" type="text/css"`;
+        let stylesheet = this._doc.createProcessingInstruction("xml-stylesheet",
+          stylesheetAttr);
+        this._doc.insertBefore(stylesheet, root);
+        this._lastExperimentData.stylesheet = stylesheet;
+      }
+      let usedVariables = [];
+      if (properties.colors) {
+        for (const property in properties.colors) {
+          const cssVariable = experiment.colors[property];
+          const value = _sanitizeCSSColor(root.ownerDocument, properties.colors[property]);
+          _setProperty(root, active, cssVariable, value);
+          usedVariables.push(cssVariable);
+        }
+      }
+      if (properties.images) {
+        for (const property in properties.images) {
+          const cssVariable = experiment.images[property];
+          _setProperty(root, active, cssVariable, `url(${properties.images[property]})`);
+          usedVariables.push(cssVariable);
+        }
+      }
+      if (properties.properties) {
+        for (const property in properties.properties) {
+          const cssVariable = experiment.properties[property];
+          _setProperty(root, active, cssVariable, properties.properties[property]);
+          usedVariables.push(cssVariable);
+        }
+      }
+      this._lastExperimentData.usedVariables = usedVariables;
+    } else {
+      this._lastExperimentData = null;
+    }
   }
 };
 
 function _getContentProperties(doc, active, data) {
   if (!active) {
     return {};
   }
   let properties = {};