Bug 1347207 - Implement theme_experiment manifest field. r=jaws

MozReview-Commit-ID: DuUiVAMcti2

--HG--
extra : rebase_source : 62ffd3057ff5fbdde32f52277eec48296007a426
This commit is contained in:
Tim Nguyen 2018-07-23 18:46:40 +01:00
Родитель 004ecb1625
Коммит 96c6b8f5ac
5 изменённых файлов: 423 добавлений и 23 удалений

Просмотреть файл

@ -1,6 +1,6 @@
"use strict";
/* global windowTracker, EventManager, EventEmitter */
/* global windowTracker, EventManager, EventEmitter, AddonManager */
ChromeUtils.import("resource://gre/modules/Services.jsm");
@ -36,7 +36,7 @@ class Theme {
* @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;
@ -45,6 +45,26 @@ class Theme {
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();
}
@ -88,6 +108,9 @@ class Theme {
}
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",
@ -163,6 +186,11 @@ class Theme {
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;
}
break;
}
}
}
@ -194,6 +222,12 @@ class Theme {
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);
}
break;
}
}
}
}
@ -285,6 +319,12 @@ class Theme {
this.lwtStyles.backgroundsTiling = tiling.join(",");
break;
}
default: {
if (this.experiment && this.experiment.properties && property in this.experiment.properties) {
this.lwtStyles.experimental.properties[property] = val;
}
break;
}
}
}
}
@ -314,10 +354,12 @@ 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,
});
}
@ -366,6 +408,7 @@ this.theme = class extends ExtensionAPI {
extension,
details,
windowId,
experiment: this.extension.manifest.theme_experiment,
});
},
reset: (windowId) => {

Просмотреть файл

@ -41,6 +41,37 @@
}
]
},
{
"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",
@ -580,7 +611,11 @@
},
"default_locale": {
"type": "string",
"optional": "true"
"optional": true
},
"theme_experiment": {
"$ref": "ThemeExperiment",
"optional": true
},
"icons": {
"type": "object",
@ -590,6 +625,15 @@
}
}
}
},
{
"$extend": "WebExtensionManifest",
"properties": {
"theme_experiment": {
"$ref": "ThemeExperiment",
"optional": true
}
}
}
]
},

Просмотреть файл

@ -9,6 +9,7 @@ skip-if = verify
[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]

Просмотреть файл

@ -0,0 +1,259 @@
"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");
});

Просмотреть файл

@ -137,14 +137,14 @@ LightweightThemeConsumer.prototype = {
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) {
@ -163,42 +163,43 @@ LightweightThemeConsumer.prototype = {
}
},
_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");
@ -207,13 +208,65 @@ LightweightThemeConsumer.prototype = {
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;
}
}
};