mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-08 19:04:45 +00:00
Backed out changeset 110829513605 (bug 1672202) for causing bc failures in browser_autocomplete_import.js
CLOSED TREE
This commit is contained in:
parent
d3beb62c1d
commit
63453c1404
@ -13,7 +13,9 @@ const { XPCOMUtils } = ChromeUtils.import(
|
||||
const { AppConstants } = ChromeUtils.import(
|
||||
"resource://gre/modules/AppConstants.jsm"
|
||||
);
|
||||
|
||||
const { ExperimentAPI } = ChromeUtils.import(
|
||||
"resource://messaging-system/experiments/ExperimentAPI.jsm"
|
||||
);
|
||||
const { PrivateBrowsingUtils } = ChromeUtils.import(
|
||||
"resource://gre/modules/PrivateBrowsingUtils.jsm"
|
||||
);
|
||||
@ -25,12 +27,12 @@ XPCOMUtils.defineLazyPreferenceGetter(
|
||||
false
|
||||
);
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "awExperimentFeature", () => {
|
||||
const { ExperimentFeature } = ChromeUtils.import(
|
||||
"resource://messaging-system/experiments/ExperimentAPI.jsm"
|
||||
);
|
||||
return new ExperimentFeature("aboutwelcome");
|
||||
});
|
||||
XPCOMUtils.defineLazyPreferenceGetter(
|
||||
this,
|
||||
"isAboutWelcomePrefEnabled",
|
||||
"browser.aboutwelcome.enabled",
|
||||
false
|
||||
);
|
||||
|
||||
class AboutNewTabChild extends JSWindowActorChild {
|
||||
handleEvent(event) {
|
||||
@ -38,7 +40,9 @@ class AboutNewTabChild extends JSWindowActorChild {
|
||||
// If the separate about:welcome page is enabled, we can skip all of this,
|
||||
// since that mode doesn't load any of the Activity Stream bits.
|
||||
if (
|
||||
awExperimentFeature.isEnabled({ defaultValue: true }) &&
|
||||
isAboutWelcomePrefEnabled &&
|
||||
// about:welcome should be enabled by default if no experiment exists.
|
||||
ExperimentAPI.isFeatureEnabled("aboutwelcome", true) &&
|
||||
this.contentWindow.location.pathname.includes("welcome")
|
||||
) {
|
||||
return;
|
||||
|
@ -1369,9 +1369,6 @@ pref("prompts.tabChromePromptSubDialog", true);
|
||||
// Activates preloading of the new tab url.
|
||||
pref("browser.newtab.preload", true);
|
||||
|
||||
// Experiment Prefs for Nimbus
|
||||
pref("browser.newtab.experiments.value", "{\"prefsButtonIcon\": \"icon-settings\"}");
|
||||
|
||||
// Preference to enable the entire new newtab experience at once.
|
||||
pref("browser.newtabpage.activity-stream.newNewtabExperience.enabled", false);
|
||||
|
||||
|
@ -41,12 +41,9 @@ const { E10SUtils } = ChromeUtils.import(
|
||||
"resource://gre/modules/E10SUtils.jsm"
|
||||
);
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "awExperimentFeature", () => {
|
||||
const { ExperimentFeature } = ChromeUtils.import(
|
||||
"resource://messaging-system/experiments/ExperimentAPI.jsm"
|
||||
);
|
||||
return new ExperimentFeature("aboutwelcome");
|
||||
});
|
||||
const { ExperimentAPI } = ChromeUtils.import(
|
||||
"resource://messaging-system/experiments/ExperimentAPI.jsm"
|
||||
);
|
||||
|
||||
/**
|
||||
* BEWARE: Do not add variables for holding state in the global scope.
|
||||
@ -61,6 +58,7 @@ const PREF_ABOUT_HOME_CACHE_ENABLED =
|
||||
"browser.startup.homepage.abouthome_cache.enabled";
|
||||
const PREF_ABOUT_HOME_CACHE_TESTING =
|
||||
"browser.startup.homepage.abouthome_cache.testing";
|
||||
const PREF_ABOUT_WELCOME_ENABLED = "browser.aboutwelcome.enabled";
|
||||
const ABOUT_WELCOME_URL =
|
||||
"resource://activity-stream/aboutwelcome/aboutwelcome.html";
|
||||
|
||||
@ -404,6 +402,13 @@ class BaseAboutNewTabService {
|
||||
this.activityStreamDebug = false;
|
||||
}
|
||||
|
||||
XPCOMUtils.defineLazyPreferenceGetter(
|
||||
this,
|
||||
"isAboutWelcomePrefEnabled",
|
||||
PREF_ABOUT_WELCOME_ENABLED,
|
||||
false
|
||||
);
|
||||
|
||||
XPCOMUtils.defineLazyPreferenceGetter(
|
||||
this,
|
||||
"privilegedAboutProcessEnabled",
|
||||
@ -448,7 +453,11 @@ class BaseAboutNewTabService {
|
||||
* This is calculated in the same way the default URL is.
|
||||
*/
|
||||
|
||||
if (awExperimentFeature.isEnabled({ defaultValue: true })) {
|
||||
if (
|
||||
this.isAboutWelcomePrefEnabled &&
|
||||
// about:welcome should be enabled by default if no experiment exists.
|
||||
ExperimentAPI.isFeatureEnabled("aboutwelcome", true)
|
||||
) {
|
||||
return ABOUT_WELCOME_URL;
|
||||
}
|
||||
return this.defaultURL;
|
||||
|
@ -6,9 +6,6 @@
|
||||
const { actionCreators: ac, actionTypes: at } = ChromeUtils.import(
|
||||
"resource://activity-stream/common/Actions.jsm"
|
||||
);
|
||||
const { XPCOMUtils } = ChromeUtils.import(
|
||||
"resource://gre/modules/XPCOMUtils.jsm"
|
||||
);
|
||||
const { Prefs } = ChromeUtils.import(
|
||||
"resource://activity-stream/lib/ActivityStreamPrefs.jsm"
|
||||
);
|
||||
@ -26,12 +23,11 @@ ChromeUtils.defineModuleGetter(
|
||||
"resource://gre/modules/AppConstants.jsm"
|
||||
);
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "aboutNewTabFeature", () => {
|
||||
const { ExperimentFeature } = ChromeUtils.import(
|
||||
"resource://messaging-system/experiments/ExperimentAPI.jsm"
|
||||
);
|
||||
return new ExperimentFeature("newtab");
|
||||
});
|
||||
ChromeUtils.defineModuleGetter(
|
||||
this,
|
||||
"ExperimentAPI",
|
||||
"resource://messaging-system/experiments/ExperimentAPI.jsm"
|
||||
);
|
||||
|
||||
ChromeUtils.defineModuleGetter(
|
||||
this,
|
||||
@ -79,20 +75,49 @@ this.PrefsFeed = class PrefsFeed {
|
||||
this._prefMap.set(key, { value });
|
||||
}
|
||||
|
||||
/**
|
||||
* Combine default values with experiment values for
|
||||
* the feature config.
|
||||
* */
|
||||
getFeatureConfigFromExperimentData(experimentData) {
|
||||
return {
|
||||
// Icon that shows up in the corner to link to preferences
|
||||
prefsButtonIcon: "icon-settings",
|
||||
|
||||
// Override defaults with any experiment values, if any exist.
|
||||
...(experimentData?.branch?.feature?.value || {}),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for initializing experiment and feature config data in .init()
|
||||
* */
|
||||
addExperimentDataToValues(values) {
|
||||
let experimentData = ExperimentAPI.getExperiment({
|
||||
featureId: "newtab",
|
||||
});
|
||||
values.experimentData = experimentData;
|
||||
values.featureConfig = this.getFeatureConfigFromExperimentData(
|
||||
experimentData
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for when experiment data updates.
|
||||
*/
|
||||
onExperimentUpdated(event, reason) {
|
||||
const value =
|
||||
aboutNewTabFeature.getValue({
|
||||
sendExposurePing: false,
|
||||
}) || {};
|
||||
onExperimentUpdated(event, experimentData) {
|
||||
this.store.dispatch(
|
||||
ac.BroadcastToContent({
|
||||
type: at.PREF_CHANGED,
|
||||
data: { name: "experimentData", value: experimentData },
|
||||
})
|
||||
);
|
||||
this.store.dispatch(
|
||||
ac.BroadcastToContent({
|
||||
type: at.PREF_CHANGED,
|
||||
data: {
|
||||
name: "featureConfig",
|
||||
value,
|
||||
value: this.getFeatureConfigFromExperimentData(experimentData),
|
||||
},
|
||||
})
|
||||
);
|
||||
@ -100,7 +125,11 @@ this.PrefsFeed = class PrefsFeed {
|
||||
|
||||
init() {
|
||||
this._prefs.observeBranch(this);
|
||||
aboutNewTabFeature.onUpdate(this.onExperimentUpdated);
|
||||
ExperimentAPI.on(
|
||||
"update",
|
||||
{ featureId: "newtab" },
|
||||
this.onExperimentUpdated
|
||||
);
|
||||
|
||||
this._storage = this.store.dbStorage.getDbTable("sectionPrefs");
|
||||
|
||||
@ -163,11 +192,7 @@ this.PrefsFeed = class PrefsFeed {
|
||||
value: handoffToAwesomebarPrefValue,
|
||||
});
|
||||
|
||||
// Add experiment values and default values
|
||||
values.featureConfig =
|
||||
aboutNewTabFeature.getValue({
|
||||
sendExposurePing: false,
|
||||
}) || {};
|
||||
this.addExperimentDataToValues(values);
|
||||
|
||||
this._setBoolPref(values, "newNewtabExperience.enabled", false);
|
||||
this._setBoolPref(values, "customizationMenu.enabled", false);
|
||||
@ -207,7 +232,7 @@ this.PrefsFeed = class PrefsFeed {
|
||||
|
||||
removeListeners() {
|
||||
this._prefs.ignoreBranch(this);
|
||||
aboutNewTabFeature.off(this.onExperimentUpdated);
|
||||
ExperimentAPI.off(this.onExperimentUpdated);
|
||||
if (this.geo === "") {
|
||||
Services.obs.removeObserver(this, Region.REGION_TOPIC);
|
||||
}
|
||||
|
@ -3,6 +3,9 @@
|
||||
const { ExperimentAPI } = ChromeUtils.import(
|
||||
"resource://messaging-system/experiments/ExperimentAPI.jsm"
|
||||
);
|
||||
const { ExperimentFakes } = ChromeUtils.import(
|
||||
"resource://testing-common/MSTestUtils.jsm"
|
||||
);
|
||||
|
||||
/**
|
||||
* Enrolls browser in an experiment with value featureValue and
|
||||
@ -15,8 +18,11 @@ const { ExperimentAPI } = ChromeUtils.import(
|
||||
async function testWithExperimentFeatureValue(slug, featureValue, test) {
|
||||
test_newtab({
|
||||
async before() {
|
||||
let updatePromise = new Promise(resolve =>
|
||||
ExperimentAPI._store.once(`update:${slug}`, resolve)
|
||||
let updatePromise = ExperimentFakes.waitForExperimentUpdate(
|
||||
ExperimentAPI,
|
||||
{
|
||||
slug,
|
||||
}
|
||||
);
|
||||
|
||||
ExperimentAPI._store.addExperiment({
|
||||
|
@ -55,7 +55,6 @@ describe("PrefsFeed", () => {
|
||||
overrider.set({
|
||||
PrivateBrowsingUtils: { enabled: true },
|
||||
Services: ServicesStub,
|
||||
aboutNewTabFeature: new global.ExperimentFeature(),
|
||||
});
|
||||
});
|
||||
afterEach(() => {
|
||||
@ -84,8 +83,15 @@ describe("PrefsFeed", () => {
|
||||
assert.isTrue(data.isPrivateBrowsingEnabled);
|
||||
});
|
||||
it("should dispatch PREFS_INITIAL_VALUES with a .featureConfig", () => {
|
||||
sandbox.stub(global.aboutNewTabFeature, "getValue").returns({
|
||||
prefsButtonIcon: "icon-foo",
|
||||
sandbox.stub(global.ExperimentAPI, "getExperiment").returns({
|
||||
active: true,
|
||||
branch: {
|
||||
slug: "foo",
|
||||
feature: {
|
||||
featureId: "newtab",
|
||||
value: { prefsButtonIcon: "icon-foo" },
|
||||
},
|
||||
},
|
||||
});
|
||||
feed.onAction({ type: at.INIT });
|
||||
assert.equal(
|
||||
@ -95,15 +101,15 @@ describe("PrefsFeed", () => {
|
||||
const [{ data }] = feed.store.dispatch.firstCall.args;
|
||||
assert.deepEqual(data.featureConfig, { prefsButtonIcon: "icon-foo" });
|
||||
});
|
||||
it("should dispatch PREFS_INITIAL_VALUES with an empty object if no experiment is returned", () => {
|
||||
sandbox.stub(global.aboutNewTabFeature, "getValue").returns(null);
|
||||
it("should dispatch PREFS_INITIAL_VALUES with a default feature config if no experiment is returned", () => {
|
||||
sandbox.stub(global.ExperimentAPI, "getExperiment").returns(null);
|
||||
feed.onAction({ type: at.INIT });
|
||||
assert.equal(
|
||||
feed.store.dispatch.firstCall.args[0].type,
|
||||
at.PREFS_INITIAL_VALUES
|
||||
);
|
||||
const [{ data }] = feed.store.dispatch.firstCall.args;
|
||||
assert.deepEqual(data.featureConfig, {});
|
||||
assert.deepEqual(data.featureConfig, { prefsButtonIcon: "icon-settings" });
|
||||
});
|
||||
it("should add one branch observer on init", () => {
|
||||
feed.onAction({ type: at.INIT });
|
||||
@ -154,21 +160,32 @@ describe("PrefsFeed", () => {
|
||||
})
|
||||
);
|
||||
});
|
||||
it("should send a PREF_CHANGED actions when onExperimentUpdated is called", () => {
|
||||
sandbox.stub(global.aboutNewTabFeature, "getValue").returns({
|
||||
prefsButtonIcon: "icon-new",
|
||||
});
|
||||
feed.onExperimentUpdated();
|
||||
it("should send 2 PREF_CHANGED actions when onExperimentUpdated is called", () => {
|
||||
const experimentData = {
|
||||
active: true,
|
||||
slug: "foo",
|
||||
branch: {
|
||||
slug: "boo",
|
||||
feature: {
|
||||
featureId: "newtab",
|
||||
value: { prefsButtonIcon: "icon-boo" },
|
||||
},
|
||||
},
|
||||
};
|
||||
feed.onExperimentUpdated({}, experimentData);
|
||||
assert.calledTwice(feed.store.dispatch);
|
||||
assert.calledWith(
|
||||
feed.store.dispatch,
|
||||
ac.BroadcastToContent({
|
||||
type: at.PREF_CHANGED,
|
||||
data: {
|
||||
name: "featureConfig",
|
||||
value: {
|
||||
prefsButtonIcon: "icon-new",
|
||||
},
|
||||
},
|
||||
data: { name: "experimentData", value: experimentData },
|
||||
})
|
||||
);
|
||||
assert.calledWith(
|
||||
feed.store.dispatch,
|
||||
ac.BroadcastToContent({
|
||||
type: at.PREF_CHANGED,
|
||||
data: { name: "featureConfig", value: { prefsButtonIcon: "icon-boo" } },
|
||||
})
|
||||
);
|
||||
});
|
||||
|
@ -66,13 +66,6 @@ class JSWindowActorChild {
|
||||
}
|
||||
}
|
||||
|
||||
class ExperimentFeature {
|
||||
isEnabled() {}
|
||||
getValue() {}
|
||||
onUpdate() {}
|
||||
off() {}
|
||||
}
|
||||
|
||||
const TEST_GLOBAL = {
|
||||
JSWindowActorParent,
|
||||
JSWindowActorChild,
|
||||
@ -441,7 +434,6 @@ const TEST_GLOBAL = {
|
||||
on: () => {},
|
||||
off: () => {},
|
||||
},
|
||||
ExperimentFeature,
|
||||
TelemetryEnvironment: {
|
||||
setExperimentActive() {},
|
||||
currentEnvironment: {
|
||||
|
@ -4,42 +4,12 @@
|
||||
|
||||
"use strict";
|
||||
|
||||
const EXPORTED_SYMBOLS = ["ExperimentAPI", "ExperimentFeature"];
|
||||
|
||||
/**
|
||||
* FEATURE MANIFEST
|
||||
* =================
|
||||
* Features must be added here to be accessible through the ExperimentFeature() API.
|
||||
* In the future, this will be moved to a configuration file.
|
||||
* @typedef {import("./@types/ExperimentManager").Enrollment} Enrollment
|
||||
* @typedef {import("./@types/ExperimentManager").FeatureConfig} FeatureConfig
|
||||
*/
|
||||
const MANIFEST = {
|
||||
aboutwelcome: {
|
||||
description: "The about:welcome page",
|
||||
enabledFallbackPref: "browser.aboutwelcome.enabled",
|
||||
variables: {
|
||||
value: {
|
||||
type: "json",
|
||||
fallbackPref: "browser.aboutwelcome.overrideContent",
|
||||
},
|
||||
},
|
||||
},
|
||||
newtab: {
|
||||
description: "The about:newtab page",
|
||||
variables: {
|
||||
value: {
|
||||
type: "json",
|
||||
fallbackPref: "browser.newtab.experiments.value",
|
||||
},
|
||||
},
|
||||
},
|
||||
"password-autocomplete": {
|
||||
description: "A special autocomplete UI for password fields.",
|
||||
},
|
||||
};
|
||||
|
||||
function isBooleanValueDefined(value) {
|
||||
return typeof value === "boolean";
|
||||
}
|
||||
const EXPORTED_SYMBOLS = ["ExperimentAPI"];
|
||||
|
||||
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
const { XPCOMUtils } = ChromeUtils.import(
|
||||
@ -66,17 +36,6 @@ XPCOMUtils.defineLazyPreferenceGetter(
|
||||
COLLECTION_ID_FALLBACK
|
||||
);
|
||||
|
||||
function parseJSON(value) {
|
||||
if (value) {
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch (e) {
|
||||
Cu.reportError(e);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const ExperimentAPI = {
|
||||
/**
|
||||
* @returns {Promise} Resolves when the API has synchronized to the main store
|
||||
@ -188,6 +147,32 @@ const ExperimentAPI = {
|
||||
return experiment?.branch || null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Lookup feature in active experiments and return status.
|
||||
* Sends exposure ping
|
||||
* @param {string} featureId Feature to lookup
|
||||
* @param {boolean} defaultValue
|
||||
* @param {{sendExposurePing: boolean}} options
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isFeatureEnabled(featureId, defaultValue, { sendExposurePing = true } = {}) {
|
||||
const branch = this.activateBranch({ featureId, sendExposurePing });
|
||||
if (branch?.feature.enabled !== undefined) {
|
||||
return branch.feature.enabled;
|
||||
}
|
||||
return defaultValue;
|
||||
},
|
||||
|
||||
/**
|
||||
* Lookup feature in active experiments and return value.
|
||||
* By default, this will send an exposure event.
|
||||
* @param {{featureId: string, sendExposurePing: boolean}} options
|
||||
* @returns {obj} The feature value
|
||||
*/
|
||||
getFeatureValue(options) {
|
||||
return this.activateBranch(options)?.feature.value;
|
||||
},
|
||||
|
||||
/**
|
||||
* Registers an event listener.
|
||||
* The following event names are used:
|
||||
@ -305,116 +290,6 @@ const ExperimentAPI = {
|
||||
},
|
||||
};
|
||||
|
||||
class ExperimentFeature {
|
||||
static MANIFEST = MANIFEST;
|
||||
constructor(featureId, manifest) {
|
||||
this.featureId = featureId;
|
||||
this.defaultPrefValues = {};
|
||||
this.manifest = manifest || ExperimentFeature.MANIFEST[featureId];
|
||||
if (!this.manifest) {
|
||||
Cu.reportError(
|
||||
`No manifest entry for ${featureId}. Please add one to toolkit/components/messaging-system/experiments/ExperimentAPI.jsm`
|
||||
);
|
||||
}
|
||||
const variables = this.manifest?.variables || {};
|
||||
|
||||
// Add special default variable.
|
||||
if (!variables.enabled) {
|
||||
variables.enabled = {
|
||||
type: "boolean",
|
||||
fallbackPref: this.manifest?.enabledFallbackPref,
|
||||
};
|
||||
}
|
||||
|
||||
Object.keys(variables).forEach(key => {
|
||||
const { type, fallbackPref } = variables[key];
|
||||
if (fallbackPref) {
|
||||
XPCOMUtils.defineLazyPreferenceGetter(
|
||||
this.defaultPrefValues,
|
||||
key,
|
||||
fallbackPref,
|
||||
null,
|
||||
() => {
|
||||
ExperimentAPI._store._emitFeatureUpdate(
|
||||
this.featureId,
|
||||
"pref-updated"
|
||||
);
|
||||
},
|
||||
type === "json" ? parseJSON : val => val
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookup feature in active experiments and return enabled.
|
||||
* By default, this will send an exposure event.
|
||||
* @param {{sendExposurePing: boolean, defaultValue?: any}} options
|
||||
* @returns {obj} The feature value
|
||||
*/
|
||||
isEnabled({ sendExposurePing, defaultValue = null } = {}) {
|
||||
const branch = ExperimentAPI.activateBranch({
|
||||
featureId: this.featureId,
|
||||
sendExposurePing,
|
||||
});
|
||||
|
||||
// First, try to return an experiment value if it exists.
|
||||
if (isBooleanValueDefined(branch?.feature.enabled)) {
|
||||
return branch.feature.enabled;
|
||||
}
|
||||
|
||||
// Then check the fallback pref, if it is defined
|
||||
if (isBooleanValueDefined(this.defaultPrefValues.enabled)) {
|
||||
return this.defaultPrefValues.enabled;
|
||||
}
|
||||
|
||||
// Finally, return options.defaulValue if neither was found
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookup feature in active experiments and return value.
|
||||
* By default, this will send an exposure event.
|
||||
* @param {{sendExposurePing: boolean, defaultValue?: any}} options
|
||||
* @returns {obj} The feature value
|
||||
*/
|
||||
getValue({ sendExposurePing, defaultValue = null } = {}) {
|
||||
const branch = ExperimentAPI.activateBranch({
|
||||
featureId: this.featureId,
|
||||
sendExposurePing,
|
||||
});
|
||||
if (branch?.feature?.value) {
|
||||
return branch.feature.value;
|
||||
}
|
||||
|
||||
return this.defaultPrefValues.value || defaultValue;
|
||||
}
|
||||
|
||||
onUpdate(callback) {
|
||||
ExperimentAPI._store._onFeatureUpdate(this.featureId, callback);
|
||||
}
|
||||
|
||||
off(callback) {
|
||||
ExperimentAPI._store._offFeatureUpdate(this.featureId, callback);
|
||||
}
|
||||
|
||||
debug() {
|
||||
return {
|
||||
enabled: this.isEnabled(),
|
||||
value: this.getValue(),
|
||||
experiment: ExperimentAPI.getExperimentMetaData({
|
||||
featureId: this.featureId,
|
||||
}),
|
||||
fallbackPrefs:
|
||||
this.defaultPrefValues &&
|
||||
Object.keys(this.defaultPrefValues).map(prefName => [
|
||||
prefName,
|
||||
this.defaultPrefValues[prefName],
|
||||
]),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
XPCOMUtils.defineLazyGetter(ExperimentAPI, "_store", function() {
|
||||
return IS_MAIN_PROCESS ? ExperimentManager.store : new ExperimentStore();
|
||||
});
|
||||
|
@ -123,25 +123,9 @@ class ExperimentStore extends SharedDataMap {
|
||||
this.emit(`update:${experiment.slug}`, experiment);
|
||||
if (experiment.branch.feature) {
|
||||
this.emit(`update:${experiment.branch.feature.featureId}`, experiment);
|
||||
this._emitFeatureUpdate(
|
||||
experiment.branch.feature.featureId,
|
||||
"experiment-updated"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_emitFeatureUpdate(featureId, reason) {
|
||||
this.emit(`featureUpdate:${featureId}`, reason);
|
||||
}
|
||||
|
||||
_onFeatureUpdate(featureId, callback) {
|
||||
this.on(`featureUpdate:${featureId}`, callback);
|
||||
}
|
||||
|
||||
_offFeatureUpdate(featureId, callback) {
|
||||
this.off(`featureUpdate:${featureId}`, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{featureId: string, experimentSlug: string, branchSlug: string}} experimentData
|
||||
*/
|
||||
|
@ -19,7 +19,6 @@ XPCSHELL_TESTS_MANIFESTS += ["test/unit/xpcshell.ini"]
|
||||
XPCSHELL_TESTS_MANIFESTS += ["targeting/test/unit/xpcshell.ini"]
|
||||
|
||||
TESTING_JS_MODULES += [
|
||||
"schemas/ExperimentFeatureManifest.schema.json",
|
||||
"schemas/NimbusExperiment.schema.json",
|
||||
"schemas/SpecialMessageActionSchemas/SpecialMessageActionSchemas.json",
|
||||
"schemas/TriggerActionSchemas/TriggerActionSchemas.json",
|
||||
|
@ -1,58 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$ref": "#/definitions/Feature",
|
||||
"definitions": {
|
||||
"Feature": {
|
||||
"additionalProperties": false,
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"enabledFallbackPref": {
|
||||
"type": "string"
|
||||
},
|
||||
"variables": {
|
||||
"additionalProperties": false,
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"value": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"const": "json"
|
||||
},
|
||||
"fallbackPref": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
]
|
||||
},
|
||||
"enabled": {
|
||||
"additionalProperties": false,
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"const": "boolean"
|
||||
},
|
||||
"fallbackPref": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"description"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
"use strict";
|
||||
|
||||
const { ExperimentAPI, ExperimentFeature } = ChromeUtils.import(
|
||||
const { ExperimentAPI } = ChromeUtils.import(
|
||||
"resource://messaging-system/experiments/ExperimentAPI.jsm"
|
||||
);
|
||||
const { ExperimentFakes } = ChromeUtils.import(
|
||||
@ -166,6 +166,145 @@ add_task(async function test_getExperiment_safe() {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
/**
|
||||
* #getValue
|
||||
*/
|
||||
add_task(async function test_getValue() {
|
||||
const sandbox = sinon.createSandbox();
|
||||
const manager = ExperimentFakes.manager();
|
||||
const feature = {
|
||||
featureId: "aboutwelcome",
|
||||
enabled: true,
|
||||
value: { title: "hi" },
|
||||
};
|
||||
const expected = ExperimentFakes.experiment("foo", {
|
||||
branch: { slug: "treatment", feature },
|
||||
});
|
||||
|
||||
await manager.onStartup();
|
||||
|
||||
sandbox.stub(ExperimentAPI, "_store").get(() => ExperimentFakes.childStore());
|
||||
|
||||
manager.store.addExperiment(expected);
|
||||
|
||||
await TestUtils.waitForCondition(
|
||||
() => ExperimentAPI.getExperiment({ slug: "foo" }),
|
||||
"Wait for child to sync"
|
||||
);
|
||||
|
||||
Assert.deepEqual(
|
||||
ExperimentAPI.getFeatureValue({ featureId: "aboutwelcome" }),
|
||||
feature.value,
|
||||
"should return a Branch by feature"
|
||||
);
|
||||
|
||||
Assert.deepEqual(
|
||||
ExperimentAPI.activateBranch({ featureId: "aboutwelcome" }),
|
||||
expected.branch,
|
||||
"should return an experiment branch by feature"
|
||||
);
|
||||
|
||||
Assert.equal(
|
||||
ExperimentAPI.activateBranch({ featureId: "doesnotexist" }),
|
||||
undefined,
|
||||
"should return undefined if the experiment is not found"
|
||||
);
|
||||
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
/**
|
||||
* #isFeatureEnabled
|
||||
*/
|
||||
|
||||
add_task(async function test_isFeatureEnabledDefault() {
|
||||
const sandbox = sinon.createSandbox();
|
||||
const manager = ExperimentFakes.manager();
|
||||
const FEATURE_ENABLED_DEFAULT = true;
|
||||
const expected = ExperimentFakes.experiment("foo");
|
||||
|
||||
await manager.onStartup();
|
||||
|
||||
sandbox.stub(ExperimentAPI, "_store").get(() => manager.store);
|
||||
|
||||
manager.store.addExperiment(expected);
|
||||
|
||||
Assert.deepEqual(
|
||||
ExperimentAPI.isFeatureEnabled("aboutwelcome", FEATURE_ENABLED_DEFAULT),
|
||||
FEATURE_ENABLED_DEFAULT,
|
||||
"should return enabled true as default"
|
||||
);
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
add_task(async function test_isFeatureEnabled() {
|
||||
const sandbox = sinon.createSandbox();
|
||||
const manager = ExperimentFakes.manager();
|
||||
const feature = {
|
||||
featureId: "aboutwelcome",
|
||||
enabled: false,
|
||||
value: null,
|
||||
};
|
||||
const expected = ExperimentFakes.experiment("foo", {
|
||||
branch: { slug: "treatment", feature },
|
||||
});
|
||||
|
||||
await manager.onStartup();
|
||||
|
||||
sandbox.stub(ExperimentAPI, "_store").get(() => manager.store);
|
||||
|
||||
manager.store.addExperiment(expected);
|
||||
|
||||
const emitSpy = sandbox.spy(manager.store, "emit");
|
||||
const actual = ExperimentAPI.isFeatureEnabled("aboutwelcome", true);
|
||||
|
||||
Assert.deepEqual(
|
||||
actual,
|
||||
feature.enabled,
|
||||
"should return feature as disabled"
|
||||
);
|
||||
|
||||
Assert.ok(emitSpy.calledWith("exposure"), "should emit an exposure event");
|
||||
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
add_task(async function test_isFeatureEnabled_no_exposure() {
|
||||
const sandbox = sinon.createSandbox();
|
||||
const manager = ExperimentFakes.manager();
|
||||
const feature = {
|
||||
featureId: "aboutwelcome",
|
||||
enabled: false,
|
||||
value: null,
|
||||
};
|
||||
const expected = ExperimentFakes.experiment("foo", {
|
||||
branch: { slug: "treatment", feature },
|
||||
});
|
||||
|
||||
await manager.onStartup();
|
||||
|
||||
sandbox.stub(ExperimentAPI, "_store").get(() => manager.store);
|
||||
|
||||
manager.store.addExperiment(expected);
|
||||
|
||||
const emitSpy = sandbox.spy(manager.store, "emit");
|
||||
const actual = ExperimentAPI.isFeatureEnabled("aboutwelcome", true, {
|
||||
sendExposurePing: false,
|
||||
});
|
||||
|
||||
Assert.deepEqual(
|
||||
actual,
|
||||
feature.enabled,
|
||||
"should return feature as disabled"
|
||||
);
|
||||
Assert.ok(
|
||||
emitSpy.neverCalledWith("exposure"),
|
||||
"should not emit an exposure event when options = { sendExposurePing: false}"
|
||||
);
|
||||
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
/**
|
||||
* #getRecipe
|
||||
*/
|
||||
@ -253,17 +392,9 @@ add_task(async function test_addExperiment_eventEmit_add() {
|
||||
|
||||
store.addExperiment(experiment);
|
||||
|
||||
Assert.equal(
|
||||
slugStub.callCount,
|
||||
1,
|
||||
"should call 'update' callback for slug when experiment is added"
|
||||
);
|
||||
Assert.equal(slugStub.callCount, 1);
|
||||
Assert.equal(slugStub.firstCall.args[1].slug, experiment.slug);
|
||||
Assert.equal(
|
||||
featureStub.callCount,
|
||||
1,
|
||||
"should call 'update' callback for featureId when an experiment is added"
|
||||
);
|
||||
Assert.equal(featureStub.callCount, 1);
|
||||
Assert.equal(featureStub.firstCall.args[1].slug, experiment.slug);
|
||||
});
|
||||
|
||||
|
@ -1,233 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
const { ExperimentAPI, ExperimentFeature } = ChromeUtils.import(
|
||||
"resource://messaging-system/experiments/ExperimentAPI.jsm"
|
||||
);
|
||||
const { ExperimentFakes } = ChromeUtils.import(
|
||||
"resource://testing-common/MSTestUtils.jsm"
|
||||
);
|
||||
const { TestUtils } = ChromeUtils.import(
|
||||
"resource://testing-common/TestUtils.jsm"
|
||||
);
|
||||
|
||||
const { Ajv } = ChromeUtils.import("resource://testing-common/ajv-4.1.1.js");
|
||||
const { XPCOMUtils } = ChromeUtils.import(
|
||||
"resource://gre/modules/XPCOMUtils.jsm"
|
||||
);
|
||||
Cu.importGlobalProperties(["fetch"]);
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "fetchSchema", async () => {
|
||||
const response = await fetch(
|
||||
"resource://testing-common/ExperimentFeatureManifest.schema.json"
|
||||
);
|
||||
const schema = await response.json();
|
||||
if (!schema) {
|
||||
throw new Error("Failed to load NimbusSchema");
|
||||
}
|
||||
return schema.definitions.Feature;
|
||||
});
|
||||
|
||||
async function setupForExperimentFeature() {
|
||||
const sandbox = sinon.createSandbox();
|
||||
const manager = ExperimentFakes.manager();
|
||||
await manager.onStartup();
|
||||
|
||||
sandbox.stub(ExperimentAPI, "_store").get(() => manager.store);
|
||||
|
||||
return { sandbox, manager };
|
||||
}
|
||||
|
||||
const FAKE_FEATURE_MANIFEST = {
|
||||
enabledFallbackPref: "testprefbranch.enabled",
|
||||
variables: {
|
||||
value: {
|
||||
type: "json",
|
||||
fallbackPref: "testprefbranch.value",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
add_task(async function test_feature_manifest_is_valid() {
|
||||
const ajv = new Ajv({ allErrors: true });
|
||||
const validate = ajv.compile(await fetchSchema);
|
||||
|
||||
// Validate each entry in the feature manifest.
|
||||
// See tookit/components/messaging-system/experiments/ExperimentAPI.jsm
|
||||
Object.keys(ExperimentFeature.MANIFEST).forEach(featureId => {
|
||||
const valid = validate(ExperimentFeature.MANIFEST[featureId]);
|
||||
if (!valid) {
|
||||
throw new Error(
|
||||
`The manfinifest entry for ${featureId} not valid in tookit/components/messaging-system/experiments/ExperimentAPI.jsm: ` +
|
||||
JSON.stringify(validate.errors, undefined, 2)
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* # ExperimentFeature.getValue
|
||||
*/
|
||||
add_task(async function test_ExperimentFeature_getValue() {
|
||||
const { sandbox } = await setupForExperimentFeature();
|
||||
|
||||
const featureInstance = new ExperimentFeature("foo", FAKE_FEATURE_MANIFEST);
|
||||
|
||||
Services.prefs.clearUserPref("testprefbranch.value");
|
||||
|
||||
Assert.deepEqual(
|
||||
featureInstance.getValue({ defaultValue: { hello: 1 } }),
|
||||
{ hello: 1 },
|
||||
"should return the defaultValue if no fallback pref is set"
|
||||
);
|
||||
|
||||
Services.prefs.setStringPref("testprefbranch.value", `{"bar": 123}`);
|
||||
|
||||
Assert.deepEqual(
|
||||
featureInstance.getValue(),
|
||||
{ bar: 123 },
|
||||
"should return the fallback pref value"
|
||||
);
|
||||
|
||||
Services.prefs.clearUserPref("testprefbranch.value");
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
add_task(
|
||||
async function test_ExperimentFeature_getValue_prefer_experiment_over_default() {
|
||||
const { sandbox, manager } = await setupForExperimentFeature();
|
||||
|
||||
const featureInstance = new ExperimentFeature("foo", FAKE_FEATURE_MANIFEST);
|
||||
|
||||
const expected = ExperimentFakes.experiment("anexperiment", {
|
||||
branch: {
|
||||
slug: "treatment",
|
||||
feature: {
|
||||
featureId: "foo",
|
||||
enabled: true,
|
||||
value: { whoa: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
manager.store.addExperiment(expected);
|
||||
|
||||
Services.prefs.setStringPref("testprefbranch.value", `{"bar": 123}`);
|
||||
|
||||
Assert.deepEqual(
|
||||
featureInstance.getValue(),
|
||||
{ whoa: true },
|
||||
"should return the experiment feature value, not the fallback one."
|
||||
);
|
||||
|
||||
Services.prefs.clearUserPref("testprefbranch.value");
|
||||
sandbox.restore();
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* # ExperimentFeature.isEnabled
|
||||
*/
|
||||
|
||||
add_task(async function test_ExperimentFeature_isEnabled_default() {
|
||||
const { sandbox } = await setupForExperimentFeature();
|
||||
|
||||
const featureInstance = new ExperimentFeature("foo", FAKE_FEATURE_MANIFEST);
|
||||
|
||||
const noPrefFeature = new ExperimentFeature("bar", {});
|
||||
|
||||
Assert.equal(
|
||||
noPrefFeature.isEnabled(),
|
||||
null,
|
||||
"should return null if no default pref branch is configured"
|
||||
);
|
||||
|
||||
Services.prefs.clearUserPref("testprefbranch.enabled");
|
||||
|
||||
Assert.equal(
|
||||
featureInstance.isEnabled(),
|
||||
null,
|
||||
"should return null if no default value or pref is set"
|
||||
);
|
||||
|
||||
Assert.equal(
|
||||
featureInstance.isEnabled({ defaultValue: false }),
|
||||
false,
|
||||
"should use the default value param if no pref is set"
|
||||
);
|
||||
|
||||
Services.prefs.setBoolPref("testprefbranch.enabled", false);
|
||||
|
||||
Assert.equal(
|
||||
featureInstance.isEnabled({ defaultValue: true }),
|
||||
false,
|
||||
"should use the default pref value, including if it is false"
|
||||
);
|
||||
|
||||
Services.prefs.clearUserPref("testprefbranch.enabled");
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
add_task(
|
||||
async function test_ExperimentFeature_isEnabled_prefer_experiment_over_default() {
|
||||
const { sandbox, manager } = await setupForExperimentFeature();
|
||||
const expected = ExperimentFakes.experiment("foo", {
|
||||
branch: {
|
||||
slug: "treatment",
|
||||
feature: {
|
||||
featureId: "foo",
|
||||
enabled: true,
|
||||
value: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const featureInstance = new ExperimentFeature("foo", FAKE_FEATURE_MANIFEST);
|
||||
|
||||
manager.store.addExperiment(expected);
|
||||
|
||||
const emitSpy = sandbox.spy(manager.store, "emit");
|
||||
Services.prefs.setBoolPref("testprefbranch.enabled", false);
|
||||
|
||||
Assert.equal(
|
||||
featureInstance.isEnabled(),
|
||||
true,
|
||||
"should return the enabled value defined in the experiment, not the default pref"
|
||||
);
|
||||
|
||||
Assert.ok(emitSpy.calledWith("exposure"), "should emit an exposure event");
|
||||
|
||||
Services.prefs.clearUserPref("testprefbranch.enabled");
|
||||
sandbox.restore();
|
||||
}
|
||||
);
|
||||
|
||||
add_task(async function test_ExperimentFeature_isEnabled_no_exposure() {
|
||||
const { sandbox, manager } = await setupForExperimentFeature();
|
||||
const expected = ExperimentFakes.experiment("blah", {
|
||||
branch: {
|
||||
slug: "treatment",
|
||||
feature: {
|
||||
featureId: "foo",
|
||||
enabled: false,
|
||||
value: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
const featureInstance = new ExperimentFeature("foo", FAKE_FEATURE_MANIFEST);
|
||||
|
||||
sandbox.stub(ExperimentAPI, "_store").get(() => manager.store);
|
||||
|
||||
manager.store.addExperiment(expected);
|
||||
|
||||
const emitSpy = sandbox.spy(manager.store, "emit");
|
||||
|
||||
const actual = featureInstance.isEnabled({ sendExposurePing: false });
|
||||
|
||||
Assert.deepEqual(actual, false, "should return feature as disabled");
|
||||
Assert.ok(
|
||||
emitSpy.neverCalledWith("exposure"),
|
||||
"should not emit an exposure event when options = { sendExposurePing: false}"
|
||||
);
|
||||
|
||||
sandbox.restore();
|
||||
});
|
@ -14,6 +14,5 @@ support-files =
|
||||
[test_MSTestUtils.js]
|
||||
[test_SharedDataMap.js]
|
||||
[test_ExperimentAPI.js]
|
||||
[test_ExperimentAPI_ExperimentFeature.js]
|
||||
[test_RemoteSettingsExperimentLoader.js]
|
||||
[test_RemoteSettingsExperimentLoader_updateRecipes.js]
|
||||
|
@ -15,17 +15,11 @@ const LoginInfo = new Components.Constructor(
|
||||
"init"
|
||||
);
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "autocompleteFeature", () => {
|
||||
const { Feature } = ChromeUtils.import(
|
||||
"resource://messaging-system/experiments/ExperimentAPI.jsm"
|
||||
);
|
||||
return new Feature("password-autocomplete");
|
||||
});
|
||||
|
||||
XPCOMUtils.defineLazyGlobalGetters(this, ["URL"]);
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetters(this, {
|
||||
ChromeMigrationUtils: "resource:///modules/ChromeMigrationUtils.jsm",
|
||||
ExperimentAPI: "resource://messaging-system/experiments/ExperimentAPI.jsm",
|
||||
LoginHelper: "resource://gre/modules/LoginHelper.jsm",
|
||||
MigrationUtils: "resource:///modules/MigrationUtils.jsm",
|
||||
PasswordGenerator: "resource://gre/modules/PasswordGenerator.jsm",
|
||||
@ -328,7 +322,8 @@ class LoginManagerParent extends JSWindowActorParent {
|
||||
const profiles = await migrator.getSourceProfiles();
|
||||
if (
|
||||
profiles.length == 1 &&
|
||||
autocompleteFeature.getValue()?.directMigrateSingleProfile
|
||||
ExperimentAPI.getFeatureValue({ featureId: "password-autocomplete" })
|
||||
?.directMigrateSingleProfile
|
||||
) {
|
||||
const loginAdded = new Promise(resolve => {
|
||||
const obs = (subject, topic, data) => {
|
||||
|
@ -49,14 +49,10 @@ add_task(async function test_initialize() {
|
||||
const migrator = sinon
|
||||
.stub(MigrationUtils, "getMigrator")
|
||||
.resolves(gTestMigrator);
|
||||
|
||||
const experiment = sinon.stub(ExperimentAPI, "activateBranch").returns({
|
||||
slug: "foo",
|
||||
ratio: 1,
|
||||
feature: {
|
||||
value: { directMigrateSingleProfile: true },
|
||||
},
|
||||
});
|
||||
const experiment = sinon.stub(ExperimentAPI, "getFeatureValue");
|
||||
experiment
|
||||
.withArgs({ featureId: "password-autocomplete" })
|
||||
.returns({ directMigrateSingleProfile: true });
|
||||
|
||||
// This makes the last autocomplete test *not* show import suggestions.
|
||||
Services.prefs.setIntPref("signon.suggestImportCount", 3);
|
||||
|
Loading…
Reference in New Issue
Block a user