"use strict"; const { ExperimentAPI, _ExperimentFeature: 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 = { 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", }, }, }; const FAKE_FEATURE_REMOTE_VALUE = { slug: "default-remote-value", variables: { enabled: true, }, targeting: "true", }; /** * # 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: { ...FAKE_FEATURE_REMOTE_VALUE, variables: { remoteValue: "mochitest", enabled: true }, }, }); Assert.equal(featureInstance.isEnabled(), true, "enabled by remote config"); Assert.equal( featureInstance.getVariable("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", features: [ { featureId: "foo", value: { enabled: true }, }, ], }, }); const featureInstance = new ExperimentFeature("foo", FAKE_FEATURE_MANIFEST); await manager.store.ready(); await 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.recordExposureEvent(); Assert.ok(exposureSpy.calledOnce, "should emit exposure event"); 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"); const getExperimentSpy = sandbox.spy(ExperimentAPI, "getExperiment"); sandbox.stub(ExperimentAPI, "_store").get(() => manager.store); featureInstance.recordExposureEvent(); Assert.ok( exposureSpy.notCalled, "should not emit an exposure event when no experiment is active" ); await manager.store.addExperiment( 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"); 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); await manager.store.addExperiment( ExperimentFakes.experiment("blah", { branch: { slug: "treatment", features: [ { 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() { const { sandbox, manager } = await setupForExperimentFeature(); const featureInstance = new ExperimentFeature("foo", FAKE_FEATURE_MANIFEST); const exposureSpy = sandbox.spy(ExperimentAPI, "recordExposureEvent"); 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.ok(exposureSpy.calledOnce, "Should emit a single exposure event"); sandbox.restore(); await doExperimentCleanup(); }); 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); await Assert.rejects( ExperimentFakes.remoteDefaultsHelper({ feature, store: manager.store, configuration: { ...FAKE_FEATURE_REMOTE_VALUE, variables: { test: true, enabled: true }, }, }), /Store not ready/, "Throws if used before init finishes" ); await manager.onStartup(); await ExperimentFakes.remoteDefaultsHelper({ feature, store: manager.store, configuration: { ...FAKE_FEATURE_REMOTE_VALUE, variables: { test: true, enabled: true }, }, }); Assert.ok(feature.getVariable("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: { ...FAKE_FEATURE_REMOTE_VALUE, variables: { enabled: false }, }, }); Assert.ok(!feature.isEnabled(), "Disabled based on remote configs"); await manager.store.addExperiment( ExperimentFakes.experiment("blah", { branch: { slug: "treatment", features: [ { featureId: "foo", enabled: true, value: {}, }, ], }, }) ); Assert.ok(exposureSpy.notCalled, "Not called until now"); Assert.ok(feature.isEnabled(), "Enabled based on experiment recipe"); }); 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", { 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[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(); await manager.store.ready(); manager.store.finalizeRemoteConfigs([]); const featureInstance = new ExperimentFeature("foo", FAKE_FEATURE_MANIFEST); featureInstance.onUpdate(stub); // Setting a really high timeout so in case our ready function doesn't handle // this late init + ready scenario correctly the test will time out await featureInstance.ready(400 * 1000); Assert.ok(stub.notCalled, "We register too late to catch any events"); Assert.ok( !featureInstance.isEnabled(), "Feature is ready even when initialized after store update" ); });