summaryrefslogtreecommitdiffstats
path: root/toolkit/components/nimbus/test/unit/test_ExperimentAPI_NimbusFeatures.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/nimbus/test/unit/test_ExperimentAPI_NimbusFeatures.js')
-rw-r--r--toolkit/components/nimbus/test/unit/test_ExperimentAPI_NimbusFeatures.js289
1 files changed, 289 insertions, 0 deletions
diff --git a/toolkit/components/nimbus/test/unit/test_ExperimentAPI_NimbusFeatures.js b/toolkit/components/nimbus/test/unit/test_ExperimentAPI_NimbusFeatures.js
new file mode 100644
index 0000000000..9333a128f5
--- /dev/null
+++ b/toolkit/components/nimbus/test/unit/test_ExperimentAPI_NimbusFeatures.js
@@ -0,0 +1,289 @@
+"use strict";
+
+const { ExperimentAPI, _ExperimentFeature: ExperimentFeature } =
+ ChromeUtils.importESModule("resource://nimbus/ExperimentAPI.sys.mjs");
+
+const { JsonSchema } = ChromeUtils.importESModule(
+ "resource://gre/modules/JsonSchema.sys.mjs"
+);
+
+Cu.importGlobalProperties(["fetch"]);
+
+XPCOMUtils.defineLazyGetter(this, "fetchSchema", () => {
+ return fetch("resource://nimbus/schemas/NimbusEnrollment.schema.json", {
+ credentials: "omit",
+ }).then(rsp => rsp.json());
+});
+
+const NON_MATCHING_ROLLOUT = Object.freeze(
+ ExperimentFakes.rollout("non-matching-rollout", {
+ branch: {
+ slug: "slug",
+ features: [
+ {
+ featureId: "aboutwelcome",
+ value: { enabled: false },
+ },
+ ],
+ },
+ })
+);
+const MATCHING_ROLLOUT = Object.freeze(
+ ExperimentFakes.rollout("matching-rollout", {
+ branch: {
+ slug: "slug",
+ features: [
+ {
+ featureId: "aboutwelcome",
+ value: { enabled: false },
+ },
+ ],
+ },
+ })
+);
+
+const AW_FAKE_MANIFEST = {
+ description: "Different manifest with a special test variable",
+ isEarlyStartup: true,
+ variables: {
+ remoteValue: {
+ type: "boolean",
+ description: "Test value",
+ },
+ mochitest: {
+ type: "boolean",
+ },
+ enabled: {
+ type: "boolean",
+ },
+ },
+};
+
+async function setupForExperimentFeature() {
+ const sandbox = sinon.createSandbox();
+ const manager = ExperimentFakes.manager();
+
+ await manager.onStartup();
+
+ sandbox.stub(ExperimentAPI, "_store").get(() => manager.store);
+
+ return { sandbox, manager };
+}
+
+add_task(async function validSchema() {
+ const validator = new JsonSchema.Validator(await fetchSchema, {
+ shortCircuit: false,
+ });
+
+ {
+ const result = validator.validate(NON_MATCHING_ROLLOUT);
+ Assert.ok(result.valid, JSON.stringify(result.errors, undefined, 2));
+ }
+ {
+ const result = validator.validate(MATCHING_ROLLOUT);
+ Assert.ok(result.valid, JSON.stringify(result.errors, undefined, 2));
+ }
+});
+
+add_task(async function readyCallAfterStore_with_remote_value() {
+ let { sandbox, manager } = await setupForExperimentFeature();
+ let feature = new ExperimentFeature("aboutwelcome");
+
+ Assert.ok(feature.getVariable("enabled"), "Feature is true by default");
+
+ await manager.store.addEnrollment(MATCHING_ROLLOUT);
+
+ Assert.ok(!feature.getVariable("enabled"), "Loads value from store");
+ manager.store._deleteForTests("aboutwelcome");
+ sandbox.restore();
+});
+
+add_task(async function has_sync_value_before_ready() {
+ let { manager } = await setupForExperimentFeature();
+ let feature = new ExperimentFeature("aboutwelcome", AW_FAKE_MANIFEST);
+
+ Assert.equal(
+ feature.getVariable("remoteValue"),
+ undefined,
+ "Feature is true by default"
+ );
+
+ Services.prefs.setStringPref(
+ "nimbus.syncdefaultsstore.aboutwelcome",
+ JSON.stringify({
+ ...MATCHING_ROLLOUT,
+ branch: { feature: MATCHING_ROLLOUT.branch.features[0] },
+ })
+ );
+
+ Services.prefs.setBoolPref(
+ "nimbus.syncdefaultsstore.aboutwelcome.remoteValue",
+ true
+ );
+
+ Assert.equal(feature.getVariable("remoteValue"), true, "Sync load from pref");
+
+ manager.store._deleteForTests("aboutwelcome");
+});
+
+add_task(async function update_remote_defaults_onUpdate() {
+ let { sandbox, manager } = await setupForExperimentFeature();
+ let feature = new ExperimentFeature("aboutwelcome");
+ let stub = sandbox.stub();
+
+ feature.onUpdate(stub);
+
+ await manager.store.addEnrollment(MATCHING_ROLLOUT);
+
+ Assert.ok(stub.called, "update event called");
+ Assert.equal(stub.callCount, 1, "Called once for remote configs");
+ Assert.equal(stub.firstCall.args[1], "rollout-updated", "Correct reason");
+
+ manager.store._deleteForTests("aboutwelcome");
+ sandbox.restore();
+});
+
+add_task(async function test_features_over_feature() {
+ let { sandbox, manager } = await setupForExperimentFeature();
+ let feature = new ExperimentFeature("aboutwelcome");
+ const rollout_features_and_feature = Object.freeze(
+ ExperimentFakes.rollout("matching-rollout", {
+ branch: {
+ slug: "slug",
+ feature: {
+ featureId: "aboutwelcome",
+ value: { enabled: false },
+ },
+ features: [
+ {
+ featureId: "aboutwelcome",
+ value: { enabled: true },
+ },
+ ],
+ },
+ })
+ );
+ const rollout_just_feature = Object.freeze(
+ ExperimentFakes.rollout("matching-rollout", {
+ branch: {
+ slug: "slug",
+ feature: {
+ featureId: "aboutwelcome",
+ value: { enabled: false },
+ },
+ },
+ })
+ );
+
+ await manager.store.addEnrollment(rollout_features_and_feature);
+ Assert.ok(
+ feature.getVariable("enabled"),
+ "Should read from the features property over feature"
+ );
+
+ manager.store._deleteForTests("aboutwelcome");
+ manager.store._deleteForTests("matching-rollout");
+
+ await manager.store.addEnrollment(rollout_just_feature);
+ Assert.ok(
+ !feature.getVariable("enabled"),
+ "Should read from the feature property when features doesn't exist"
+ );
+
+ manager.store._deleteForTests("aboutwelcome");
+ manager.store._deleteForTests("matching-rollout");
+ sandbox.restore();
+});
+
+add_task(async function update_remote_defaults_readyPromise() {
+ let { sandbox, manager } = await setupForExperimentFeature();
+ let feature = new ExperimentFeature("aboutwelcome");
+ let stub = sandbox.stub();
+
+ feature.onUpdate(stub);
+
+ await manager.store.addEnrollment(MATCHING_ROLLOUT);
+
+ Assert.ok(stub.calledOnce, "Update called after enrollment processed.");
+ Assert.ok(
+ stub.calledWith("featureUpdate:aboutwelcome", "rollout-updated"),
+ "Update called after enrollment processed."
+ );
+
+ manager.store._deleteForTests("aboutwelcome");
+ sandbox.restore();
+});
+
+add_task(async function update_remote_defaults_enabled() {
+ let { sandbox, manager } = await setupForExperimentFeature();
+ let feature = new ExperimentFeature("aboutwelcome");
+
+ Assert.equal(
+ feature.getVariable("enabled"),
+ true,
+ "Feature is enabled by manifest.variables.enabled"
+ );
+
+ await manager.store.addEnrollment(NON_MATCHING_ROLLOUT);
+
+ Assert.ok(
+ !feature.getVariable("enabled"),
+ "Feature is disabled by remote configuration"
+ );
+
+ manager.store._deleteForTests("aboutwelcome");
+ sandbox.restore();
+});
+
+// If the branch data returned from the store is not modified
+// this test should not throw
+add_task(async function test_getVariable_no_mutation() {
+ let { sandbox, manager } = await setupForExperimentFeature();
+ sandbox.stub(manager.store, "getExperimentForFeature").returns(
+ Cu.cloneInto(
+ {
+ branch: {
+ features: [{ featureId: "aboutwelcome", value: { mochitest: true } }],
+ },
+ },
+ {},
+ { deepFreeze: true }
+ )
+ );
+ let feature = new ExperimentFeature("aboutwelcome", AW_FAKE_MANIFEST);
+
+ Assert.ok(feature.getVariable("mochitest"), "Got back the expected feature");
+
+ sandbox.restore();
+});
+
+add_task(async function remote_isEarlyStartup_config() {
+ let { manager } = await setupForExperimentFeature();
+ let rollout = ExperimentFakes.rollout("password-autocomplete", {
+ branch: {
+ slug: "remote-config-isEarlyStartup",
+ features: [
+ {
+ featureId: "password-autocomplete",
+ enabled: true,
+ value: { remote: true },
+ isEarlyStartup: true,
+ },
+ ],
+ },
+ });
+
+ await manager.onStartup();
+ await manager.store.addEnrollment(rollout);
+
+ Assert.ok(
+ Services.prefs.prefHasUserValue(
+ "nimbus.syncdefaultsstore.password-autocomplete"
+ ),
+ "Configuration is marked early startup"
+ );
+
+ Services.prefs.clearUserPref(
+ "nimbus.syncdefaultsstore.password-autocomplete"
+ );
+});