Source code

Revision control

Copy as Markdown

Other Tools

Test Info: Warnings

"use strict";
const { ExperimentAPI, _ExperimentFeature: ExperimentFeature } =
ChromeUtils.importESModule("resource://nimbus/ExperimentAPI.sys.mjs");
const { ExperimentFakes } = ChromeUtils.importESModule(
);
async function setupForExperimentFeature() {
const sandbox = sinon.createSandbox();
const manager = ExperimentFakes.manager();
await manager.onStartup();
sandbox.stub(ExperimentAPI, "_store").get(() => manager.store);
return { sandbox, manager };
}
function setDefaultBranch(pref, value) {
let branch = Services.prefs.getDefaultBranch("");
branch.setStringPref(pref, value);
}
const TEST_FALLBACK_PREF = "testprefbranch.config";
const FAKE_FEATURE_MANIFEST = {
description: "Test feature",
exposureDescription: "Used in tests",
variables: {
enabled: {
type: "boolean",
fallbackPref: "testprefbranch.enabled",
},
config: {
type: "json",
fallbackPref: TEST_FALLBACK_PREF,
},
remoteValue: {
type: "boolean",
},
test: {
type: "boolean",
},
title: {
type: "string",
},
},
};
/**
* FOG requires a little setup in order to test it
*/
add_setup(function test_setup() {
// FOG needs a profile directory to put its data in.
do_get_profile();
// FOG needs to be initialized in order for data to flow.
Services.fog.initializeFOG();
});
add_task(async function test_ExperimentFeature_test_helper_ready() {
const { manager } = await setupForExperimentFeature();
await manager.store.ready();
const featureInstance = new ExperimentFeature("foo", FAKE_FEATURE_MANIFEST);
await ExperimentFakes.enrollWithFeatureConfig(
{
featureId: "foo",
value: { remoteValue: "mochitest", enabled: true },
},
{
manager,
isRollout: true,
}
);
Assert.equal(
featureInstance.getVariable("remoteValue"),
"mochitest",
"set by remote config"
);
});
add_task(async function test_record_exposure_event() {
const { sandbox, manager } = await setupForExperimentFeature();
const featureInstance = new ExperimentFeature("foo", FAKE_FEATURE_MANIFEST);
const exposureSpy = sandbox.spy(ExperimentAPI, "recordExposureEvent");
const getExperimentSpy = sandbox.spy(ExperimentAPI, "getExperimentMetaData");
sandbox.stub(ExperimentAPI, "_store").get(() => manager.store);
// Clear any pre-existing data in Glean
Services.fog.testResetFOG();
featureInstance.recordExposureEvent();
Assert.ok(
exposureSpy.notCalled,
"should not emit an exposure event when no experiment is active"
);
// Check that there aren't any Glean exposure events yet
var exposureEvents = Glean.nimbusEvents.exposure.testGetValue();
Assert.equal(
undefined,
exposureEvents,
"no Glean exposure events before exposure"
);
await manager.store.addEnrollment(
ExperimentFakes.experiment("blah", {
branch: {
slug: "treatment",
features: [
{
featureId: "foo",
value: { enabled: false },
},
],
},
})
);
featureInstance.recordExposureEvent();
Assert.ok(
exposureSpy.calledOnce,
"should emit an exposure event when there is an experiment"
);
Assert.equal(getExperimentSpy.callCount, 2, "Should be called every time");
// Check that the Glean exposure event was recorded.
exposureEvents = Glean.nimbusEvents.exposure.testGetValue();
// We expect only one event
Assert.equal(1, exposureEvents.length);
// And that one event matches the expected
Assert.equal(
"blah",
exposureEvents[0].extra.experiment,
"Glean.nimbusEvents.exposure recorded with correct experiment slug"
);
Assert.equal(
"treatment",
exposureEvents[0].extra.branch,
"Glean.nimbusEvents.exposure recorded with correct branch slug"
);
Assert.equal(
"foo",
exposureEvents[0].extra.feature_id,
"Glean.nimbusEvents.exposure recorded with correct feature id"
);
sandbox.restore();
});
add_task(async function test_record_exposure_event_once() {
const { sandbox, manager } = await setupForExperimentFeature();
const featureInstance = new ExperimentFeature("foo", FAKE_FEATURE_MANIFEST);
const exposureSpy = sandbox.spy(ExperimentAPI, "recordExposureEvent");
sandbox.stub(ExperimentAPI, "_store").get(() => manager.store);
// Clear any pre-existing data in Glean
Services.fog.testResetFOG();
await manager.store.addEnrollment(
ExperimentFakes.experiment("blah", {
branch: {
slug: "treatment",
features: [
{
featureId: "foo",
value: { enabled: false },
},
],
},
})
);
featureInstance.recordExposureEvent({ once: true });
featureInstance.recordExposureEvent({ once: true });
featureInstance.recordExposureEvent({ once: true });
Assert.ok(
exposureSpy.calledOnce,
"Should emit a single exposure event when the once param is true."
);
// Check that the Glean exposure event was recorded.
let exposureEvents = Glean.nimbusEvents.exposure.testGetValue();
// We expect only one event
Assert.equal(1, exposureEvents.length);
sandbox.restore();
});
add_task(async function test_allow_multiple_exposure_events() {
const { sandbox, manager } = await setupForExperimentFeature();
const featureInstance = new ExperimentFeature("foo", FAKE_FEATURE_MANIFEST);
const exposureSpy = sandbox.spy(ExperimentAPI, "recordExposureEvent");
// Clear any pre-existing data in Glean
Services.fog.testResetFOG();
let doExperimentCleanup = await ExperimentFakes.enrollWithFeatureConfig(
{
featureId: "foo",
value: { enabled: false },
},
{ manager }
);
featureInstance.recordExposureEvent();
featureInstance.recordExposureEvent();
featureInstance.recordExposureEvent();
Assert.ok(exposureSpy.called, "Should emit exposure event");
Assert.equal(
exposureSpy.callCount,
3,
"Should emit an exposure event for each function call"
);
// Check that the Glean exposure event was recorded.
let exposureEvents = Glean.nimbusEvents.exposure.testGetValue();
// We expect 3 events
Assert.equal(3, exposureEvents.length);
sandbox.restore();
await doExperimentCleanup();
});
add_task(async function test_onUpdate_before_store_ready() {
let sandbox = sinon.createSandbox();
const feature = new ExperimentFeature("foo", FAKE_FEATURE_MANIFEST);
const stub = sandbox.stub();
const manager = ExperimentFakes.manager();
sandbox.stub(ExperimentAPI, "_store").get(() => manager.store);
sandbox.stub(manager.store, "getAllActiveExperiments").returns([
ExperimentFakes.experiment("foo-experiment", {
branch: {
slug: "control",
features: [
{
featureId: "foo",
value: null,
},
],
},
}),
]);
// We register for updates before the store finished loading experiments
// from disk
feature.onUpdate(stub);
await manager.onStartup();
Assert.ok(
stub.calledOnce,
"Called on startup after loading experiments from disk"
);
Assert.equal(
stub.firstCall.args[0],
`featureUpdate:${feature.featureId}`,
"Called for correct feature"
);
Assert.equal(
stub.firstCall.args[1],
"feature-experiment-loaded",
"Called for the expected reason"
);
});
add_task(async function test_ExperimentFeature_test_ready_late() {
const { manager, sandbox } = await setupForExperimentFeature();
const stub = sandbox.stub();
const featureInstance = new ExperimentFeature(
"test-feature",
FAKE_FEATURE_MANIFEST
);
const rollout = ExperimentFakes.rollout("foo", {
branch: {
slug: "slug",
features: [
{
featureId: featureInstance.featureId,
value: {
title: "hello",
enabled: true,
},
},
],
},
});
sandbox.stub(manager.store, "getAllActiveRollouts").returns([rollout]);
await manager.onStartup();
featureInstance.onUpdate(stub);
await featureInstance.ready();
Assert.ok(stub.calledOnce, "Callback called");
Assert.equal(stub.firstCall.args[0], "featureUpdate:test-feature");
Assert.equal(stub.firstCall.args[1], "rollout-updated");
setDefaultBranch(TEST_FALLBACK_PREF, JSON.stringify({ foo: true }));
Assert.deepEqual(
featureInstance.getVariable("config"),
{ foo: true },
"Feature is ready even when initialized after store update"
);
Assert.equal(
featureInstance.getVariable("title"),
"hello",
"Returns the NimbusTestUtils rollout default value"
);
});