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

555 lines
15 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"
);
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 = {
variables: {
enabled: {
type: "boolean",
fallbackPref: "testprefbranch.enabled",
},
config: {
type: "json",
fallbackPref: TEST_FALLBACK_PREF,
},
},
};
const FAKE_FEATURE_REMOTE_VALUE = {
variables: {
enabled: true,
},
targeting: "true",
};
/**
* # ExperimentFeature.getValue
*/
add_task(async function test_ExperimentFeature_ready() {
const { sandbox, manager } = await setupForExperimentFeature();
const featureInstance = new ExperimentFeature("foo", FAKE_FEATURE_MANIFEST);
let readyPromise = featureInstance.ready();
let stub = sandbox.stub();
featureInstance.onUpdate(stub);
const expected = ExperimentFakes.experiment("anexperiment", {
branch: {
slug: "treatment",
feature: {
featureId: "foo",
enabled: true,
value: { whoa: true },
},
},
});
manager.store.addExperiment(expected);
await readyPromise;
Assert.deepEqual(
featureInstance.getValue(),
{ whoa: true },
"should return getValue after waiting on ready"
);
Assert.equal(stub.callCount, 1, "Called when experiment registered");
Assert.equal(stub.firstCall.args[1], "experiment-updated", "Verify reason");
Services.prefs.clearUserPref("testprefbranch.value");
sandbox.restore();
});
/**
* # ExperimentFeature.getValue
*/
add_task(async function test_ExperimentFeature_getValue() {
const { sandbox } = await setupForExperimentFeature();
const featureInstance = new ExperimentFeature("foo", FAKE_FEATURE_MANIFEST);
Services.prefs.setStringPref(TEST_FALLBACK_PREF, `{"bar": 123}`);
Assert.deepEqual(
featureInstance.getValue().config,
{ bar: 123 },
"should return the fallback pref value"
);
Services.prefs.clearUserPref(TEST_FALLBACK_PREF);
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);
setDefaultBranch(TEST_FALLBACK_PREF, `{"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_default_over_remote() {
const { manager, sandbox } = await setupForExperimentFeature();
await manager.store.ready();
const featureInstance = new ExperimentFeature("foo", FAKE_FEATURE_MANIFEST);
Services.prefs.setBoolPref("testprefbranch.enabled", false);
Assert.equal(
featureInstance.isEnabled(),
false,
"should use the default pref value, including if it is false"
);
manager.store.updateRemoteConfigs("foo", {
...FAKE_FEATURE_REMOTE_VALUE,
variables: { enabled: true },
});
await featureInstance.ready();
Assert.equal(
featureInstance.isEnabled(),
false,
"Should still use userpref over remote"
);
Services.prefs.clearUserPref("testprefbranch.enabled");
Assert.equal(
featureInstance.isEnabled(),
true,
"Should use remote value over default pref"
);
sandbox.restore();
});
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.remoteDefaultsHelper({
feature: featureInstance,
store: manager.store,
configuration: { variables: { remoteValue: "mochitest" }, enabled: true },
});
Assert.equal(featureInstance.isEnabled(), true, "enabled by remote config");
Assert.equal(
featureInstance.getValue().remoteValue,
"mochitest",
"set by remote config"
);
});
add_task(
async function test_ExperimentFeature_isEnabled_prefer_experiment_over_remote() {
const { sandbox, manager } = await setupForExperimentFeature();
const expected = ExperimentFakes.experiment("foo", {
branch: {
slug: "treatment",
feature: {
featureId: "foo",
value: { enabled: true },
},
},
});
const featureInstance = new ExperimentFeature("foo", FAKE_FEATURE_MANIFEST);
await manager.store.ready();
manager.store.addExperiment(expected);
const exposureSpy = sandbox.spy(ExperimentAPI, "recordExposureEvent");
manager.store.updateRemoteConfigs("foo", {
...FAKE_FEATURE_REMOTE_VALUE,
variables: { enabled: false },
});
await featureInstance.ready();
Assert.equal(
featureInstance.isEnabled(),
true,
"should return the enabled value defined in the experiment not the remote value"
);
Services.prefs.setBoolPref("testprefbranch.enabled", false);
Assert.equal(
featureInstance.isEnabled(),
false,
"should return the user pref not the experiment value"
);
// Exposure is not triggered if user pref is set
Services.prefs.clearUserPref("testprefbranch.enabled");
Assert.ok(exposureSpy.notCalled, "should not emit exposure by default");
featureInstance.isEnabled({ sendExposureEvent: true });
Assert.ok(exposureSpy.calledOnce, "should emit exposure event");
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",
value: { enabled: false },
},
},
});
const featureInstance = new ExperimentFeature("foo", FAKE_FEATURE_MANIFEST);
sandbox.stub(ExperimentAPI, "_store").get(() => manager.store);
manager.store.addExperiment(expected);
const exposureSpy = sandbox.spy(ExperimentAPI, "recordExposureEvent");
const actual = featureInstance.isEnabled({ sendExposureEvent: false });
Assert.deepEqual(actual, false, "should return feature as disabled");
Assert.ok(
exposureSpy.notCalled,
"should not emit an exposure event when options = { sendExposureEvent: false}"
);
sandbox.restore();
});
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");
sandbox.stub(ExperimentAPI, "_store").get(() => manager.store);
featureInstance.recordExposureEvent();
Assert.ok(
exposureSpy.notCalled,
"should not emit an exposure event when no experiment is active"
);
manager.store.addExperiment(
ExperimentFakes.experiment("blah", {
featureIds: ["foo"],
branch: {
slug: "treatment",
feature: {
featureId: "foo",
value: { enabled: false },
},
},
})
);
featureInstance.recordExposureEvent();
Assert.ok(
exposureSpy.calledOnce,
"should emit an exposure event when there is an experiment"
);
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);
manager.store.addExperiment(
ExperimentFakes.experiment("blah", {
featureIds: ["foo"],
branch: {
slug: "treatment",
feature: {
featureId: "foo",
value: { enabled: false },
},
},
})
);
featureInstance.recordExposureEvent();
featureInstance.recordExposureEvent();
featureInstance.recordExposureEvent();
Assert.ok(exposureSpy.calledOnce, "Should emit a single exposure event.");
sandbox.restore();
});
add_task(async function test_prevent_double_exposure_getValue() {
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);
manager.store.addExperiment(
ExperimentFakes.experiment("blah", {
featureIds: ["foo"],
branch: {
slug: "treatment",
feature: {
featureId: "foo",
value: { enabled: false },
},
},
})
);
featureInstance.getValue({ sendExposureEvent: true });
featureInstance.getValue({ sendExposureEvent: true });
featureInstance.getValue({ sendExposureEvent: true });
Assert.ok(
exposureSpy.calledOnce,
"Should emit a single exposure event (getValue)."
);
sandbox.restore();
});
add_task(async function test_prevent_double_exposure_isEnabled() {
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);
manager.store.addExperiment(
ExperimentFakes.experiment("blah", {
featureIds: ["foo"],
branch: {
slug: "treatment",
feature: {
featureId: "foo",
value: { enabled: false },
},
},
})
);
featureInstance.isEnabled({ sendExposureEvent: true });
featureInstance.isEnabled({ sendExposureEvent: true });
featureInstance.isEnabled({ sendExposureEvent: true });
Assert.ok(
exposureSpy.calledOnce,
"Should emit a single exposure event (getValue)."
);
sandbox.restore();
});
add_task(async function test_set_remote_before_ready() {
let sandbox = sinon.createSandbox();
const manager = ExperimentFakes.manager();
sandbox.stub(ExperimentAPI, "_store").get(() => manager.store);
const feature = new ExperimentFeature("foo", FAKE_FEATURE_MANIFEST);
Assert.throws(
() =>
ExperimentFakes.remoteDefaultsHelper({
feature,
store: manager.store,
configuration: { variables: { test: true } },
}),
/Store not ready/,
"Throws if used before init finishes"
);
await manager.onStartup();
await ExperimentFakes.remoteDefaultsHelper({
feature,
store: manager.store,
configuration: { variables: { test: true } },
});
Assert.ok(feature.getValue().test, "Successfully set");
});
add_task(async function test_isEnabled_backwards_compatible() {
const PREVIOUS_FEATURE_MANIFEST = {
variables: {
config: {
type: "json",
fallbackPref: TEST_FALLBACK_PREF,
},
},
};
let sandbox = sinon.createSandbox();
const manager = ExperimentFakes.manager();
sandbox.stub(ExperimentAPI, "_store").get(() => manager.store);
const exposureSpy = sandbox.spy(ExperimentAPI, "recordExposureEvent");
const feature = new ExperimentFeature("foo", PREVIOUS_FEATURE_MANIFEST);
await manager.onStartup();
await ExperimentFakes.remoteDefaultsHelper({
feature,
store: manager.store,
configuration: { variables: {}, enabled: false },
});
Assert.ok(!feature.isEnabled(), "Disabled based on remote configs");
manager.store.addExperiment(
ExperimentFakes.experiment("blah", {
featureIds: ["foo"],
branch: {
slug: "treatment",
feature: {
featureId: "foo",
enabled: true,
value: {},
},
},
})
);
Assert.ok(exposureSpy.notCalled, "Not called until now");
Assert.ok(
feature.isEnabled({ sendExposureEvent: true }),
"Enabled based on experiment recipe"
);
Assert.ok(exposureSpy.calledOnce, "Exposure event sent");
});
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, "getAllActive").returns([
ExperimentFakes.experiment("foo-experiment", {
featureIds: ["foo"],
branch: { slug: "control", feature: { 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[1],
"feature-experiment-loaded",
"Called for the expected reason"
);
});