forked from mirrors/gecko-dev
446 lines
12 KiB
JavaScript
446 lines
12 KiB
JavaScript
/* Any copyright is dedicated to the Public Domain.
|
|
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
|
|
|
"use strict";
|
|
|
|
const { FeatureGate } = ChromeUtils.import(
|
|
"resource://featuregates/FeatureGate.jsm"
|
|
);
|
|
const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
|
|
const { AppConstants } = ChromeUtils.import(
|
|
"resource://gre/modules/AppConstants.jsm"
|
|
);
|
|
|
|
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.`
|
|
);
|
|
}
|
|
});
|
|
}
|