summaryrefslogtreecommitdiffstats
path: root/toolkit/components/featuregates/test/unit
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/featuregates/test/unit')
-rw-r--r--toolkit/components/featuregates/test/unit/head.js3
-rw-r--r--toolkit/components/featuregates/test/unit/test_FeatureGate.js446
-rw-r--r--toolkit/components/featuregates/test/unit/test_FeatureGateImplementation.js141
-rw-r--r--toolkit/components/featuregates/test/unit/xpcshell.ini11
4 files changed, 601 insertions, 0 deletions
diff --git a/toolkit/components/featuregates/test/unit/head.js b/toolkit/components/featuregates/test/unit/head.js
new file mode 100644
index 0000000000..bd90d22f03
--- /dev/null
+++ b/toolkit/components/featuregates/test/unit/head.js
@@ -0,0 +1,3 @@
+var { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+);
diff --git a/toolkit/components/featuregates/test/unit/test_FeatureGate.js b/toolkit/components/featuregates/test/unit/test_FeatureGate.js
new file mode 100644
index 0000000000..b0aed54e8e
--- /dev/null
+++ b/toolkit/components/featuregates/test/unit/test_FeatureGate.js
@@ -0,0 +1,446 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { FeatureGate } = ChromeUtils.importESModule(
+ "resource://featuregates/FeatureGate.sys.mjs"
+);
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+const kDefinitionDefaults = {
+ id: "test-feature",
+ title: "Test Feature",
+ description: "A feature for testing",
+ restartRequired: false,
+ type: "boolean",
+ preference: "test.feature",
+ defaultValue: false,
+ isPublic: false,
+};
+
+function definitionFactory(override = {}) {
+ return Object.assign({}, kDefinitionDefaults, override);
+}
+
+class DefinitionServer {
+ constructor(definitionOverrides = []) {
+ this.server = new HttpServer();
+ this.server.registerPathHandler("/definitions.json", this);
+ this.definitions = {};
+
+ for (const override of definitionOverrides) {
+ this.addDefinition(override);
+ }
+
+ this.server.start();
+ registerCleanupFunction(
+ () => new Promise(resolve => this.server.stop(resolve))
+ );
+ }
+
+ // for nsIHttpRequestHandler
+ handle(request, response) {
+ // response.setHeader("Content-Type", "application/json");
+ response.write(JSON.stringify(this.definitions));
+ }
+
+ get definitionsUrl() {
+ const { primaryScheme, primaryHost, primaryPort } = this.server.identity;
+ return `${primaryScheme}://${primaryHost}:${primaryPort}/definitions.json`;
+ }
+
+ addDefinition(overrides = {}) {
+ const definition = definitionFactory(overrides);
+ // convert targeted values, used by fromId
+ definition.isPublic = {
+ default: definition.isPublic,
+ "test-fact": !definition.isPublic,
+ };
+ definition.defaultValue = {
+ default: definition.defaultValue,
+ "test-fact": !definition.defaultValue,
+ };
+ this.definitions[definition.id] = definition;
+ return definition;
+ }
+}
+
+// ============================================================================
+add_task(async function testReadAll() {
+ const server = new DefinitionServer();
+ let ids = ["test-featureA", "test-featureB", "test-featureC"];
+ for (let id of ids) {
+ server.addDefinition({ id });
+ }
+ let sortedIds = ids.sort();
+ const features = await FeatureGate.all(server.definitionsUrl);
+ for (let feature of features) {
+ equal(
+ feature.id,
+ sortedIds.shift(),
+ "Features are returned in order of definition"
+ );
+ }
+ equal(sortedIds.length, 0, "All features are returned when calling all()");
+});
+
+// The getters and setters should read correctly from the definition
+add_task(async function testReadFromDefinition() {
+ const server = new DefinitionServer();
+ const definition = server.addDefinition({ id: "test-feature" });
+ const feature = await FeatureGate.fromId(
+ "test-feature",
+ server.definitionsUrl
+ );
+
+ // simple fields
+ equal(feature.id, definition.id, "id should be read from definition");
+ equal(
+ feature.title,
+ definition.title,
+ "title should be read from definition"
+ );
+ equal(
+ feature.description,
+ definition.description,
+ "description should be read from definition"
+ );
+ equal(
+ feature.restartRequired,
+ definition.restartRequired,
+ "restartRequired should be read from definition"
+ );
+ equal(feature.type, definition.type, "type should be read from definition");
+ equal(
+ feature.preference,
+ definition.preference,
+ "preference should be read from definition"
+ );
+
+ // targeted fields
+ equal(
+ feature.defaultValue,
+ definition.defaultValue.default,
+ "defaultValue should be processed as a targeted value"
+ );
+ equal(
+ feature.defaultValueWith(new Map()),
+ definition.defaultValue.default,
+ "An empty set of extra facts results in the same value"
+ );
+ equal(
+ feature.defaultValueWith(new Map([["test-fact", true]])),
+ !definition.defaultValue.default,
+ "Including an extra fact can change the value"
+ );
+
+ equal(
+ feature.isPublic,
+ definition.isPublic.default,
+ "isPublic should be processed as a targeted value"
+ );
+ equal(
+ feature.isPublicWith(new Map()),
+ definition.isPublic.default,
+ "An empty set of extra facts results in the same value"
+ );
+ equal(
+ feature.isPublicWith(new Map([["test-fact", true]])),
+ !definition.isPublic.default,
+ "Including an extra fact can change the value"
+ );
+
+ // cleanup
+ Services.prefs.getDefaultBranch("").deleteBranch("test.feature");
+});
+
+// Targeted values should return the correct value
+add_task(async function testTargetedValues() {
+ const targetingFacts = new Map(
+ Object.entries({ true1: true, true2: true, false1: false, false2: false })
+ );
+
+ Assert.equal(
+ FeatureGate.evaluateTargetedValue({ default: "foo" }, targetingFacts),
+ "foo",
+ "A lone default value should be returned"
+ );
+ Assert.equal(
+ FeatureGate.evaluateTargetedValue(
+ { default: "foo", true1: "bar" },
+ targetingFacts
+ ),
+ "bar",
+ "A true target should override the default"
+ );
+ Assert.equal(
+ FeatureGate.evaluateTargetedValue(
+ { default: "foo", false1: "bar" },
+ targetingFacts
+ ),
+ "foo",
+ "A false target should not overrides the default"
+ );
+ Assert.equal(
+ FeatureGate.evaluateTargetedValue(
+ { default: "foo", "true1,true2": "bar" },
+ targetingFacts
+ ),
+ "bar",
+ "A compound target of two true targets should override the default"
+ );
+ Assert.equal(
+ FeatureGate.evaluateTargetedValue(
+ { default: "foo", "true1,false1": "bar" },
+ targetingFacts
+ ),
+ "foo",
+ "A compound target of a true target and a false target should not override the default"
+ );
+ Assert.equal(
+ FeatureGate.evaluateTargetedValue(
+ { default: "foo", "false1,false2": "bar" },
+ targetingFacts
+ ),
+ "foo",
+ "A compound target of two false targets should not override the default"
+ );
+ Assert.equal(
+ FeatureGate.evaluateTargetedValue(
+ { default: "foo", false1: "bar", true1: "baz" },
+ targetingFacts
+ ),
+ "baz",
+ "A true target should override the default when a false target is also present"
+ );
+});
+
+// getValue should work
+add_task(async function testGetValue() {
+ equal(
+ Services.prefs.getPrefType("test.feature.1"),
+ Services.prefs.PREF_INVALID,
+ "Before creating the feature gate, the preference should not exist"
+ );
+
+ const server = new DefinitionServer([
+ { id: "test-feature-1", defaultValue: false, preference: "test.feature.1" },
+ { id: "test-feature-2", defaultValue: true, preference: "test.feature.2" },
+ ]);
+
+ equal(
+ await FeatureGate.getValue("test-feature-1", server.definitionsUrl),
+ false,
+ "getValue() starts by returning the default value"
+ );
+ equal(
+ await FeatureGate.getValue("test-feature-2", server.definitionsUrl),
+ true,
+ "getValue() starts by returning the default value"
+ );
+
+ Services.prefs.setBoolPref("test.feature.1", true);
+ equal(
+ await FeatureGate.getValue("test-feature-1", server.definitionsUrl),
+ true,
+ "getValue() return the new value"
+ );
+
+ Services.prefs.setBoolPref("test.feature.1", false);
+ equal(
+ await FeatureGate.getValue("test-feature-1", server.definitionsUrl),
+ false,
+ "getValue() should return the second value"
+ );
+
+ // cleanup
+ Services.prefs.getDefaultBranch("").deleteBranch("test.feature.");
+});
+
+// getValue should work
+add_task(async function testGetValue() {
+ const server = new DefinitionServer([
+ { id: "test-feature-1", defaultValue: false, preference: "test.feature.1" },
+ { id: "test-feature-2", defaultValue: true, preference: "test.feature.2" },
+ ]);
+
+ equal(
+ Services.prefs.getPrefType("test.feature.1"),
+ Services.prefs.PREF_INVALID,
+ "Before creating the feature gate, the first preference should not exist"
+ );
+ equal(
+ Services.prefs.getPrefType("test.feature.2"),
+ Services.prefs.PREF_INVALID,
+ "Before creating the feature gate, the second preference should not exist"
+ );
+
+ equal(
+ await FeatureGate.isEnabled("test-feature-1", server.definitionsUrl),
+ false,
+ "isEnabled() starts by returning the default value"
+ );
+ equal(
+ await FeatureGate.isEnabled("test-feature-2", server.definitionsUrl),
+ true,
+ "isEnabled() starts by returning the default value"
+ );
+
+ Services.prefs.setBoolPref("test.feature.1", true);
+ equal(
+ await FeatureGate.isEnabled("test-feature-1", server.definitionsUrl),
+ true,
+ "isEnabled() return the new value"
+ );
+
+ Services.prefs.setBoolPref("test.feature.1", false);
+ equal(
+ await FeatureGate.isEnabled("test-feature-1", server.definitionsUrl),
+ false,
+ "isEnabled() should return the second value"
+ );
+
+ // cleanup
+ Services.prefs.getDefaultBranch("").deleteBranch("test.feature.");
+});
+
+// adding and removing event observers should work
+add_task(async function testGetValue() {
+ const preference = "test.pref";
+ const server = new DefinitionServer([
+ { id: "test-feature", defaultValue: false, preference },
+ ]);
+ const observer = {
+ onChange: sinon.stub(),
+ onEnable: sinon.stub(),
+ onDisable: sinon.stub(),
+ };
+
+ let rv = await FeatureGate.addObserver(
+ "test-feature",
+ observer,
+ server.definitionsUrl
+ );
+ equal(rv, false, "addObserver returns the current value");
+
+ Assert.deepEqual(observer.onChange.args, [], "onChange should not be called");
+ Assert.deepEqual(observer.onEnable.args, [], "onEnable should not be called");
+ Assert.deepEqual(
+ observer.onDisable.args,
+ [],
+ "onDisable should not be called"
+ );
+
+ Services.prefs.setBoolPref(preference, true);
+ await Promise.resolve(); // Allow events to be called async
+ Assert.deepEqual(
+ observer.onChange.args,
+ [[true]],
+ "onChange should be called with the new value"
+ );
+ Assert.deepEqual(observer.onEnable.args, [[]], "onEnable should be called");
+ Assert.deepEqual(
+ observer.onDisable.args,
+ [],
+ "onDisable should not be called"
+ );
+
+ Services.prefs.setBoolPref(preference, false);
+ await Promise.resolve(); // Allow events to be called async
+ Assert.deepEqual(
+ observer.onChange.args,
+ [[true], [false]],
+ "onChange should be called again with the new value"
+ );
+ Assert.deepEqual(
+ observer.onEnable.args,
+ [[]],
+ "onEnable should not be called a second time"
+ );
+ Assert.deepEqual(
+ observer.onDisable.args,
+ [[]],
+ "onDisable should be called for the first time"
+ );
+
+ Services.prefs.setBoolPref(preference, false);
+ await Promise.resolve(); // Allow events to be called async
+ Assert.deepEqual(
+ observer.onChange.args,
+ [[true], [false]],
+ "onChange should not be called if the value did not change"
+ );
+ Assert.deepEqual(
+ observer.onEnable.args,
+ [[]],
+ "onEnable should not be called again if the value did not change"
+ );
+ Assert.deepEqual(
+ observer.onDisable.args,
+ [[]],
+ "onDisable should not be called if the value did not change"
+ );
+
+ // remove the listener and make sure the observer isn't called again
+ FeatureGate.removeObserver("test-feature", observer);
+ await Promise.resolve(); // Allow events to be called async
+
+ Services.prefs.setBoolPref(preference, true);
+ await Promise.resolve(); // Allow events to be called async
+ Assert.deepEqual(
+ observer.onChange.args,
+ [[true], [false]],
+ "onChange should not be called after observer was removed"
+ );
+ Assert.deepEqual(
+ observer.onEnable.args,
+ [[]],
+ "onEnable should not be called after observer was removed"
+ );
+ Assert.deepEqual(
+ observer.onDisable.args,
+ [[]],
+ "onDisable should not be called after observer was removed"
+ );
+
+ // cleanup
+ Services.prefs.getDefaultBranch("").deleteBranch(preference);
+});
+
+if (AppConstants.platform != "android") {
+ // All preferences should have default values.
+ add_task(async function testAllHaveDefault() {
+ const featuresList = await FeatureGate.all();
+ for (let feature of featuresList) {
+ notEqual(
+ typeof feature.defaultValue,
+ "undefined",
+ `Feature ${feature.id} should have a defined default value!`
+ );
+ notEqual(
+ feature.defaultValue,
+ null,
+ `Feature ${feature.id} should have a non-null default value!`
+ );
+ }
+ });
+
+ // All preference defaults should match service pref defaults
+ add_task(async function testAllDefaultsMatchSettings() {
+ const featuresList = await FeatureGate.all();
+ for (let feature of featuresList) {
+ let value = Services.prefs
+ .getDefaultBranch("")
+ .getBoolPref(feature.preference);
+ equal(
+ feature.defaultValue,
+ value,
+ `Feature ${feature.preference} should match runtime value.`
+ );
+ }
+ });
+}
diff --git a/toolkit/components/featuregates/test/unit/test_FeatureGateImplementation.js b/toolkit/components/featuregates/test/unit/test_FeatureGateImplementation.js
new file mode 100644
index 0000000000..ac844b4880
--- /dev/null
+++ b/toolkit/components/featuregates/test/unit/test_FeatureGateImplementation.js
@@ -0,0 +1,141 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { FeatureGateImplementation } = ChromeUtils.importESModule(
+ "resource://featuregates/FeatureGateImplementation.sys.mjs"
+);
+
+const kDefinitionDefaults = {
+ id: "test-feature",
+ title: "Test Feature",
+ description: "A feature for testing",
+ restartRequired: false,
+ type: "boolean",
+ preference: "test.feature",
+ defaultValue: false,
+ isPublic: false,
+};
+
+function definitionFactory(override = {}) {
+ return Object.assign({}, kDefinitionDefaults, override);
+}
+
+// getValue should work
+add_task(async function testGetValue() {
+ const preference = "test.pref";
+ equal(
+ Services.prefs.getPrefType(preference),
+ Services.prefs.PREF_INVALID,
+ "Before creating the feature gate, the preference should not exist"
+ );
+ const feature = new FeatureGateImplementation(
+ definitionFactory({ preference, defaultValue: false })
+ );
+ equal(
+ Services.prefs.getPrefType(preference),
+ Services.prefs.PREF_INVALID,
+ "Instantiating a feature gate should not set its default value"
+ );
+ equal(
+ await feature.getValue(),
+ false,
+ "getValue() should return the feature gate's default"
+ );
+
+ Services.prefs.setBoolPref(preference, true);
+ equal(
+ await feature.getValue(),
+ true,
+ "getValue() should return the new value"
+ );
+
+ Services.prefs.setBoolPref(preference, false);
+ equal(
+ await feature.getValue(),
+ false,
+ "getValue() should return the third value"
+ );
+
+ // cleanup
+ Services.prefs.getDefaultBranch("").deleteBranch(preference);
+});
+
+// event observers should work
+add_task(async function testGetValue() {
+ const preference = "test.pref";
+ const feature = new FeatureGateImplementation(
+ definitionFactory({ preference, defaultValue: false })
+ );
+ const observer = {
+ onChange: sinon.stub(),
+ onEnable: sinon.stub(),
+ onDisable: sinon.stub(),
+ };
+
+ let rv = await feature.addObserver(observer);
+ equal(rv, false, "addObserver returns the current value");
+
+ Assert.deepEqual(observer.onChange.args, [], "onChange should not be called");
+ Assert.deepEqual(observer.onEnable.args, [], "onEnable should not be called");
+ Assert.deepEqual(
+ observer.onDisable.args,
+ [],
+ "onDisable should not be called"
+ );
+
+ Services.prefs.setBoolPref(preference, true);
+ await Promise.resolve(); // Allow events to be called async
+ Assert.deepEqual(
+ observer.onChange.args,
+ [[true]],
+ "onChange should be called with the new value"
+ );
+ Assert.deepEqual(observer.onEnable.args, [[]], "onEnable should be called");
+ Assert.deepEqual(
+ observer.onDisable.args,
+ [],
+ "onDisable should not be called"
+ );
+
+ Services.prefs.setBoolPref(preference, false);
+ await Promise.resolve(); // Allow events to be called async
+ Assert.deepEqual(
+ observer.onChange.args,
+ [[true], [false]],
+ "onChange should be called again with the new value"
+ );
+ Assert.deepEqual(
+ observer.onEnable.args,
+ [[]],
+ "onEnable should not be called a second time"
+ );
+ Assert.deepEqual(
+ observer.onDisable.args,
+ [[]],
+ "onDisable should be called for the first time"
+ );
+
+ Services.prefs.setBoolPref(preference, false);
+ await Promise.resolve(); // Allow events to be called async
+ Assert.deepEqual(
+ observer.onChange.args,
+ [[true], [false]],
+ "onChange should not be called if the value did not change"
+ );
+ Assert.deepEqual(
+ observer.onEnable.args,
+ [[]],
+ "onEnable should not be called again if the value did not change"
+ );
+ Assert.deepEqual(
+ observer.onDisable.args,
+ [[]],
+ "onDisable should not be called if the value did not change"
+ );
+
+ // cleanup
+ feature.removeAllObservers();
+ Services.prefs.getDefaultBranch("").deleteBranch(preference);
+});
diff --git a/toolkit/components/featuregates/test/unit/xpcshell.ini b/toolkit/components/featuregates/test/unit/xpcshell.ini
new file mode 100644
index 0000000000..dc6cb296bd
--- /dev/null
+++ b/toolkit/components/featuregates/test/unit/xpcshell.ini
@@ -0,0 +1,11 @@
+[DEFAULT]
+head = head.js
+tags = featuregates
+firefox-appdir = browser
+
+[test_FeatureGate.js]
+# Ignore platforms which the use the update channel 'default' on non-nightly
+# platforms because it gets compared to preference values guarded by variables
+# like RELEASE_OR_BETA which are set based on the build channel.
+skip-if = !nightly_build && (asan || debug)
+[test_FeatureGateImplementation.js]