mirror of
https://github.com/mozilla/gecko-dev.git
synced 2025-11-09 12:51:09 +02:00
555 lines
15 KiB
JavaScript
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"
|
|
);
|
|
});
|