1310 lines
31 KiB
JavaScript
1310 lines
31 KiB
JavaScript
/* Any copyright is dedicated to the Public Domain.
|
|
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
|
|
|
const { MatchStatus } = ChromeUtils.importESModule(
|
|
"resource://nimbus/lib/RemoteSettingsExperimentLoader.sys.mjs"
|
|
);
|
|
|
|
const LOCALIZATIONS = {
|
|
"en-US": {
|
|
foo: "localized foo text",
|
|
qux: "localized qux text",
|
|
grault: "localized grault text",
|
|
waldo: "localized waldo text",
|
|
},
|
|
};
|
|
|
|
const DEEPLY_NESTED_VALUE = {
|
|
foo: {
|
|
$l10n: {
|
|
id: "foo",
|
|
comment: "foo comment",
|
|
text: "original foo text",
|
|
},
|
|
},
|
|
bar: {
|
|
qux: {
|
|
$l10n: {
|
|
id: "qux",
|
|
comment: "qux comment",
|
|
text: "original qux text",
|
|
},
|
|
},
|
|
quux: {
|
|
grault: {
|
|
$l10n: {
|
|
id: "grault",
|
|
comment: "grault comment",
|
|
text: "orginal grault text",
|
|
},
|
|
},
|
|
garply: "original garply text",
|
|
},
|
|
corge: "original corge text",
|
|
},
|
|
baz: "original baz text",
|
|
waldo: [
|
|
{
|
|
$l10n: {
|
|
id: "waldo",
|
|
comment: "waldo comment",
|
|
text: "original waldo text",
|
|
},
|
|
},
|
|
],
|
|
};
|
|
|
|
const LOCALIZED_DEEPLY_NESTED_VALUE = {
|
|
foo: "localized foo text",
|
|
bar: {
|
|
qux: "localized qux text",
|
|
quux: {
|
|
grault: "localized grault text",
|
|
garply: "original garply text",
|
|
},
|
|
corge: "original corge text",
|
|
},
|
|
baz: "original baz text",
|
|
waldo: ["localized waldo text"],
|
|
};
|
|
|
|
const FEATURE_ID = "testfeature1";
|
|
const TEST_PREF_BRANCH = "testfeature1.";
|
|
const FEATURE = new ExperimentFeature(FEATURE_ID, {
|
|
isEarlyStartup: false,
|
|
variables: {
|
|
foo: {
|
|
type: "string",
|
|
fallbackPref: `${TEST_PREF_BRANCH}foo`,
|
|
},
|
|
bar: {
|
|
type: "json",
|
|
fallbackPref: `${TEST_PREF_BRANCH}bar`,
|
|
},
|
|
baz: {
|
|
type: "string",
|
|
fallbackPref: `${TEST_PREF_BRANCH}baz`,
|
|
},
|
|
waldo: {
|
|
type: "json",
|
|
fallbackPref: `${TEST_PREF_BRANCH}waldo`,
|
|
},
|
|
},
|
|
});
|
|
|
|
add_setup(function setup() {
|
|
Services.fog.initializeFOG();
|
|
|
|
registerCleanupFunction(NimbusTestUtils.addTestFeatures(FEATURE));
|
|
});
|
|
|
|
function setupTest({ ...args } = {}) {
|
|
return NimbusTestUtils.setupTest({ ...args, clearTelemetry: true });
|
|
}
|
|
|
|
add_task(async function test_schema() {
|
|
const recipe = NimbusTestUtils.factories.recipe("foo");
|
|
|
|
info("Testing recipe without a localizations entry");
|
|
await NimbusTestUtils.validateExperiment(recipe);
|
|
|
|
info("Testing recipe with a 'null' localizations entry");
|
|
await NimbusTestUtils.validateExperiment({
|
|
...recipe,
|
|
localizations: null,
|
|
});
|
|
|
|
info("Testing recipe with a valid localizations entry");
|
|
await NimbusTestUtils.validateExperiment({
|
|
...recipe,
|
|
localizations: LOCALIZATIONS,
|
|
});
|
|
|
|
info("Testing recipe with an invalid localizations entry");
|
|
await Assert.rejects(
|
|
NimbusTestUtils.validateExperiment({
|
|
...recipe,
|
|
localizations: [],
|
|
}),
|
|
/Experiment foo not valid/
|
|
);
|
|
});
|
|
|
|
add_task(function test_substituteLocalizations() {
|
|
Assert.equal(
|
|
ExperimentFeature.substituteLocalizations("string", LOCALIZATIONS["en-US"]),
|
|
"string",
|
|
"String values should not be subsituted"
|
|
);
|
|
|
|
Assert.equal(
|
|
ExperimentFeature.substituteLocalizations(
|
|
{
|
|
$l10n: {
|
|
id: "foo",
|
|
comment: "foo comment",
|
|
text: "original foo text",
|
|
},
|
|
},
|
|
LOCALIZATIONS["en-US"]
|
|
),
|
|
"localized foo text",
|
|
"$l10n objects should be substituted"
|
|
);
|
|
|
|
Assert.deepEqual(
|
|
ExperimentFeature.substituteLocalizations(
|
|
DEEPLY_NESTED_VALUE,
|
|
LOCALIZATIONS["en-US"]
|
|
),
|
|
LOCALIZED_DEEPLY_NESTED_VALUE,
|
|
"Supports nested substitutions"
|
|
);
|
|
|
|
Assert.throws(
|
|
() =>
|
|
ExperimentFeature.substituteLocalizations(
|
|
{
|
|
foo: {
|
|
$l10n: {
|
|
id: "BOGUS",
|
|
comment: "A variable with a missing id",
|
|
text: "Original text",
|
|
},
|
|
},
|
|
},
|
|
LOCALIZATIONS["en-US"]
|
|
),
|
|
ex => ex.reason === "l10n-missing-entry"
|
|
);
|
|
});
|
|
|
|
add_task(async function test_getLocalizedValue() {
|
|
const { manager, cleanup } = await setupTest();
|
|
|
|
const experiment = NimbusTestUtils.factories.recipe.withFeatureConfig(
|
|
"experiment",
|
|
{ featureId: FEATURE_ID, value: DEEPLY_NESTED_VALUE },
|
|
{ localizations: LOCALIZATIONS }
|
|
);
|
|
|
|
const doExperimentCleanup = await NimbusTestUtils.enroll(experiment);
|
|
|
|
const enrollment = manager.store.getExperimentForFeature(FEATURE_ID);
|
|
|
|
Assert.deepEqual(
|
|
FEATURE._getLocalizedValue(enrollment),
|
|
LOCALIZED_DEEPLY_NESTED_VALUE,
|
|
"_getLocalizedValue() for all values"
|
|
);
|
|
|
|
Assert.deepEqual(
|
|
FEATURE._getLocalizedValue(enrollment, "foo"),
|
|
LOCALIZED_DEEPLY_NESTED_VALUE.foo,
|
|
"_getLocalizedValue() with a top-level localized variable"
|
|
);
|
|
|
|
Assert.deepEqual(
|
|
FEATURE._getLocalizedValue(enrollment, "bar"),
|
|
LOCALIZED_DEEPLY_NESTED_VALUE.bar,
|
|
"_getLocalizedValue() with a nested localization"
|
|
);
|
|
|
|
await doExperimentCleanup();
|
|
await cleanup();
|
|
});
|
|
|
|
add_task(async function test_getLocalizedValue_unenroll_missingEntry() {
|
|
const { manager, cleanup } = await setupTest();
|
|
|
|
const experiment = NimbusTestUtils.factories.recipe.withFeatureConfig(
|
|
"experiment",
|
|
{
|
|
featureId: FEATURE_ID,
|
|
value: {
|
|
bar: {
|
|
$l10n: {
|
|
id: "BOGUS",
|
|
comment: "Bogus localization",
|
|
text: "Original text",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{ localizations: LOCALIZATIONS }
|
|
);
|
|
|
|
await NimbusTestUtils.enroll(experiment);
|
|
|
|
const enrollment = manager.store.getExperimentForFeature(FEATURE_ID);
|
|
|
|
Assert.deepEqual(
|
|
FEATURE._getLocalizedValue(enrollment),
|
|
undefined,
|
|
"_getLocalizedValue() with a bogus localization"
|
|
);
|
|
|
|
await NimbusTestUtils.waitForInactiveEnrollment(enrollment.slug);
|
|
|
|
Assert.equal(
|
|
manager.store.getExperimentForFeature(FEATURE_ID),
|
|
null,
|
|
"Experiment should be unenrolled"
|
|
);
|
|
|
|
const gleanEvents = Glean.nimbusEvents.unenrollment.testGetValue("events");
|
|
Assert.equal(gleanEvents.length, 1, "Should be one unenrollment event");
|
|
Assert.equal(
|
|
gleanEvents[0].extra.reason,
|
|
"l10n-missing-entry",
|
|
"Reason should match"
|
|
);
|
|
Assert.equal(
|
|
gleanEvents[0].extra.experiment,
|
|
"experiment",
|
|
"Slug should match"
|
|
);
|
|
|
|
TelemetryTestUtils.assertEvents(
|
|
[
|
|
{
|
|
value: "experiment",
|
|
extra: { reason: "l10n-missing-entry" },
|
|
},
|
|
],
|
|
{
|
|
category: "normandy",
|
|
method: "unenroll",
|
|
object: "nimbus_experiment",
|
|
}
|
|
);
|
|
|
|
await cleanup();
|
|
});
|
|
|
|
add_task(async function test_getLocalizedValue_unenroll_missingEntry() {
|
|
const { manager, cleanup } = await setupTest();
|
|
|
|
await manager.store.init();
|
|
await manager.onStartup();
|
|
|
|
const experiment = NimbusTestUtils.factories.recipe.withFeatureConfig(
|
|
"experiment",
|
|
{
|
|
featureId: FEATURE_ID,
|
|
value: {
|
|
bar: {
|
|
$l10n: {
|
|
id: "BOGUS",
|
|
comment: "Bogus localization",
|
|
text: "Original text",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{ localizations: { "en-CA": {} } }
|
|
);
|
|
|
|
await NimbusTestUtils.enroll(experiment);
|
|
|
|
const enrollment = manager.store.getExperimentForFeature(FEATURE_ID);
|
|
|
|
Assert.deepEqual(
|
|
FEATURE._getLocalizedValue(enrollment),
|
|
undefined,
|
|
"_getLocalizedValue() with a bogus localization"
|
|
);
|
|
|
|
await NimbusTestUtils.waitForInactiveEnrollment(enrollment.slug);
|
|
|
|
Assert.equal(
|
|
manager.store.getExperimentForFeature(FEATURE_ID),
|
|
null,
|
|
"Experiment should be unenrolled"
|
|
);
|
|
|
|
const gleanEvents = Glean.nimbusEvents.unenrollment.testGetValue("events");
|
|
Assert.equal(gleanEvents.length, 1, "Should be one unenrollment event");
|
|
Assert.equal(
|
|
gleanEvents[0].extra.reason,
|
|
"l10n-missing-locale",
|
|
"Reason should match"
|
|
);
|
|
Assert.equal(
|
|
gleanEvents[0].extra.experiment,
|
|
"experiment",
|
|
"Slug should match"
|
|
);
|
|
|
|
TelemetryTestUtils.assertEvents(
|
|
[
|
|
{
|
|
value: "experiment",
|
|
extra: { reason: "l10n-missing-locale" },
|
|
},
|
|
],
|
|
{
|
|
category: "normandy",
|
|
method: "unenroll",
|
|
object: "nimbus_experiment",
|
|
}
|
|
);
|
|
|
|
await cleanup();
|
|
});
|
|
|
|
add_task(async function test_getVariables() {
|
|
const { cleanup } = await setupTest();
|
|
|
|
const experiment = NimbusTestUtils.factories.recipe.withFeatureConfig(
|
|
"experiment",
|
|
{ featureId: FEATURE_ID, value: DEEPLY_NESTED_VALUE },
|
|
{ localizations: LOCALIZATIONS }
|
|
);
|
|
|
|
const doExperimentCleanup = await NimbusTestUtils.enroll(experiment);
|
|
|
|
Assert.deepEqual(
|
|
FEATURE.getAllVariables(),
|
|
LOCALIZED_DEEPLY_NESTED_VALUE,
|
|
"getAllVariables() returns subsituted values"
|
|
);
|
|
|
|
Assert.equal(
|
|
FEATURE.getVariable("foo"),
|
|
LOCALIZED_DEEPLY_NESTED_VALUE.foo,
|
|
"getVariable() returns a top-level substituted value"
|
|
);
|
|
|
|
Assert.deepEqual(
|
|
FEATURE.getVariable("bar"),
|
|
LOCALIZED_DEEPLY_NESTED_VALUE.bar,
|
|
"getVariable() returns a nested substitution"
|
|
);
|
|
|
|
Assert.deepEqual(
|
|
FEATURE.getVariable("baz"),
|
|
DEEPLY_NESTED_VALUE.baz,
|
|
"getVariable() returns non-localized variables unmodified"
|
|
);
|
|
|
|
Assert.deepEqual(
|
|
FEATURE.getVariable("waldo"),
|
|
LOCALIZED_DEEPLY_NESTED_VALUE.waldo,
|
|
"getVariable() returns substitutions inside arrays"
|
|
);
|
|
|
|
await doExperimentCleanup();
|
|
await cleanup();
|
|
});
|
|
|
|
add_task(async function test_getVariables_fallback() {
|
|
const { cleanup } = await setupTest();
|
|
|
|
Services.prefs.setStringPref(
|
|
FEATURE.manifest.variables.foo.fallbackPref,
|
|
"fallback-foo-pref-value"
|
|
);
|
|
Services.prefs.setStringPref(
|
|
FEATURE.manifest.variables.baz.fallbackPref,
|
|
"fallback-baz-pref-value"
|
|
);
|
|
|
|
const recipes = {
|
|
experiment: NimbusTestUtils.factories.recipe.withFeatureConfig(
|
|
"experiment",
|
|
{
|
|
featureId: FEATURE_ID,
|
|
value: { foo: DEEPLY_NESTED_VALUE.foo },
|
|
},
|
|
{
|
|
localizations: {
|
|
"en-US": {
|
|
foo: LOCALIZATIONS["en-US"].foo,
|
|
},
|
|
},
|
|
}
|
|
),
|
|
|
|
rollout: NimbusTestUtils.factories.recipe.withFeatureConfig(
|
|
"rollout",
|
|
{
|
|
featureId: FEATURE_ID,
|
|
value: { bar: DEEPLY_NESTED_VALUE.bar },
|
|
},
|
|
{
|
|
isRollout: true,
|
|
localizations: {
|
|
"en-US": {
|
|
qux: LOCALIZATIONS["en-US"].qux,
|
|
grault: LOCALIZATIONS["en-US"].grault,
|
|
},
|
|
},
|
|
}
|
|
),
|
|
};
|
|
|
|
const experimentCleanup = {};
|
|
|
|
Assert.deepEqual(
|
|
FEATURE.getAllVariables({ defaultValues: { waldo: ["default-value"] } }),
|
|
{
|
|
foo: "fallback-foo-pref-value",
|
|
bar: null,
|
|
baz: "fallback-baz-pref-value",
|
|
waldo: ["default-value"],
|
|
},
|
|
"getAllVariables() returns only values from prefs and defaults"
|
|
);
|
|
|
|
Assert.equal(
|
|
FEATURE.getVariable("foo"),
|
|
"fallback-foo-pref-value",
|
|
"variable foo returned from prefs"
|
|
);
|
|
Assert.equal(
|
|
FEATURE.getVariable("bar"),
|
|
undefined,
|
|
"variable bar returned from rollout"
|
|
);
|
|
Assert.equal(
|
|
FEATURE.getVariable("baz"),
|
|
"fallback-baz-pref-value",
|
|
"variable baz returned from prefs"
|
|
);
|
|
|
|
// Enroll in the rollout.
|
|
experimentCleanup.rollout = await NimbusTestUtils.enroll(recipes.rollout);
|
|
|
|
Assert.deepEqual(
|
|
FEATURE.getAllVariables({ defaultValues: { waldo: ["default-value"] } }),
|
|
{
|
|
foo: "fallback-foo-pref-value",
|
|
bar: LOCALIZED_DEEPLY_NESTED_VALUE.bar,
|
|
baz: "fallback-baz-pref-value",
|
|
waldo: ["default-value"],
|
|
},
|
|
"getAllVariables() returns subsituted values from the rollout"
|
|
);
|
|
|
|
Assert.equal(
|
|
FEATURE.getVariable("foo"),
|
|
"fallback-foo-pref-value",
|
|
"variable foo returned from prefs"
|
|
);
|
|
Assert.deepEqual(
|
|
FEATURE.getVariable("bar"),
|
|
LOCALIZED_DEEPLY_NESTED_VALUE.bar,
|
|
"variable bar returned from rollout"
|
|
);
|
|
Assert.equal(
|
|
FEATURE.getVariable("baz"),
|
|
"fallback-baz-pref-value",
|
|
"variable baz returned from prefs"
|
|
);
|
|
|
|
// Enroll in the experiment.
|
|
experimentCleanup.experiment = await NimbusTestUtils.enroll(
|
|
recipes.experiment
|
|
);
|
|
|
|
Assert.deepEqual(
|
|
FEATURE.getAllVariables({ defaultValues: { waldo: ["default-value"] } }),
|
|
{
|
|
foo: LOCALIZED_DEEPLY_NESTED_VALUE.foo,
|
|
bar: null,
|
|
baz: "fallback-baz-pref-value",
|
|
waldo: ["default-value"],
|
|
},
|
|
"getAllVariables() returns subsituted values from the experiment"
|
|
);
|
|
|
|
Assert.equal(
|
|
FEATURE.getVariable("foo"),
|
|
LOCALIZED_DEEPLY_NESTED_VALUE.foo,
|
|
"variable foo returned from experiment"
|
|
);
|
|
Assert.deepEqual(
|
|
FEATURE.getVariable("bar"),
|
|
LOCALIZED_DEEPLY_NESTED_VALUE.bar,
|
|
"variable bar returned from rollout"
|
|
);
|
|
Assert.equal(
|
|
FEATURE.getVariable("baz"),
|
|
"fallback-baz-pref-value",
|
|
"variable baz returned from prefs"
|
|
);
|
|
|
|
// Unenroll from the rollout so we are only enrolled in an experiment.
|
|
await experimentCleanup.rollout();
|
|
|
|
Assert.deepEqual(
|
|
FEATURE.getAllVariables({ defaultValues: { waldo: ["default-value"] } }),
|
|
{
|
|
foo: LOCALIZED_DEEPLY_NESTED_VALUE.foo,
|
|
bar: null,
|
|
baz: "fallback-baz-pref-value",
|
|
waldo: ["default-value"],
|
|
},
|
|
"getAllVariables() returns substituted values from the experiment"
|
|
);
|
|
|
|
Assert.equal(
|
|
FEATURE.getVariable("foo"),
|
|
LOCALIZED_DEEPLY_NESTED_VALUE.foo,
|
|
"variable foo returned from experiment"
|
|
);
|
|
Assert.equal(
|
|
FEATURE.getVariable("bar"),
|
|
undefined,
|
|
"variable bar is not set"
|
|
);
|
|
Assert.equal(
|
|
FEATURE.getVariable("baz"),
|
|
"fallback-baz-pref-value",
|
|
"variable baz returned from prefs"
|
|
);
|
|
|
|
// Unenroll from experiment. We are enrolled in nothing.
|
|
await experimentCleanup.experiment();
|
|
|
|
Assert.deepEqual(
|
|
FEATURE.getAllVariables({ defaultValues: { waldo: ["default-value"] } }),
|
|
{
|
|
foo: "fallback-foo-pref-value",
|
|
bar: null,
|
|
baz: "fallback-baz-pref-value",
|
|
waldo: ["default-value"],
|
|
},
|
|
"getAllVariables() returns only values from prefs and defaults"
|
|
);
|
|
|
|
Assert.equal(
|
|
FEATURE.getVariable("foo"),
|
|
"fallback-foo-pref-value",
|
|
"variable foo returned from prefs"
|
|
);
|
|
Assert.equal(
|
|
FEATURE.getVariable("bar"),
|
|
undefined,
|
|
"variable bar returned from rollout"
|
|
);
|
|
Assert.equal(
|
|
FEATURE.getVariable("baz"),
|
|
"fallback-baz-pref-value",
|
|
"variable baz returned from prefs"
|
|
);
|
|
|
|
Services.prefs.clearUserPref(FEATURE.manifest.variables.foo.fallbackPref);
|
|
Services.prefs.clearUserPref(FEATURE.manifest.variables.baz.fallbackPref);
|
|
|
|
await cleanup();
|
|
});
|
|
|
|
add_task(async function test_getVariables_fallback_unenroll() {
|
|
const { manager, cleanup } = await setupTest();
|
|
|
|
Services.prefs.setStringPref(
|
|
FEATURE.manifest.variables.foo.fallbackPref,
|
|
"fallback-foo-pref-value"
|
|
);
|
|
Services.prefs.setStringPref(
|
|
FEATURE.manifest.variables.bar.fallbackPref,
|
|
`"fallback-bar-pref-value"`
|
|
);
|
|
Services.prefs.setStringPref(
|
|
FEATURE.manifest.variables.baz.fallbackPref,
|
|
"fallback-baz-pref-value"
|
|
);
|
|
Services.prefs.setStringPref(
|
|
FEATURE.manifest.variables.waldo.fallbackPref,
|
|
JSON.stringify(["fallback-waldo-pref-value"])
|
|
);
|
|
|
|
const recipes = [
|
|
NimbusTestUtils.factories.recipe.withFeatureConfig(
|
|
"experiment",
|
|
{ featureId: FEATURE_ID, value: { foo: DEEPLY_NESTED_VALUE.foo } },
|
|
{ localizations: {} }
|
|
),
|
|
|
|
NimbusTestUtils.factories.recipe.withFeatureConfig(
|
|
"rollout",
|
|
{ featureId: FEATURE_ID, value: { bar: DEEPLY_NESTED_VALUE.bar } },
|
|
{ isRollout: true, localizations: { "en-US": {} } }
|
|
),
|
|
];
|
|
|
|
for (const recipe of recipes) {
|
|
await NimbusTestUtils.enroll(recipe);
|
|
}
|
|
|
|
Assert.deepEqual(FEATURE.getAllVariables(), {
|
|
foo: "fallback-foo-pref-value",
|
|
bar: "fallback-bar-pref-value",
|
|
baz: "fallback-baz-pref-value",
|
|
waldo: ["fallback-waldo-pref-value"],
|
|
});
|
|
|
|
await NimbusTestUtils.waitForInactiveEnrollment("experiment");
|
|
await NimbusTestUtils.waitForInactiveEnrollment("rollout");
|
|
|
|
Assert.equal(
|
|
manager.store.getExperimentForFeature(FEATURE_ID),
|
|
null,
|
|
"Experiment should be unenrolled"
|
|
);
|
|
|
|
Assert.equal(
|
|
manager.store.getRolloutForFeature(FEATURE_ID),
|
|
null,
|
|
"Rollout should be unenrolled"
|
|
);
|
|
|
|
const gleanEvents = Glean.nimbusEvents.unenrollment.testGetValue("events");
|
|
Assert.equal(gleanEvents.length, 2, "Should be two unenrollment events");
|
|
Assert.equal(
|
|
gleanEvents[0].extra.reason,
|
|
"l10n-missing-locale",
|
|
"Reason should match"
|
|
);
|
|
Assert.equal(
|
|
gleanEvents[0].extra.experiment,
|
|
"experiment",
|
|
"Slug should match"
|
|
);
|
|
Assert.equal(
|
|
gleanEvents[1].extra.reason,
|
|
"l10n-missing-entry",
|
|
"Reason should match"
|
|
);
|
|
Assert.equal(gleanEvents[1].extra.experiment, "rollout", "Slug should match");
|
|
|
|
TelemetryTestUtils.assertEvents(
|
|
[
|
|
{
|
|
value: "experiment",
|
|
extra: { reason: "l10n-missing-locale" },
|
|
},
|
|
{
|
|
value: "rollout",
|
|
extra: { reason: "l10n-missing-entry" },
|
|
},
|
|
],
|
|
{
|
|
category: "normandy",
|
|
method: "unenroll",
|
|
object: "nimbus_experiment",
|
|
}
|
|
);
|
|
|
|
Services.prefs.clearUserPref(FEATURE.manifest.variables.foo.fallbackPref);
|
|
Services.prefs.clearUserPref(FEATURE.manifest.variables.bar.fallbackPref);
|
|
Services.prefs.clearUserPref(FEATURE.manifest.variables.baz.fallbackPref);
|
|
Services.prefs.clearUserPref(FEATURE.manifest.variables.waldo.fallbackPref);
|
|
|
|
await cleanup();
|
|
});
|
|
|
|
add_task(async function test_updateRecipes() {
|
|
const { sandbox, loader, manager, cleanup } = await setupTest();
|
|
|
|
sandbox.stub(manager, "onRecipe");
|
|
|
|
const recipe = NimbusTestUtils.factories.recipe.withFeatureConfig(
|
|
"foo",
|
|
{ featureId: FEATURE_ID, value: DEEPLY_NESTED_VALUE },
|
|
{ localizations: LOCALIZATIONS }
|
|
);
|
|
|
|
loader.remoteSettingsClients.experiments.get.resolves([recipe]);
|
|
await loader.updateRecipes();
|
|
|
|
Assert.ok(
|
|
manager.onRecipe.calledOnceWith(recipe, "rs-loader", {
|
|
ok: true,
|
|
status: MatchStatus.TARGETING_AND_BUCKETING,
|
|
}),
|
|
"would enroll"
|
|
);
|
|
|
|
await cleanup();
|
|
});
|
|
|
|
async function test_updateRecipes_missingLocale({
|
|
featureValidationOptOut = false,
|
|
} = {}) {
|
|
const { sandbox, loader, manager, cleanup } = await setupTest();
|
|
|
|
const recipe = NimbusTestUtils.factories.recipe.withFeatureConfig(
|
|
"foo",
|
|
{ featureId: FEATURE_ID, value: DEEPLY_NESTED_VALUE },
|
|
{ localizations: {}, featureValidationOptOut }
|
|
);
|
|
|
|
sandbox.spy(manager, "onRecipe");
|
|
sandbox.stub(manager, "enroll");
|
|
loader.remoteSettingsClients.experiments.get.resolves([recipe]);
|
|
|
|
await loader.updateRecipes();
|
|
|
|
Assert.ok(
|
|
manager.onRecipe.calledOnceWith(recipe, "rs-loader", {
|
|
ok: false,
|
|
reason: "l10n-missing-locale",
|
|
locale: "en-US",
|
|
}),
|
|
"Called onRecipe with missing locale"
|
|
);
|
|
Assert.ok(manager.enroll.notCalled, "Did not enroll");
|
|
|
|
const gleanEvents =
|
|
Glean.nimbusEvents.validationFailed.testGetValue("events");
|
|
Assert.equal(gleanEvents.length, 1, "Should be one validationFailed event");
|
|
Assert.equal(
|
|
gleanEvents[0].extra.experiment,
|
|
"foo",
|
|
"Experiment slug should match"
|
|
);
|
|
Assert.equal(
|
|
gleanEvents[0].extra.reason,
|
|
"l10n-missing-locale",
|
|
"Reason should match"
|
|
);
|
|
Assert.equal(gleanEvents[0].extra.locale, "en-US", "Locale should match");
|
|
|
|
TelemetryTestUtils.assertEvents(
|
|
[
|
|
{
|
|
value: "foo",
|
|
},
|
|
],
|
|
{
|
|
category: "normandy",
|
|
method: "validationFailed",
|
|
object: "nimbus_experiment",
|
|
}
|
|
);
|
|
|
|
await cleanup();
|
|
}
|
|
|
|
add_task(test_updateRecipes_missingLocale);
|
|
|
|
add_task(async function test_updateRecipes_missingEntry() {
|
|
const { sandbox, loader, manager, cleanup } = await setupTest();
|
|
|
|
const recipe = NimbusTestUtils.factories.recipe.withFeatureConfig(
|
|
"foo",
|
|
{ featureId: FEATURE_ID, value: DEEPLY_NESTED_VALUE },
|
|
{ localizations: { "en-US": {} } }
|
|
);
|
|
|
|
sandbox.spy(manager, "onRecipe");
|
|
sandbox.stub(manager, "enroll");
|
|
loader.remoteSettingsClients.experiments.get.resolves([recipe]);
|
|
|
|
await loader.updateRecipes();
|
|
|
|
Assert.ok(
|
|
manager.onRecipe.calledOnceWith(
|
|
recipe,
|
|
"rs-loader",
|
|
sinon.match({
|
|
ok: false,
|
|
reason: "l10n-missing-entry",
|
|
locale: "en-US",
|
|
missingL10nIds: sinon.match.array.contains([
|
|
"foo",
|
|
"qux",
|
|
"grault",
|
|
"waldo",
|
|
]),
|
|
})
|
|
),
|
|
"Called onRecipe with missing l10n ids"
|
|
);
|
|
Assert.ok(manager.enroll.notCalled, "Did not enroll");
|
|
|
|
const gleanEvents =
|
|
Glean.nimbusEvents.validationFailed.testGetValue("events");
|
|
Assert.equal(gleanEvents.length, 1, "Should be one validationFailed event");
|
|
Assert.equal(
|
|
gleanEvents[0].extra.experiment,
|
|
"foo",
|
|
"Experiment slug should match"
|
|
);
|
|
Assert.equal(
|
|
gleanEvents[0].extra.reason,
|
|
"l10n-missing-entry",
|
|
"Reason should match"
|
|
);
|
|
Assert.equal(
|
|
gleanEvents[0].extra.l10n_ids,
|
|
"foo,qux,grault,waldo",
|
|
"Missing IDs should match"
|
|
);
|
|
Assert.equal(gleanEvents[0].extra.locale, "en-US", "Locale should match");
|
|
|
|
TelemetryTestUtils.assertEvents(
|
|
[
|
|
{
|
|
value: "foo",
|
|
extra: {
|
|
reason: "l10n-missing-entry",
|
|
locale: "en-US",
|
|
l10n_ids: "foo,qux,grault,waldo",
|
|
},
|
|
},
|
|
],
|
|
{
|
|
category: "normandy",
|
|
method: "validationFailed",
|
|
object: "nimbus_experiment",
|
|
}
|
|
);
|
|
|
|
await cleanup();
|
|
});
|
|
|
|
add_task(async function test_updateRecipes_validationDisabled_pref() {
|
|
Services.prefs.setBoolPref("nimbus.validation.enabled", false);
|
|
|
|
await test_updateRecipes_missingLocale();
|
|
|
|
Services.prefs.clearUserPref("nimbus.validation.enabled");
|
|
});
|
|
|
|
add_task(async function test_updateRecipes_validationDisabled_flag() {
|
|
await test_updateRecipes_missingLocale({ featureValidationOptOut: true });
|
|
});
|
|
|
|
add_task(async function test_updateRecipes_unenroll_missingEntry() {
|
|
const { sandbox, loader, manager, cleanup } = await setupTest();
|
|
|
|
const recipe = NimbusTestUtils.factories.recipe.withFeatureConfig(
|
|
"foo",
|
|
{ featureId: FEATURE_ID, value: DEEPLY_NESTED_VALUE },
|
|
{ localizations: LOCALIZATIONS }
|
|
);
|
|
|
|
const badRecipe = { ...recipe, localizations: { "en-US": {} } };
|
|
|
|
sandbox.spy(manager, "updateEnrollment");
|
|
sandbox.spy(manager, "_unenroll");
|
|
loader.remoteSettingsClients.experiments.get.resolves([badRecipe]);
|
|
|
|
await manager.enroll(recipe, "rs-loader");
|
|
Assert.ok(
|
|
!!manager.store.getExperimentForFeature(FEATURE_ID),
|
|
"Should be enrolled in the experiment"
|
|
);
|
|
|
|
await loader.updateRecipes();
|
|
|
|
Assert.ok(
|
|
manager.updateEnrollment.calledOnceWith(
|
|
sinon.match({ slug: recipe.slug }),
|
|
badRecipe,
|
|
"rs-loader",
|
|
sinon.match({
|
|
ok: false,
|
|
reason: "l10n-missing-entry",
|
|
locale: "en-US",
|
|
missingL10nIds: sinon.match.array.contains([
|
|
"foo",
|
|
"qux",
|
|
"grault",
|
|
"waldo",
|
|
]),
|
|
})
|
|
),
|
|
"Should call .onRecipe with the missing l10n entries"
|
|
);
|
|
|
|
Assert.ok(
|
|
manager._unenroll.calledOnceWith(sinon.match({ slug: recipe.slug }), {
|
|
reason: "l10n-missing-entry",
|
|
})
|
|
);
|
|
|
|
Assert.equal(
|
|
manager.store.getExperimentForFeature(FEATURE_ID),
|
|
null,
|
|
"Should no longer be enrolled in the experiment"
|
|
);
|
|
|
|
const unenrollEvents = Glean.nimbusEvents.unenrollment.testGetValue("events");
|
|
Assert.equal(unenrollEvents.length, 1, "Should be one unenroll event");
|
|
Assert.equal(
|
|
unenrollEvents[0].extra.experiment,
|
|
"foo",
|
|
"Experiment slug should match"
|
|
);
|
|
Assert.equal(
|
|
unenrollEvents[0].extra.reason,
|
|
"l10n-missing-entry",
|
|
"Reason should match"
|
|
);
|
|
|
|
const validationFailedEvents =
|
|
Glean.nimbusEvents.validationFailed.testGetValue("events");
|
|
Assert.equal(
|
|
validationFailedEvents.length,
|
|
1,
|
|
"Should be one validation failed event"
|
|
);
|
|
Assert.equal(
|
|
validationFailedEvents[0].extra.experiment,
|
|
"foo",
|
|
"Experiment slug should match"
|
|
);
|
|
Assert.equal(
|
|
validationFailedEvents[0].extra.reason,
|
|
"l10n-missing-entry",
|
|
"Reason should match"
|
|
);
|
|
Assert.equal(
|
|
validationFailedEvents[0].extra.l10n_ids,
|
|
"foo,qux,grault,waldo",
|
|
"Missing IDs should match"
|
|
);
|
|
Assert.equal(
|
|
validationFailedEvents[0].extra.locale,
|
|
"en-US",
|
|
"Locale should match"
|
|
);
|
|
|
|
TelemetryTestUtils.assertEvents(
|
|
[
|
|
{
|
|
value: "foo",
|
|
extra: {
|
|
reason: "l10n-missing-entry",
|
|
},
|
|
},
|
|
],
|
|
{
|
|
category: "normandy",
|
|
method: "unenroll",
|
|
object: "nimbus_experiment",
|
|
},
|
|
{ clear: false }
|
|
);
|
|
|
|
TelemetryTestUtils.assertEvents(
|
|
[
|
|
{
|
|
value: "foo",
|
|
extra: {
|
|
reason: "l10n-missing-entry",
|
|
l10n_ids: "foo,qux,grault,waldo",
|
|
locale: "en-US",
|
|
},
|
|
},
|
|
],
|
|
{
|
|
category: "normandy",
|
|
method: "validationFailed",
|
|
object: "nimbus_experiment",
|
|
}
|
|
);
|
|
|
|
await cleanup();
|
|
});
|
|
|
|
add_task(async function test_updateRecipes_unenroll_missingLocale() {
|
|
const { sandbox, manager, loader, cleanup } = await setupTest();
|
|
|
|
const recipe = NimbusTestUtils.factories.recipe.withFeatureConfig(
|
|
"foo",
|
|
{
|
|
featureId: FEATURE_ID,
|
|
value: DEEPLY_NESTED_VALUE,
|
|
},
|
|
{
|
|
localizations: LOCALIZATIONS,
|
|
}
|
|
);
|
|
|
|
const badRecipe = { ...recipe, localizations: {} };
|
|
|
|
sandbox.spy(manager, "updateEnrollment");
|
|
sandbox.spy(manager, "_unenroll");
|
|
loader.remoteSettingsClients.experiments.get.resolves([badRecipe]);
|
|
|
|
await manager.enroll(recipe, "rs-loader");
|
|
Assert.ok(
|
|
!!manager.store.getExperimentForFeature(FEATURE_ID),
|
|
"Should be enrolled in the experiment"
|
|
);
|
|
|
|
await loader.updateRecipes();
|
|
|
|
Assert.ok(
|
|
manager.updateEnrollment.calledOnceWith(
|
|
sinon.match({ slug: recipe.slug }),
|
|
badRecipe,
|
|
"rs-loader",
|
|
{
|
|
ok: false,
|
|
reason: "l10n-missing-locale",
|
|
locale: "en-US",
|
|
}
|
|
),
|
|
"Should call .onFinal with missing-locale"
|
|
);
|
|
|
|
Assert.ok(
|
|
manager._unenroll.calledWith(sinon.match({ slug: recipe.slug }), {
|
|
reason: "l10n-missing-locale",
|
|
})
|
|
);
|
|
|
|
Assert.equal(
|
|
manager.store.getExperimentForFeature(FEATURE_ID),
|
|
null,
|
|
"Should no longer be enrolled in the experiment"
|
|
);
|
|
|
|
const unenrollEvents = Glean.nimbusEvents.unenrollment.testGetValue("events");
|
|
Assert.equal(unenrollEvents.length, 1, "Should be one unenroll event");
|
|
Assert.equal(
|
|
unenrollEvents[0].extra.experiment,
|
|
"foo",
|
|
"Experiment slug should match"
|
|
);
|
|
Assert.equal(
|
|
unenrollEvents[0].extra.reason,
|
|
"l10n-missing-locale",
|
|
"Reason should match"
|
|
);
|
|
|
|
const validationFailedEvents =
|
|
Glean.nimbusEvents.validationFailed.testGetValue("events");
|
|
Assert.equal(
|
|
validationFailedEvents.length,
|
|
1,
|
|
"Should be one validation failed event"
|
|
);
|
|
Assert.equal(
|
|
validationFailedEvents[0].extra.experiment,
|
|
"foo",
|
|
"Experiment slug should match"
|
|
);
|
|
Assert.equal(
|
|
validationFailedEvents[0].extra.reason,
|
|
"l10n-missing-locale",
|
|
"Reason should match"
|
|
);
|
|
Assert.equal(
|
|
validationFailedEvents[0].extra.locale,
|
|
"en-US",
|
|
"Locale should match"
|
|
);
|
|
|
|
TelemetryTestUtils.assertEvents(
|
|
[
|
|
{
|
|
value: "foo",
|
|
extra: {
|
|
reason: "l10n-missing-locale",
|
|
},
|
|
},
|
|
],
|
|
{
|
|
category: "normandy",
|
|
method: "unenroll",
|
|
object: "nimbus_experiment",
|
|
},
|
|
{ clear: false }
|
|
);
|
|
|
|
TelemetryTestUtils.assertEvents(
|
|
[
|
|
{
|
|
value: "foo",
|
|
extra: {
|
|
reason: "l10n-missing-locale",
|
|
locale: "en-US",
|
|
},
|
|
},
|
|
],
|
|
{
|
|
category: "normandy",
|
|
method: "validationFailed",
|
|
object: "nimbus_experiment",
|
|
}
|
|
);
|
|
|
|
await cleanup();
|
|
});
|
|
|
|
add_task(async function testCoenrolling() {
|
|
const { manager, cleanup } = await setupTest();
|
|
|
|
const featureId = "coenrolling-feature";
|
|
|
|
const cleanupFeature = NimbusTestUtils.addTestFeatures(
|
|
new ExperimentFeature(featureId, {
|
|
allowCoenrollment: true,
|
|
isEarlyStartup: false,
|
|
variables: {
|
|
foo: {
|
|
type: "string",
|
|
fallbackPref: `${TEST_PREF_BRANCH}.coenrolling.foo`,
|
|
},
|
|
bar: {
|
|
type: "json",
|
|
},
|
|
baz: {
|
|
type: "string",
|
|
},
|
|
waldo: {
|
|
type: "json",
|
|
},
|
|
},
|
|
})
|
|
);
|
|
|
|
Services.prefs.setStringPref(
|
|
NimbusFeatures["coenrolling-feature"].getFallbackPrefName("foo"),
|
|
"fallback-foo-pref-value"
|
|
);
|
|
|
|
await manager.enroll(
|
|
NimbusTestUtils.factories.recipe.withFeatureConfig("experiment-1", {
|
|
branchSlug: "treatment-a",
|
|
featureId,
|
|
value: {
|
|
foo: "foo",
|
|
bar: "bar",
|
|
baz: "baz",
|
|
waldo: "waldo",
|
|
},
|
|
}),
|
|
"test"
|
|
);
|
|
|
|
await manager.enroll(
|
|
NimbusTestUtils.factories.recipe.withFeatureConfig(
|
|
"experiment-2",
|
|
{
|
|
branchSlug: "treatment-b",
|
|
featureId,
|
|
value: {
|
|
foo: {
|
|
$l10n: {
|
|
id: "foo",
|
|
comment: "foo comment",
|
|
text: "original foo",
|
|
},
|
|
},
|
|
bar: "bar",
|
|
baz: "baz",
|
|
waldo: "waldo",
|
|
},
|
|
},
|
|
{
|
|
localizations: LOCALIZATIONS,
|
|
}
|
|
),
|
|
"test"
|
|
);
|
|
|
|
await manager.enroll(
|
|
NimbusTestUtils.factories.recipe.withFeatureConfig(
|
|
"rollout-1",
|
|
{
|
|
featureId,
|
|
value: {
|
|
bar: DEEPLY_NESTED_VALUE.bar,
|
|
},
|
|
},
|
|
{
|
|
localizations: LOCALIZATIONS,
|
|
}
|
|
),
|
|
"test"
|
|
);
|
|
|
|
await manager.enroll(
|
|
NimbusTestUtils.factories.recipe.withFeatureConfig(
|
|
"rollout-2",
|
|
{
|
|
featureId,
|
|
value: DEEPLY_NESTED_VALUE,
|
|
},
|
|
{
|
|
localizations: LOCALIZATIONS,
|
|
}
|
|
),
|
|
"test"
|
|
);
|
|
|
|
const enrollments = NimbusFeatures[featureId]
|
|
.getAllEnrollments()
|
|
.sort((a, b) => a.meta.slug.localeCompare(b.meta.slug));
|
|
|
|
Assert.deepEqual(enrollments, [
|
|
{
|
|
meta: {
|
|
slug: "experiment-1",
|
|
branch: "treatment-a",
|
|
isRollout: false,
|
|
},
|
|
value: {
|
|
foo: "foo",
|
|
bar: "bar",
|
|
baz: "baz",
|
|
waldo: "waldo",
|
|
},
|
|
},
|
|
{
|
|
meta: {
|
|
slug: "experiment-2",
|
|
branch: "treatment-b",
|
|
isRollout: false,
|
|
},
|
|
value: {
|
|
foo: LOCALIZED_DEEPLY_NESTED_VALUE.foo,
|
|
bar: "bar",
|
|
baz: "baz",
|
|
waldo: "waldo",
|
|
},
|
|
},
|
|
{
|
|
meta: {
|
|
slug: "rollout-1",
|
|
branch: "control",
|
|
isRollout: false,
|
|
},
|
|
value: {
|
|
foo: "fallback-foo-pref-value",
|
|
bar: LOCALIZED_DEEPLY_NESTED_VALUE.bar,
|
|
},
|
|
},
|
|
{
|
|
meta: {
|
|
slug: "rollout-2",
|
|
branch: "control",
|
|
isRollout: false,
|
|
},
|
|
value: LOCALIZED_DEEPLY_NESTED_VALUE,
|
|
},
|
|
]);
|
|
|
|
await NimbusTestUtils.cleanupManager([
|
|
"experiment-1",
|
|
"experiment-2",
|
|
"rollout-1",
|
|
"rollout-2",
|
|
]);
|
|
|
|
Services.prefs.clearUserPref(
|
|
NimbusFeatures["coenrolling-feature"].getFallbackPrefName("foo")
|
|
);
|
|
|
|
cleanupFeature();
|
|
await cleanup();
|
|
});
|