"use strict"; const { ExperimentAPI, _ExperimentFeature: ExperimentFeature } = ChromeUtils.importESModule("resource://nimbus/ExperimentAPI.sys.mjs"); const { ExperimentFakes } = ChromeUtils.importESModule( "resource://testing-common/NimbusTestUtils.sys.mjs" ); 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.enrollWithRollout( { featureId: "foo", value: { remoteValue: "mochitest", enabled: true }, }, { manager, } ); 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" ); });