mirror of
https://github.com/mozilla/gecko-dev.git
synced 2025-11-06 19:29:24 +02:00
578 lines
16 KiB
JavaScript
578 lines
16 KiB
JavaScript
"use strict";
|
|
|
|
const { ExperimentAPI } = ChromeUtils.import(
|
|
"resource://nimbus/ExperimentAPI.jsm"
|
|
);
|
|
const { ExperimentFakes } = ChromeUtils.import(
|
|
"resource://testing-common/NimbusTestUtils.jsm"
|
|
);
|
|
const { TestUtils } = ChromeUtils.importESModule(
|
|
"resource://testing-common/TestUtils.sys.mjs"
|
|
);
|
|
const COLLECTION_ID_PREF = "messaging-system.rsexperimentloader.collection_id";
|
|
|
|
/**
|
|
* #getExperiment
|
|
*/
|
|
add_task(async function test_getExperiment_fromChild_slug() {
|
|
const sandbox = sinon.createSandbox();
|
|
const manager = ExperimentFakes.manager();
|
|
const expected = ExperimentFakes.experiment("foo");
|
|
|
|
await manager.onStartup();
|
|
|
|
sandbox.stub(ExperimentAPI, "_store").get(() => ExperimentFakes.childStore());
|
|
|
|
await manager.store.addEnrollment(expected);
|
|
|
|
// Wait to sync to child
|
|
await TestUtils.waitForCondition(
|
|
() => ExperimentAPI.getExperiment({ slug: "foo" }),
|
|
"Wait for child to sync"
|
|
);
|
|
|
|
Assert.equal(
|
|
ExperimentAPI.getExperiment({ slug: "foo" }).slug,
|
|
expected.slug,
|
|
"should return an experiment by slug"
|
|
);
|
|
|
|
Assert.deepEqual(
|
|
ExperimentAPI.getExperiment({ slug: "foo" }).branch,
|
|
expected.branch,
|
|
"should return the right branch by slug"
|
|
);
|
|
|
|
sandbox.restore();
|
|
});
|
|
|
|
add_task(async function test_getExperiment_fromParent_slug() {
|
|
const sandbox = sinon.createSandbox();
|
|
const manager = ExperimentFakes.manager();
|
|
const expected = ExperimentFakes.experiment("foo");
|
|
|
|
await manager.onStartup();
|
|
sandbox.stub(ExperimentAPI, "_store").get(() => manager.store);
|
|
await ExperimentAPI.ready();
|
|
|
|
await manager.store.addEnrollment(expected);
|
|
|
|
Assert.equal(
|
|
ExperimentAPI.getExperiment({ slug: "foo" }).slug,
|
|
expected.slug,
|
|
"should return an experiment by slug"
|
|
);
|
|
|
|
sandbox.restore();
|
|
});
|
|
|
|
add_task(async function test_getExperimentMetaData() {
|
|
const sandbox = sinon.createSandbox();
|
|
const manager = ExperimentFakes.manager();
|
|
const expected = ExperimentFakes.experiment("foo");
|
|
let exposureStub = sandbox.stub(ExperimentAPI, "recordExposureEvent");
|
|
|
|
await manager.onStartup();
|
|
sandbox.stub(ExperimentAPI, "_store").get(() => manager.store);
|
|
await ExperimentAPI.ready();
|
|
|
|
await manager.store.addEnrollment(expected);
|
|
|
|
let metadata = ExperimentAPI.getExperimentMetaData({ slug: expected.slug });
|
|
|
|
Assert.equal(
|
|
Object.keys(metadata.branch).length,
|
|
1,
|
|
"Should only expose one property"
|
|
);
|
|
Assert.equal(
|
|
metadata.branch.slug,
|
|
expected.branch.slug,
|
|
"Should have the slug prop"
|
|
);
|
|
|
|
Assert.ok(exposureStub.notCalled, "Not called for this method");
|
|
|
|
sandbox.restore();
|
|
});
|
|
|
|
add_task(async function test_getRolloutMetaData() {
|
|
const sandbox = sinon.createSandbox();
|
|
const manager = ExperimentFakes.manager();
|
|
const expected = ExperimentFakes.rollout("foo");
|
|
let exposureStub = sandbox.stub(ExperimentAPI, "recordExposureEvent");
|
|
|
|
await manager.onStartup();
|
|
sandbox.stub(ExperimentAPI, "_store").get(() => manager.store);
|
|
await ExperimentAPI.ready();
|
|
|
|
await manager.store.addEnrollment(expected);
|
|
|
|
let metadata = ExperimentAPI.getExperimentMetaData({ slug: expected.slug });
|
|
|
|
Assert.equal(
|
|
Object.keys(metadata.branch).length,
|
|
1,
|
|
"Should only expose one property"
|
|
);
|
|
Assert.equal(
|
|
metadata.branch.slug,
|
|
expected.branch.slug,
|
|
"Should have the slug prop"
|
|
);
|
|
|
|
Assert.ok(exposureStub.notCalled, "Not called for this method");
|
|
|
|
sandbox.restore();
|
|
});
|
|
|
|
add_task(function test_getExperimentMetaData_safe() {
|
|
const sandbox = sinon.createSandbox();
|
|
let exposureStub = sandbox.stub(ExperimentAPI, "recordExposureEvent");
|
|
|
|
sandbox.stub(ExperimentAPI._store, "get").throws();
|
|
sandbox.stub(ExperimentAPI._store, "getExperimentForFeature").throws();
|
|
|
|
try {
|
|
let metadata = ExperimentAPI.getExperimentMetaData({ slug: "foo" });
|
|
Assert.equal(metadata, null, "Should not throw");
|
|
} catch (e) {
|
|
Assert.ok(false, "Error should be caught in ExperimentAPI");
|
|
}
|
|
|
|
Assert.ok(ExperimentAPI._store.get.calledOnce, "Sanity check");
|
|
|
|
try {
|
|
let metadata = ExperimentAPI.getExperimentMetaData({ featureId: "foo" });
|
|
Assert.equal(metadata, null, "Should not throw");
|
|
} catch (e) {
|
|
Assert.ok(false, "Error should be caught in ExperimentAPI");
|
|
}
|
|
|
|
Assert.ok(
|
|
ExperimentAPI._store.getExperimentForFeature.calledOnce,
|
|
"Sanity check"
|
|
);
|
|
|
|
Assert.ok(exposureStub.notCalled, "Not called for this feature");
|
|
|
|
sandbox.restore();
|
|
});
|
|
|
|
add_task(async function test_getExperiment_feature() {
|
|
const sandbox = sinon.createSandbox();
|
|
const manager = ExperimentFakes.manager();
|
|
const expected = ExperimentFakes.experiment("foo", {
|
|
branch: {
|
|
slug: "treatment",
|
|
features: [{ featureId: "cfr", value: null }],
|
|
feature: {
|
|
featureId: "unused-feature-id-for-legacy-support",
|
|
enabled: false,
|
|
value: {},
|
|
},
|
|
},
|
|
});
|
|
|
|
await manager.onStartup();
|
|
|
|
sandbox.stub(ExperimentAPI, "_store").get(() => ExperimentFakes.childStore());
|
|
let exposureStub = sandbox.stub(ExperimentAPI, "recordExposureEvent");
|
|
|
|
await manager.store.addEnrollment(expected);
|
|
|
|
// Wait to sync to child
|
|
await TestUtils.waitForCondition(
|
|
() => ExperimentAPI.getExperiment({ featureId: "cfr" }),
|
|
"Wait for child to sync"
|
|
);
|
|
|
|
Assert.equal(
|
|
ExperimentAPI.getExperiment({ featureId: "cfr" }).slug,
|
|
expected.slug,
|
|
"should return an experiment by featureId"
|
|
);
|
|
|
|
Assert.deepEqual(
|
|
ExperimentAPI.getExperiment({ featureId: "cfr" }).branch,
|
|
expected.branch,
|
|
"should return the right branch by featureId"
|
|
);
|
|
|
|
Assert.ok(exposureStub.notCalled, "Not called by default");
|
|
|
|
sandbox.restore();
|
|
});
|
|
|
|
add_task(async function test_getExperiment_safe() {
|
|
const sandbox = sinon.createSandbox();
|
|
sandbox.stub(ExperimentAPI._store, "getExperimentForFeature").throws();
|
|
|
|
try {
|
|
Assert.equal(
|
|
ExperimentAPI.getExperiment({ featureId: "foo" }),
|
|
null,
|
|
"It should not fail even when it throws."
|
|
);
|
|
} catch (e) {
|
|
Assert.ok(false, "Error should be caught by ExperimentAPI");
|
|
}
|
|
|
|
sandbox.restore();
|
|
});
|
|
|
|
add_task(async function test_getExperiment_featureAccess() {
|
|
const sandbox = sinon.createSandbox();
|
|
const expected = ExperimentFakes.experiment("foo", {
|
|
branch: {
|
|
slug: "treatment",
|
|
value: { title: "hi" },
|
|
features: [{ featureId: "cfr", value: { message: "content" } }],
|
|
},
|
|
});
|
|
const stub = sandbox
|
|
.stub(ExperimentAPI._store, "getExperimentForFeature")
|
|
.returns(expected);
|
|
|
|
let { branch } = ExperimentAPI.getExperiment({ featureId: "cfr" });
|
|
|
|
Assert.equal(branch.slug, "treatment");
|
|
let feature = branch.cfr;
|
|
Assert.ok(feature, "Should allow to access by featureId");
|
|
Assert.equal(feature.value.message, "content");
|
|
|
|
stub.restore();
|
|
});
|
|
|
|
add_task(async function test_getExperiment_featureAccess_backwardsCompat() {
|
|
const sandbox = sinon.createSandbox();
|
|
const expected = ExperimentFakes.experiment("foo", {
|
|
branch: {
|
|
slug: "treatment",
|
|
feature: { featureId: "cfr", value: { message: "content" } },
|
|
},
|
|
});
|
|
const stub = sandbox
|
|
.stub(ExperimentAPI._store, "getExperimentForFeature")
|
|
.returns(expected);
|
|
|
|
let { branch } = ExperimentAPI.getExperiment({ featureId: "cfr" });
|
|
|
|
Assert.equal(branch.slug, "treatment");
|
|
let feature = branch.cfr;
|
|
Assert.ok(feature, "Should allow to access by featureId");
|
|
Assert.equal(feature.value.message, "content");
|
|
|
|
stub.restore();
|
|
});
|
|
|
|
/**
|
|
* #getRecipe
|
|
*/
|
|
add_task(async function test_getRecipe() {
|
|
const sandbox = sinon.createSandbox();
|
|
const RECIPE = ExperimentFakes.recipe("foo");
|
|
const collectionName = Services.prefs.getStringPref(COLLECTION_ID_PREF);
|
|
sandbox.stub(ExperimentAPI._remoteSettingsClient, "get").resolves([RECIPE]);
|
|
|
|
const recipe = await ExperimentAPI.getRecipe("foo");
|
|
Assert.deepEqual(
|
|
recipe,
|
|
RECIPE,
|
|
"should return an experiment recipe if found"
|
|
);
|
|
Assert.equal(
|
|
ExperimentAPI._remoteSettingsClient.collectionName,
|
|
collectionName,
|
|
"Loaded the expected collection"
|
|
);
|
|
|
|
sandbox.restore();
|
|
});
|
|
|
|
add_task(async function test_getRecipe_Failure() {
|
|
const sandbox = sinon.createSandbox();
|
|
sandbox.stub(ExperimentAPI._remoteSettingsClient, "get").throws();
|
|
|
|
const recipe = await ExperimentAPI.getRecipe("foo");
|
|
Assert.equal(recipe, undefined, "should return undefined if RS throws");
|
|
|
|
sandbox.restore();
|
|
});
|
|
|
|
/**
|
|
* #getAllBranches
|
|
*/
|
|
add_task(async function test_getAllBranches() {
|
|
const sandbox = sinon.createSandbox();
|
|
const RECIPE = ExperimentFakes.recipe("foo");
|
|
sandbox.stub(ExperimentAPI._remoteSettingsClient, "get").resolves([RECIPE]);
|
|
|
|
const branches = await ExperimentAPI.getAllBranches("foo");
|
|
Assert.deepEqual(
|
|
branches,
|
|
RECIPE.branches,
|
|
"should return all branches if found a recipe"
|
|
);
|
|
|
|
sandbox.restore();
|
|
});
|
|
|
|
// API used by Messaging System
|
|
add_task(async function test_getAllBranches_featureIdAccessor() {
|
|
const sandbox = sinon.createSandbox();
|
|
const RECIPE = ExperimentFakes.recipe("foo");
|
|
sandbox.stub(ExperimentAPI._remoteSettingsClient, "get").resolves([RECIPE]);
|
|
|
|
const branches = await ExperimentAPI.getAllBranches("foo");
|
|
Assert.deepEqual(
|
|
branches,
|
|
RECIPE.branches,
|
|
"should return all branches if found a recipe"
|
|
);
|
|
branches.forEach(branch => {
|
|
Assert.equal(
|
|
branch.testFeature.featureId,
|
|
"testFeature",
|
|
"Should use the experimentBranchAccessor proxy getter"
|
|
);
|
|
});
|
|
|
|
sandbox.restore();
|
|
});
|
|
|
|
// For schema version before 1.6.2 branch.feature was accessed
|
|
// instead of branch.features
|
|
add_task(async function test_getAllBranches_backwardsCompat() {
|
|
const sandbox = sinon.createSandbox();
|
|
const RECIPE = ExperimentFakes.recipe("foo");
|
|
delete RECIPE.branches[0].features;
|
|
delete RECIPE.branches[1].features;
|
|
let feature = {
|
|
featureId: "backwardsCompat",
|
|
value: {
|
|
enabled: true,
|
|
},
|
|
};
|
|
RECIPE.branches[0].feature = feature;
|
|
RECIPE.branches[1].feature = feature;
|
|
sandbox.stub(ExperimentAPI._remoteSettingsClient, "get").resolves([RECIPE]);
|
|
|
|
const branches = await ExperimentAPI.getAllBranches("foo");
|
|
Assert.deepEqual(
|
|
branches,
|
|
RECIPE.branches,
|
|
"should return all branches if found a recipe"
|
|
);
|
|
branches.forEach(branch => {
|
|
Assert.equal(
|
|
branch.backwardsCompat.featureId,
|
|
"backwardsCompat",
|
|
"Should use the experimentBranchAccessor proxy getter"
|
|
);
|
|
});
|
|
|
|
sandbox.restore();
|
|
});
|
|
|
|
add_task(async function test_getAllBranches_Failure() {
|
|
const sandbox = sinon.createSandbox();
|
|
sandbox.stub(ExperimentAPI._remoteSettingsClient, "get").throws();
|
|
|
|
const branches = await ExperimentAPI.getAllBranches("foo");
|
|
Assert.equal(branches, undefined, "should return undefined if RS throws");
|
|
|
|
sandbox.restore();
|
|
});
|
|
|
|
/**
|
|
* #on
|
|
* #off
|
|
*/
|
|
add_task(async function test_addEnrollment_eventEmit_add() {
|
|
const sandbox = sinon.createSandbox();
|
|
const slugStub = sandbox.stub();
|
|
const featureStub = sandbox.stub();
|
|
const experiment = ExperimentFakes.experiment("foo", {
|
|
branch: {
|
|
slug: "variant",
|
|
features: [{ featureId: "purple", value: null }],
|
|
},
|
|
});
|
|
const store = ExperimentFakes.store();
|
|
sandbox.stub(ExperimentAPI, "_store").get(() => store);
|
|
|
|
await store.init();
|
|
await ExperimentAPI.ready();
|
|
|
|
ExperimentAPI.on("update", { slug: "foo" }, slugStub);
|
|
ExperimentAPI.on("update", { featureId: "purple" }, featureStub);
|
|
|
|
await store.addEnrollment(experiment);
|
|
|
|
Assert.equal(
|
|
slugStub.callCount,
|
|
1,
|
|
"should call 'update' callback for slug when experiment is added"
|
|
);
|
|
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.firstCall.args[1].slug, experiment.slug);
|
|
});
|
|
|
|
add_task(async function test_updateExperiment_eventEmit_add_and_update() {
|
|
const sandbox = sinon.createSandbox();
|
|
const slugStub = sandbox.stub();
|
|
const featureStub = sandbox.stub();
|
|
const experiment = ExperimentFakes.experiment("foo", {
|
|
branch: {
|
|
slug: "variant",
|
|
features: [{ featureId: "purple", value: null }],
|
|
},
|
|
});
|
|
const store = ExperimentFakes.store();
|
|
sandbox.stub(ExperimentAPI, "_store").get(() => store);
|
|
|
|
await store.init();
|
|
await ExperimentAPI.ready();
|
|
|
|
await store.addEnrollment(experiment);
|
|
|
|
ExperimentAPI.on("update", { slug: "foo" }, slugStub);
|
|
ExperimentAPI.on("update", { featureId: "purple" }, featureStub);
|
|
|
|
store.updateExperiment(experiment.slug, experiment);
|
|
|
|
await TestUtils.waitForCondition(
|
|
() => slugStub.callCount == 2,
|
|
"Wait for `on` method to notify callback about the `add` event."
|
|
);
|
|
// Called twice, once when attaching the event listener (because there is an
|
|
// existing experiment with that name) and 2nd time for the update event
|
|
Assert.equal(slugStub.firstCall.args[1].slug, experiment.slug);
|
|
Assert.equal(featureStub.callCount, 2, "Called twice for feature");
|
|
Assert.equal(featureStub.firstCall.args[1].slug, experiment.slug);
|
|
});
|
|
|
|
add_task(async function test_updateExperiment_eventEmit_off() {
|
|
const sandbox = sinon.createSandbox();
|
|
const slugStub = sandbox.stub();
|
|
const featureStub = sandbox.stub();
|
|
const experiment = ExperimentFakes.experiment("foo", {
|
|
branch: {
|
|
slug: "variant",
|
|
features: [{ featureId: "purple", value: null }],
|
|
},
|
|
});
|
|
const store = ExperimentFakes.store();
|
|
sandbox.stub(ExperimentAPI, "_store").get(() => store);
|
|
|
|
await store.init();
|
|
await ExperimentAPI.ready();
|
|
|
|
ExperimentAPI.on("update", { slug: "foo" }, slugStub);
|
|
ExperimentAPI.on("update", { featureId: "purple" }, featureStub);
|
|
|
|
await store.addEnrollment(experiment);
|
|
|
|
ExperimentAPI.off("update:foo", slugStub);
|
|
ExperimentAPI.off("update:purple", featureStub);
|
|
|
|
store.updateExperiment(experiment.slug, experiment);
|
|
|
|
Assert.equal(slugStub.callCount, 1, "Called only once before `off`");
|
|
Assert.equal(featureStub.callCount, 1, "Called only once before `off`");
|
|
});
|
|
|
|
add_task(async function test_getActiveBranch() {
|
|
const sandbox = sinon.createSandbox();
|
|
const store = ExperimentFakes.store();
|
|
sandbox.stub(ExperimentAPI, "_store").get(() => store);
|
|
const experiment = ExperimentFakes.experiment("foo", {
|
|
branch: {
|
|
slug: "variant",
|
|
features: [{ featureId: "green", value: null }],
|
|
},
|
|
});
|
|
|
|
await store.init();
|
|
await store.addEnrollment(experiment);
|
|
|
|
Assert.deepEqual(
|
|
ExperimentAPI.getActiveBranch({ featureId: "green" }),
|
|
experiment.branch,
|
|
"Should return feature of active experiment"
|
|
);
|
|
|
|
sandbox.restore();
|
|
});
|
|
|
|
add_task(async function test_getActiveBranch_safe() {
|
|
const sandbox = sinon.createSandbox();
|
|
sandbox.stub(ExperimentAPI._store, "getAllActiveExperiments").throws();
|
|
|
|
try {
|
|
Assert.equal(
|
|
ExperimentAPI.getActiveBranch({ featureId: "green" }),
|
|
null,
|
|
"Should not throw"
|
|
);
|
|
} catch (e) {
|
|
Assert.ok(false, "Should catch error in ExperimentAPI");
|
|
}
|
|
|
|
sandbox.restore();
|
|
});
|
|
|
|
add_task(async function test_getActiveBranch_storeFailure() {
|
|
const store = ExperimentFakes.store();
|
|
const sandbox = sinon.createSandbox();
|
|
sandbox.stub(ExperimentAPI, "_store").get(() => store);
|
|
const experiment = ExperimentFakes.experiment("foo", {
|
|
branch: {
|
|
slug: "variant",
|
|
features: [{ featureId: "green" }],
|
|
},
|
|
});
|
|
|
|
await store.init();
|
|
await store.addEnrollment(experiment);
|
|
// Adding stub later because `addEnrollment` emits update events
|
|
const stub = sandbox.stub(store, "emit");
|
|
// Call getActiveBranch to trigger an activation event
|
|
sandbox.stub(store, "getAllActiveExperiments").throws();
|
|
try {
|
|
ExperimentAPI.getActiveBranch({ featureId: "green" });
|
|
} catch (e) {
|
|
/* This is expected */
|
|
}
|
|
|
|
Assert.equal(stub.callCount, 0, "Not called if store somehow fails");
|
|
sandbox.restore();
|
|
});
|
|
|
|
add_task(async function test_getActiveBranch_noActivationEvent() {
|
|
const store = ExperimentFakes.store();
|
|
const sandbox = sinon.createSandbox();
|
|
sandbox.stub(ExperimentAPI, "_store").get(() => store);
|
|
const experiment = ExperimentFakes.experiment("foo", {
|
|
branch: {
|
|
slug: "variant",
|
|
features: [{ featureId: "green" }],
|
|
},
|
|
});
|
|
|
|
await store.init();
|
|
await store.addEnrollment(experiment);
|
|
// Adding stub later because `addEnrollment` emits update events
|
|
const stub = sandbox.stub(store, "emit");
|
|
// Call getActiveBranch to trigger an activation event
|
|
ExperimentAPI.getActiveBranch({ featureId: "green" });
|
|
|
|
Assert.equal(stub.callCount, 0, "Not called: sendExposureEvent is false");
|
|
sandbox.restore();
|
|
});
|