summaryrefslogtreecommitdiffstats
path: root/toolkit/components/messaging-system/test/unit/test_ExperimentStore.js
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
commit2aa4a82499d4becd2284cdb482213d541b8804dd (patch)
treeb80bf8bf13c3766139fbacc530efd0dd9d54394c /toolkit/components/messaging-system/test/unit/test_ExperimentStore.js
parentInitial commit. (diff)
downloadfirefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz
firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/messaging-system/test/unit/test_ExperimentStore.js')
-rw-r--r--toolkit/components/messaging-system/test/unit/test_ExperimentStore.js449
1 files changed, 449 insertions, 0 deletions
diff --git a/toolkit/components/messaging-system/test/unit/test_ExperimentStore.js b/toolkit/components/messaging-system/test/unit/test_ExperimentStore.js
new file mode 100644
index 0000000000..fada9a66bd
--- /dev/null
+++ b/toolkit/components/messaging-system/test/unit/test_ExperimentStore.js
@@ -0,0 +1,449 @@
+"use strict";
+
+const SYNC_DATA_PREF = "messaging-system.syncdatastore.data";
+
+const { ExperimentFakes } = ChromeUtils.import(
+ "resource://testing-common/MSTestUtils.jsm"
+);
+const { ExperimentStore } = ChromeUtils.import(
+ "resource://messaging-system/experiments/ExperimentStore.jsm"
+);
+
+add_task(async function test_sharedDataMap_key() {
+ const store = new ExperimentStore();
+
+ // Outside of tests we use sharedDataKey for the profile dir filepath
+ // where we store experiments
+ Assert.ok(store._sharedDataKey, "Make sure it's defined");
+});
+
+add_task(async function test_usageBeforeInitialization() {
+ const store = ExperimentFakes.store();
+ const experiment = ExperimentFakes.experiment("foo", {
+ branch: {
+ slug: "variant",
+ feature: { featureId: "purple", enabled: true },
+ },
+ });
+
+ Assert.equal(store.getAll().length, 0, "It should not fail");
+
+ await store.init();
+ store.addExperiment(experiment);
+
+ Assert.equal(
+ store.getExperimentForFeature("purple"),
+ experiment,
+ "should return a matching experiment for the given feature"
+ );
+});
+
+add_task(async function test_event_add_experiment() {
+ const sandbox = sinon.createSandbox();
+ const store = ExperimentFakes.store();
+ const expected = ExperimentFakes.experiment("foo");
+ const updateEventCbStub = sandbox.stub();
+
+ // Setup ExperimentManager and child store for ExperimentAPI
+ await store.init();
+
+ // Set update cb
+ store.on("update:foo", updateEventCbStub);
+
+ // Add some data
+ store.addExperiment(expected);
+
+ Assert.equal(updateEventCbStub.callCount, 1, "Called once for add");
+});
+
+add_task(async function test_event_updates_main() {
+ const sandbox = sinon.createSandbox();
+ const store = ExperimentFakes.store();
+ const experiment = ExperimentFakes.experiment("foo");
+ const updateEventCbStub = sandbox.stub();
+
+ // Setup ExperimentManager and child store for ExperimentAPI
+ await store.init();
+
+ // Set update cb
+ store.on("update:aboutwelcome", updateEventCbStub);
+
+ store.addExperiment(experiment);
+ store.updateExperiment("foo", { active: false });
+
+ Assert.equal(
+ updateEventCbStub.callCount,
+ 2,
+ "Should be called twice: add, update"
+ );
+ Assert.equal(
+ updateEventCbStub.secondCall.args[1].active,
+ false,
+ "Should be called with updated experiment status"
+ );
+});
+
+add_task(async function test_getExperimentForGroup() {
+ const store = ExperimentFakes.store();
+ const experiment = ExperimentFakes.experiment("foo", {
+ branch: {
+ slug: "variant",
+ feature: { featureId: "purple", enabled: true },
+ },
+ });
+
+ await store.init();
+ store.addExperiment(ExperimentFakes.experiment("bar"));
+ store.addExperiment(experiment);
+
+ Assert.equal(
+ store.getExperimentForFeature("purple"),
+ experiment,
+ "should return a matching experiment for the given feature"
+ );
+});
+
+add_task(async function test_recordExposureEvent() {
+ const manager = ExperimentFakes.manager();
+ const experiment = ExperimentFakes.experiment("foo");
+ const experimentData = {
+ experimentSlug: experiment.slug,
+ branchSlug: experiment.branch.slug,
+ featureId: experiment.branch.feature.featureId,
+ };
+ await manager.onStartup();
+
+ let exposureEvEmit = new Promise(resolve =>
+ manager.store.on("exposure", (ev, data) => resolve(data))
+ );
+
+ manager.store.addExperiment(experiment);
+ manager.store._emitExperimentExposure(experimentData);
+
+ let result = await exposureEvEmit;
+
+ Assert.deepEqual(
+ result,
+ experimentData,
+ "should return the same data as sent"
+ );
+});
+
+add_task(async function test_activateBranch() {
+ const store = ExperimentFakes.store();
+ const experiment = ExperimentFakes.experiment("foo", {
+ branch: {
+ slug: "variant",
+ feature: { featureId: "green", enabled: true },
+ },
+ });
+
+ await store.init();
+ store.addExperiment(experiment);
+
+ Assert.deepEqual(
+ store.activateBranch({ featureId: "green" }),
+ experiment.branch,
+ "Should return feature of active experiment"
+ );
+});
+
+add_task(async function test_activateBranch_activationEvent() {
+ const store = ExperimentFakes.store();
+ const sandbox = sinon.createSandbox();
+ const experiment = ExperimentFakes.experiment("foo", {
+ branch: {
+ slug: "variant",
+ feature: { featureId: "green", enabled: true },
+ },
+ });
+
+ await store.init();
+ store.addExperiment(experiment);
+ // Adding stub later because `addExperiment` emits update events
+ const stub = sandbox.stub(store, "emit");
+ // Call activateBranch to trigger an activation event
+ store.activateBranch({ featureId: "green" });
+
+ Assert.equal(stub.callCount, 1, "Called by doing activateBranch");
+ Assert.equal(stub.firstCall.args[0], "exposure", "Has correct event name");
+ Assert.equal(
+ stub.firstCall.args[1].experimentSlug,
+ experiment.slug,
+ "Has correct payload"
+ );
+});
+
+add_task(async function test_activateBranch_storeFailure() {
+ const store = ExperimentFakes.store();
+ const sandbox = sinon.createSandbox();
+ const experiment = ExperimentFakes.experiment("foo", {
+ branch: {
+ slug: "variant",
+ feature: { featureId: "green", enabled: true },
+ },
+ });
+
+ await store.init();
+ store.addExperiment(experiment);
+ // Adding stub later because `addExperiment` emits update events
+ const stub = sandbox.stub(store, "emit");
+ // Call activateBranch to trigger an activation event
+ sandbox.stub(store, "getAllActive").throws();
+ try {
+ store.activateBranch({ featureId: "green" });
+ } catch (e) {
+ /* This is expected */
+ }
+
+ Assert.equal(stub.callCount, 0, "Not called if store somehow fails");
+});
+
+add_task(async function test_activateBranch_noActivationEvent() {
+ const store = ExperimentFakes.store();
+ const sandbox = sinon.createSandbox();
+ const experiment = ExperimentFakes.experiment("foo", {
+ branch: {
+ slug: "variant",
+ feature: { featureId: "green", enabled: true },
+ },
+ });
+
+ await store.init();
+ store.addExperiment(experiment);
+ // Adding stub later because `addExperiment` emits update events
+ const stub = sandbox.stub(store, "emit");
+ // Call activateBranch to trigger an activation event
+ store.activateBranch({ featureId: "green", sendExposurePing: false });
+
+ Assert.equal(stub.callCount, 0, "Not called: sendExposurePing is false");
+});
+
+add_task(async function test_hasExperimentForFeature() {
+ const store = ExperimentFakes.store();
+
+ await store.init();
+ store.addExperiment(
+ ExperimentFakes.experiment("foo", {
+ branch: {
+ slug: "variant",
+ feature: { featureId: "green", enabled: true },
+ },
+ })
+ );
+ store.addExperiment(
+ ExperimentFakes.experiment("foo2", {
+ branch: {
+ slug: "variant",
+ feature: { featureId: "yellow", enabled: true },
+ },
+ })
+ );
+ store.addExperiment(
+ ExperimentFakes.experiment("bar_expired", {
+ active: false,
+ branch: {
+ slug: "variant",
+ feature: { featureId: "purple", enabled: true },
+ },
+ })
+ );
+ Assert.equal(
+ store.hasExperimentForFeature(),
+ false,
+ "should return false if the input is empty"
+ );
+
+ Assert.equal(
+ store.hasExperimentForFeature(undefined),
+ false,
+ "should return false if the input is undefined"
+ );
+
+ Assert.equal(
+ store.hasExperimentForFeature("green"),
+ true,
+ "should return true if there is an experiment with any of the given groups"
+ );
+
+ Assert.equal(
+ store.hasExperimentForFeature("purple"),
+ false,
+ "should return false if there is a non-active experiment with the given groups"
+ );
+});
+
+add_task(async function test_getAll_getAllActive() {
+ const store = ExperimentFakes.store();
+
+ await store.init();
+ ["foo", "bar", "baz"].forEach(slug =>
+ store.addExperiment(ExperimentFakes.experiment(slug, { active: false }))
+ );
+ store.addExperiment(ExperimentFakes.experiment("qux", { active: true }));
+
+ Assert.deepEqual(
+ store.getAll().map(e => e.slug),
+ ["foo", "bar", "baz", "qux"],
+ ".getAll() should return all experiments"
+ );
+ Assert.deepEqual(
+ store.getAllActive().map(e => e.slug),
+ ["qux"],
+ ".getAllActive() should return all experiments that are active"
+ );
+});
+
+add_task(async function test_addExperiment() {
+ const store = ExperimentFakes.store();
+ const exp = ExperimentFakes.experiment("foo");
+
+ await store.init();
+ store.addExperiment(exp);
+
+ Assert.equal(store.get("foo"), exp, "should save experiment by slug");
+});
+
+add_task(async function test_updateExperiment() {
+ const feature = { featureId: "cfr", enabled: true };
+ const experiment = Object.freeze(
+ ExperimentFakes.experiment("foo", { feature, active: true })
+ );
+ const store = ExperimentFakes.store();
+
+ await store.init();
+ store.addExperiment(experiment);
+ store.updateExperiment("foo", { active: false });
+
+ const actual = store.get("foo");
+ Assert.equal(actual.active, false, "should change updated props");
+ Assert.deepEqual(
+ actual.branch.feature,
+ feature,
+ "should not update other props"
+ );
+});
+
+add_task(async function test_sync_access_before_init() {
+ Services.prefs.clearUserPref(SYNC_DATA_PREF);
+ let store = ExperimentFakes.store();
+
+ Assert.equal(store.getAll().length, 0, "Start with an empty store");
+
+ const syncAccessExp = ExperimentFakes.experiment("foo", {
+ feature: { featureId: "newtab", enabled: "true" },
+ });
+ await store.init();
+ store.addExperiment(syncAccessExp);
+
+ let prefValue = JSON.parse(Services.prefs.getStringPref(SYNC_DATA_PREF));
+
+ Assert.ok(Object.keys(prefValue).length === 1, "Parsed stored experiment");
+ Assert.equal(
+ prefValue.foo.slug,
+ syncAccessExp.slug,
+ "Got back the experiment"
+ );
+
+ // New un-initialized store that should read the pref value
+ store = ExperimentFakes.store();
+
+ Assert.equal(store.getAll().length, 1, "Returns experiment from pref");
+});
+
+add_task(async function test_sync_access_update() {
+ Services.prefs.clearUserPref(SYNC_DATA_PREF);
+ let store = ExperimentFakes.store();
+ let experiment = ExperimentFakes.experiment("foo", {
+ feature: { featureId: "aboutwelcome", enabled: true },
+ });
+
+ await store.init();
+
+ store.addExperiment(experiment);
+ store.updateExperiment("foo", {
+ branch: {
+ ...experiment.branch,
+ feature: { featureId: "aboutwelcome", enabled: true, value: "bar" },
+ },
+ });
+
+ store = ExperimentFakes.store();
+ let experiments = store.getAll();
+
+ Assert.equal(experiments.length, 1, "Got back 1 experiment");
+ Assert.equal(experiments[0].branch.feature.value, "bar", "Got updated value");
+});
+
+add_task(async function test_sync_features_only() {
+ Services.prefs.clearUserPref(SYNC_DATA_PREF);
+ let store = ExperimentFakes.store();
+ let experiment = ExperimentFakes.experiment("foo", {
+ feature: { featureId: "cfr", enabled: true },
+ });
+
+ await store.init();
+
+ store.addExperiment(experiment);
+ store = ExperimentFakes.store();
+
+ Assert.equal(store.getAll().length, 0, "cfr is not a sync access experiment");
+});
+
+add_task(async function test_sync_access_unenroll() {
+ Services.prefs.clearUserPref(SYNC_DATA_PREF);
+ let store = ExperimentFakes.store();
+ let experiment = ExperimentFakes.experiment("foo", {
+ feature: { featureId: "aboutwelcome", enabled: true },
+ active: true,
+ });
+
+ await store.init();
+
+ store.addExperiment(experiment);
+ store.updateExperiment("foo", { active: false });
+
+ store = ExperimentFakes.store();
+ let experiments = store.getAll();
+
+ Assert.equal(experiments.length, 0, "Unenrolled experiment is deleted");
+});
+
+add_task(async function test_sync_access_unenroll_2() {
+ Services.prefs.clearUserPref(SYNC_DATA_PREF);
+ let store = ExperimentFakes.store();
+ let experiment1 = ExperimentFakes.experiment("foo", {
+ feature: { featureId: "aboutwelcome", enabled: true },
+ });
+ let experiment2 = ExperimentFakes.experiment("bar", {
+ feature: { featureId: "aboutwelcome", enabled: true },
+ });
+
+ await store.init();
+
+ store.addExperiment(experiment1);
+ store.addExperiment(experiment2);
+
+ Assert.equal(store.getAll().length, 2, "2/2 experiments");
+
+ store.updateExperiment("bar", { active: false });
+ let other_store = ExperimentFakes.store();
+ Assert.equal(
+ other_store.getAll().length,
+ 1,
+ "Unenrolled from 1/2 experiments"
+ );
+
+ store.updateExperiment("foo", { active: false });
+ Assert.equal(
+ other_store.getAll().length,
+ 0,
+ "Unenrolled from 2/2 experiments"
+ );
+
+ Assert.equal(
+ Services.prefs.getStringPref(SYNC_DATA_PREF),
+ "{}",
+ "Empty store"
+ );
+});