Backed out changeset 110829513605 (bug 1672202) for causing bc failures in browser_autocomplete_import.js

CLOSED TREE
This commit is contained in:
Mihai Alexandru Michis 2021-02-06 01:00:32 +02:00
parent d3beb62c1d
commit 63453c1404
16 changed files with 295 additions and 557 deletions

View File

@ -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;

View File

@ -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);

View File

@ -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;

View File

@ -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);
}

View File

@ -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({

View File

@ -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" } },
})
);
});

View File

@ -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: {

View File

@ -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();
});

View File

@ -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
*/

View File

@ -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",

View File

@ -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"
]
}
}
}

View File

@ -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);
});

View File

@ -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();
});

View File

@ -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]

View File

@ -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) => {

View File

@ -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);