gecko-dev/toolkit/components/nimbus/test/unit/test_ExperimentAPI.js

486 lines
14 KiB
JavaScript

"use strict";
const { ExperimentAPI, ExperimentFeature } = ChromeUtils.import(
"resource://nimbus/ExperimentAPI.jsm"
);
const { ExperimentFakes } = ChromeUtils.import(
"resource://testing-common/NimbusTestUtils.jsm"
);
const { TestUtils } = ChromeUtils.import(
"resource://testing-common/TestUtils.jsm"
);
const { TelemetryTestUtils } = ChromeUtils.import(
"resource://testing-common/TelemetryTestUtils.jsm"
);
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());
manager.store.addExperiment(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();
manager.store.addExperiment(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();
manager.store.addExperiment(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",
value: { title: "hi" },
feature: { featureId: "cfr", enabled: true },
},
});
await manager.onStartup();
sandbox.stub(ExperimentAPI, "_store").get(() => ExperimentFakes.childStore());
let exposureStub = sandbox.stub(ExperimentAPI, "recordExposureEvent");
manager.store.addExperiment(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");
ExperimentAPI.getExperiment({ featureId: "cfr", sendExposureEvent: true });
Assert.ok(exposureStub.calledOnce, "Called explicitly.");
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();
});
/**
* #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();
});
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_addExperiment_eventEmit_add() {
const sandbox = sinon.createSandbox();
const slugStub = sandbox.stub();
const featureStub = sandbox.stub();
const experiment = ExperimentFakes.experiment("foo", {
branch: {
slug: "variant",
feature: { featureId: "purple", enabled: true },
},
});
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);
store.addExperiment(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",
feature: { featureId: "purple", enabled: true },
},
});
const store = ExperimentFakes.store();
sandbox.stub(ExperimentAPI, "_store").get(() => store);
await store.init();
await ExperimentAPI.ready();
store.addExperiment(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",
feature: { featureId: "purple", enabled: true },
},
});
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);
store.addExperiment(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_activateBranch() {
const sandbox = sinon.createSandbox();
const store = ExperimentFakes.store();
sandbox.stub(ExperimentAPI, "_store").get(() => store);
const experiment = ExperimentFakes.experiment("foo", {
branch: {
slug: "variant",
feature: { featureId: "green", enabled: true },
},
});
await store.init();
store.addExperiment(experiment);
Assert.deepEqual(
ExperimentAPI.activateBranch({ featureId: "green" }),
experiment.branch,
"Should return feature of active experiment"
);
sandbox.restore();
});
add_task(async function test_activateBranch_safe() {
const sandbox = sinon.createSandbox();
sandbox.stub(ExperimentAPI._store, "getAllActive").throws();
try {
Assert.equal(
ExperimentAPI.activateBranch({ featureId: "green" }),
null,
"Should not throw"
);
} catch (e) {
Assert.ok(false, "Should catch error in ExperimentAPI");
}
sandbox.restore();
});
add_task(async function test_activateBranch_activationEvent() {
const store = ExperimentFakes.store();
const sandbox = sinon.createSandbox();
sandbox.stub(ExperimentAPI, "_store").get(() => store);
const experiment = ExperimentFakes.experiment("foo", {
branch: {
slug: "variant",
feature: { featureId: "green", enabled: true },
},
});
await store.init();
store.addExperiment(experiment);
// Adding stub later because `addExperiment` emits update events
const stub = sandbox.stub(ExperimentAPI, "recordExposureEvent");
ExperimentAPI.activateBranch({ featureId: "green" });
Assert.equal(
stub.callCount,
0,
"Exposure is not sent by default by activateBranch"
);
ExperimentAPI.activateBranch({ featureId: "green", sendExposureEvent: true });
Assert.equal(stub.callCount, 1, "Called by doing activateBranch");
Assert.deepEqual(
stub.firstCall.args[0],
{
featureId: experiment.branch.feature.featureId,
experimentSlug: experiment.slug,
branchSlug: experiment.branch.slug,
},
"Has correct payload"
);
sandbox.restore();
});
add_task(async function test_activateBranch_storeFailure() {
const store = ExperimentFakes.store();
const sandbox = sinon.createSandbox();
sandbox.stub(ExperimentAPI, "_store").get(() => store);
const experiment = ExperimentFakes.experiment("foo", {
branch: {
slug: "variant",
feature: { featureId: "green", enabled: true },
},
});
await store.init();
store.addExperiment(experiment);
// Adding stub later because `addExperiment` emits update events
const stub = sandbox.stub(store, "emit");
// Call activateBranch to trigger an activation event
sandbox.stub(store, "getAllActive").throws();
try {
ExperimentAPI.activateBranch({ 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_activateBranch_noActivationEvent() {
const store = ExperimentFakes.store();
const sandbox = sinon.createSandbox();
sandbox.stub(ExperimentAPI, "_store").get(() => store);
const experiment = ExperimentFakes.experiment("foo", {
branch: {
slug: "variant",
feature: { featureId: "green", enabled: true },
},
});
await store.init();
store.addExperiment(experiment);
// Adding stub later because `addExperiment` emits update events
const stub = sandbox.stub(store, "emit");
// Call activateBranch to trigger an activation event
ExperimentAPI.activateBranch({ featureId: "green" });
Assert.equal(stub.callCount, 0, "Not called: sendExposureEvent is false");
sandbox.restore();
});