Source code

Revision control

Copy as Markdown

Other Tools

import {
_ASRouterPreferences,
ASRouterPreferences as ASRouterPreferencesSingleton,
TEST_PROVIDERS,
} from "modules/ASRouterPreferences.sys.mjs";
const FAKE_PROVIDERS = [{ id: "foo" }, { id: "bar" }];
const PROVIDER_PREF_BRANCH =
"browser.newtabpage.activity-stream.asrouter.providers.";
const DEVTOOLS_PREF =
"browser.newtabpage.activity-stream.asrouter.devtoolsEnabled";
const CFR_USER_PREF_ADDONS =
"browser.newtabpage.activity-stream.asrouter.userprefs.cfr.addons";
const CFR_USER_PREF_FEATURES =
"browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features";
/** NUMBER_OF_PREFS_TO_OBSERVE includes:
* 1. asrouter.providers. pref branch
* 2. asrouter.devtoolsEnabled
* 3. browser.newtabpage.activity-stream.asrouter.userprefs.cfr.addons (user preference - cfr)
* 4. browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features (user preference - cfr)
* 5. services.sync.username
*/
const NUMBER_OF_PREFS_TO_OBSERVE = 5;
describe("ASRouterPreferences", () => {
let ASRouterPreferences;
let sandbox;
let addObserverStub;
let stringPrefStub;
let boolPrefStub;
let resetStub;
let hasUserValueStub;
let childListStub;
let setStringPrefStub;
beforeEach(() => {
ASRouterPreferences = new _ASRouterPreferences();
sandbox = sinon.createSandbox();
addObserverStub = sandbox.stub(global.Services.prefs, "addObserver");
stringPrefStub = sandbox.stub(global.Services.prefs, "getStringPref");
resetStub = sandbox.stub(global.Services.prefs, "clearUserPref");
setStringPrefStub = sandbox.stub(global.Services.prefs, "setStringPref");
FAKE_PROVIDERS.forEach(provider => {
stringPrefStub
.withArgs(`${PROVIDER_PREF_BRANCH}${provider.id}`)
.returns(JSON.stringify(provider));
});
boolPrefStub = sandbox
.stub(global.Services.prefs, "getBoolPref")
.returns(false);
hasUserValueStub = sandbox
.stub(global.Services.prefs, "prefHasUserValue")
.returns(false);
childListStub = sandbox.stub(global.Services.prefs, "getChildList");
childListStub
.withArgs(PROVIDER_PREF_BRANCH)
.returns(
FAKE_PROVIDERS.map(provider => `${PROVIDER_PREF_BRANCH}${provider.id}`)
);
});
afterEach(() => {
sandbox.restore();
});
function getPrefNameForProvider(providerId) {
return `${PROVIDER_PREF_BRANCH}${providerId}`;
}
function setPrefForProvider(providerId, value) {
stringPrefStub
.withArgs(getPrefNameForProvider(providerId))
.returns(JSON.stringify(value));
}
it("ASRouterPreferences should be an instance of _ASRouterPreferences", () => {
assert.instanceOf(ASRouterPreferencesSingleton, _ASRouterPreferences);
});
describe("#init", () => {
it("should set ._initialized to true", () => {
ASRouterPreferences.init();
assert.isTrue(ASRouterPreferences._initialized);
});
it("should migrate the provider prefs", () => {
ASRouterPreferences.uninit();
// Should be migrated because they contain bucket and not collection
const MIGRATE_PROVIDERS = [
{ id: "baz", bucket: "buk" },
{ id: "qux", bucket: "buk" },
];
// Should be cleared to defaults because it throws on setStringPref
const ERROR_PROVIDER = { id: "err", bucket: "buk" };
// Should not be migrated because, although modified, it lacks bucket
const MODIFIED_SAFE_PROVIDER = { id: "safe" };
const ALL_PROVIDERS = [
...MIGRATE_PROVIDERS,
...FAKE_PROVIDERS, // Should not be migrated because they're unmodified
MODIFIED_SAFE_PROVIDER,
ERROR_PROVIDER,
];
// The migrator should attempt to read prefs for all of these providers
const TRY_PROVIDERS = [
...MIGRATE_PROVIDERS,
MODIFIED_SAFE_PROVIDER,
ERROR_PROVIDER,
];
// Update the full list of provider prefs
childListStub
.withArgs(PROVIDER_PREF_BRANCH)
.returns(
ALL_PROVIDERS.map(provider => getPrefNameForProvider(provider.id))
);
// Stub the pref values so the migrator can read them
ALL_PROVIDERS.forEach(provider => {
stringPrefStub
.withArgs(getPrefNameForProvider(provider.id))
.returns(JSON.stringify(provider));
});
// Consider these providers' prefs "modified"
TRY_PROVIDERS.forEach(provider => {
hasUserValueStub
.withArgs(`${PROVIDER_PREF_BRANCH}${provider.id}`)
.returns(true);
});
// Spoof an error when trying to set the pref for this provider so we can
// test that the pref is gracefully reset on error
setStringPrefStub
.withArgs(getPrefNameForProvider(ERROR_PROVIDER.id))
.throws();
ASRouterPreferences.init();
// The migrator should have tried to check each pref for user modification
ALL_PROVIDERS.forEach(provider =>
assert.calledWith(hasUserValueStub, getPrefNameForProvider(provider.id))
);
// Test that we don't call getStringPref for providers that don't have a
// user-defined value
FAKE_PROVIDERS.forEach(provider =>
assert.neverCalledWith(
stringPrefStub,
getPrefNameForProvider(provider.id)
)
);
// But we do call it for providers that do have a user-defined value
TRY_PROVIDERS.forEach(provider =>
assert.calledWith(stringPrefStub, getPrefNameForProvider(provider.id))
);
// Test that we don't call setStringPref to migrate providers that don't
// have a bucket property
assert.neverCalledWith(
setStringPrefStub,
getPrefNameForProvider(MODIFIED_SAFE_PROVIDER.id)
);
/**
* For a given provider, return a sinon matcher that matches if the value
* looks like a migrated version of the original provider. Requires that:
* its id matches the original provider's id; it has no bucket; and its
* collection is set to the value of the original provider's bucket.
* @param {object} provider the provider object to compare to
* @returns {object} custom matcher object for sinon
*/
function providerJsonMatches(provider) {
return sandbox.match(migrated => {
const parsed = JSON.parse(migrated);
return (
parsed.id === provider.id &&
!("bucket" in parsed) &&
parsed.collection === provider.bucket
);
});
}
// Test that we call setStringPref to migrate providers that have a bucket
// property and don't have a collection property
MIGRATE_PROVIDERS.forEach(provider =>
assert.calledWith(
setStringPrefStub,
getPrefNameForProvider(provider.id),
providerJsonMatches(provider) // Verify the migrated pref value
)
);
// Test that we clear the pref for providers that throw when we try to
// read or write them
assert.calledWith(resetStub, getPrefNameForProvider(ERROR_PROVIDER.id));
});
it(`should set ${NUMBER_OF_PREFS_TO_OBSERVE} observers and not re-initialize if already initialized`, () => {
ASRouterPreferences.init();
assert.callCount(addObserverStub, NUMBER_OF_PREFS_TO_OBSERVE);
ASRouterPreferences.init();
ASRouterPreferences.init();
assert.callCount(addObserverStub, NUMBER_OF_PREFS_TO_OBSERVE);
});
});
describe("#uninit", () => {
it("should set ._initialized to false", () => {
ASRouterPreferences.init();
ASRouterPreferences.uninit();
assert.isFalse(ASRouterPreferences._initialized);
});
it("should clear cached values for ._initialized, .devtoolsEnabled", () => {
ASRouterPreferences.init();
// trigger caching
// eslint-disable-next-line no-unused-vars
const result = [
ASRouterPreferences.providers,
ASRouterPreferences.devtoolsEnabled,
];
assert.isNotNull(
ASRouterPreferences._providers,
"providers should not be null"
);
assert.isNotNull(
ASRouterPreferences._devtoolsEnabled,
"devtolosEnabled should not be null"
);
ASRouterPreferences.uninit();
assert.isNull(ASRouterPreferences._providers);
assert.isNull(ASRouterPreferences._devtoolsEnabled);
});
it("should clear all listeners and remove observers (only once)", () => {
const removeStub = sandbox.stub(global.Services.prefs, "removeObserver");
ASRouterPreferences.init();
ASRouterPreferences.addListener(() => {});
ASRouterPreferences.addListener(() => {});
assert.equal(ASRouterPreferences._callbacks.size, 2);
ASRouterPreferences.uninit();
// Tests to make sure we don't remove observers that weren't set
ASRouterPreferences.uninit();
assert.callCount(removeStub, NUMBER_OF_PREFS_TO_OBSERVE);
assert.calledWith(removeStub, PROVIDER_PREF_BRANCH);
assert.calledWith(removeStub, DEVTOOLS_PREF);
assert.isEmpty(ASRouterPreferences._callbacks);
});
});
describe(".providers", () => {
it("should return the value the first time .providers is accessed", () => {
ASRouterPreferences.init();
const result = ASRouterPreferences.providers;
assert.deepEqual(result, FAKE_PROVIDERS);
// once per pref
assert.calledTwice(stringPrefStub);
});
it("should return the cached value the second time .providers is accessed", () => {
ASRouterPreferences.init();
const [, secondCall] = [
ASRouterPreferences.providers,
ASRouterPreferences.providers,
];
assert.deepEqual(secondCall, FAKE_PROVIDERS);
// once per pref
assert.calledTwice(stringPrefStub);
});
it("should just parse the pref each time if ASRouterPreferences hasn't been initialized yet", () => {
// Intentionally not initialized
const [firstCall, secondCall] = [
ASRouterPreferences.providers,
ASRouterPreferences.providers,
];
assert.deepEqual(firstCall, FAKE_PROVIDERS);
assert.deepEqual(secondCall, FAKE_PROVIDERS);
assert.callCount(stringPrefStub, 4);
});
it("should skip the pref without throwing if a pref is not parsable", () => {
stringPrefStub.withArgs(`${PROVIDER_PREF_BRANCH}foo`).returns("not json");
ASRouterPreferences.init();
assert.deepEqual(ASRouterPreferences.providers, [{ id: "bar" }]);
});
it("should include TEST_PROVIDERS if devtools is turned on", () => {
boolPrefStub.withArgs(DEVTOOLS_PREF).returns(true);
ASRouterPreferences.init();
assert.deepEqual(ASRouterPreferences.providers, [
...TEST_PROVIDERS,
...FAKE_PROVIDERS,
]);
});
});
describe(".devtoolsEnabled", () => {
it("should read the pref the first time .devtoolsEnabled is accessed", () => {
ASRouterPreferences.init();
const result = ASRouterPreferences.devtoolsEnabled;
assert.deepEqual(result, false);
assert.calledOnce(boolPrefStub);
});
it("should return the cached value the second time .devtoolsEnabled is accessed", () => {
ASRouterPreferences.init();
const [, secondCall] = [
ASRouterPreferences.devtoolsEnabled,
ASRouterPreferences.devtoolsEnabled,
];
assert.deepEqual(secondCall, false);
assert.calledOnce(boolPrefStub);
});
it("should just parse the pref each time if ASRouterPreferences hasn't been initialized yet", () => {
// Intentionally not initialized
const [firstCall, secondCall] = [
ASRouterPreferences.devtoolsEnabled,
ASRouterPreferences.devtoolsEnabled,
];
assert.deepEqual(firstCall, false);
assert.deepEqual(secondCall, false);
assert.calledTwice(boolPrefStub);
});
});
describe("#getAllUserPreferences", () => {
it("should return all user preferences", () => {
boolPrefStub.withArgs(CFR_USER_PREF_ADDONS).returns(false);
boolPrefStub.withArgs(CFR_USER_PREF_FEATURES).returns(true);
const result = ASRouterPreferences.getAllUserPreferences();
assert.deepEqual(result, {
cfrAddons: false,
cfrFeatures: true,
});
});
});
describe("#enableOrDisableProvider", () => {
it("should enable an existing provider if second param is true", () => {
setPrefForProvider("foo", { id: "foo", enabled: false });
assert.isFalse(ASRouterPreferences.providers[0].enabled);
ASRouterPreferences.enableOrDisableProvider("foo", true);
assert.calledWith(
setStringPrefStub,
getPrefNameForProvider("foo"),
JSON.stringify({ id: "foo", enabled: true })
);
});
it("should disable an existing provider if second param is false", () => {
setPrefForProvider("foo", { id: "foo", enabled: true });
assert.isTrue(ASRouterPreferences.providers[0].enabled);
ASRouterPreferences.enableOrDisableProvider("foo", false);
assert.calledWith(
setStringPrefStub,
getPrefNameForProvider("foo"),
JSON.stringify({ id: "foo", enabled: false })
);
});
it("should not throw if the id does not exist", () => {
assert.doesNotThrow(() => {
ASRouterPreferences.enableOrDisableProvider("does_not_exist", true);
});
});
it("should not throw if pref is not parseable", () => {
stringPrefStub
.withArgs(getPrefNameForProvider("foo"))
.returns("not valid");
assert.doesNotThrow(() => {
ASRouterPreferences.enableOrDisableProvider("foo", true);
});
});
});
describe("#setUserPreference", () => {
it("should do nothing if the pref doesn't exist", () => {
ASRouterPreferences.setUserPreference("foo", true);
assert.notCalled(boolPrefStub);
});
it("should set the given pref", () => {
const setStub = sandbox.stub(global.Services.prefs, "setBoolPref");
ASRouterPreferences.setUserPreference("cfrAddons", true);
assert.calledWith(setStub, CFR_USER_PREF_ADDONS, true);
});
});
describe("#resetProviderPref", () => {
it("should reset the pref and user prefs", () => {
ASRouterPreferences.resetProviderPref();
FAKE_PROVIDERS.forEach(provider => {
assert.calledWith(resetStub, getPrefNameForProvider(provider.id));
});
assert.calledWith(resetStub, CFR_USER_PREF_ADDONS);
assert.calledWith(resetStub, CFR_USER_PREF_FEATURES);
});
});
describe("observer, listeners", () => {
it("should invalidate .providers when the pref is changed", () => {
const testProvider = { id: "newstuff" };
const newProviders = [...FAKE_PROVIDERS, testProvider];
ASRouterPreferences.init();
assert.deepEqual(ASRouterPreferences.providers, FAKE_PROVIDERS);
stringPrefStub
.withArgs(getPrefNameForProvider(testProvider.id))
.returns(JSON.stringify(testProvider));
childListStub
.withArgs(PROVIDER_PREF_BRANCH)
.returns(
newProviders.map(provider => getPrefNameForProvider(provider.id))
);
ASRouterPreferences.observe(
null,
null,
getPrefNameForProvider(testProvider.id)
);
// Cache should be invalidated so we access the new value of the pref now
assert.deepEqual(ASRouterPreferences.providers, newProviders);
});
it("should invalidate .devtoolsEnabled and .providers when the pref is changed", () => {
ASRouterPreferences.init();
assert.isFalse(ASRouterPreferences.devtoolsEnabled);
boolPrefStub.withArgs(DEVTOOLS_PREF).returns(true);
childListStub.withArgs(PROVIDER_PREF_BRANCH).returns([]);
ASRouterPreferences.observe(null, null, DEVTOOLS_PREF);
// Cache should be invalidated so we access the new value of the pref now
// Note that providers needs to be invalidated because devtools adds test content to it.
assert.isTrue(ASRouterPreferences.devtoolsEnabled);
assert.deepEqual(ASRouterPreferences.providers, TEST_PROVIDERS);
});
it("should call listeners added with .addListener", () => {
const callback1 = sinon.stub();
const callback2 = sinon.stub();
ASRouterPreferences.init();
ASRouterPreferences.addListener(callback1);
ASRouterPreferences.addListener(callback2);
ASRouterPreferences.observe(null, null, getPrefNameForProvider("foo"));
assert.calledWith(callback1, getPrefNameForProvider("foo"));
ASRouterPreferences.observe(null, null, DEVTOOLS_PREF);
assert.calledWith(callback2, DEVTOOLS_PREF);
});
it("should not call listeners after they are removed with .removeListeners", () => {
const callback = sinon.stub();
ASRouterPreferences.init();
ASRouterPreferences.addListener(callback);
ASRouterPreferences.observe(null, null, getPrefNameForProvider("foo"));
assert.calledWith(callback, getPrefNameForProvider("foo"));
callback.reset();
ASRouterPreferences.removeListener(callback);
ASRouterPreferences.observe(null, null, DEVTOOLS_PREF);
assert.notCalled(callback);
});
});
describe("#_transformPersonalizedCfrScores", () => {
it("should report JSON.parse errors", () => {
sandbox.stub(global.console, "error");
ASRouterPreferences._transformPersonalizedCfrScores("");
assert.calledOnce(global.console.error);
});
it("should return an object parsed from a string", () => {
const scores = { FOO: 3000, BAR: 4000 };
assert.deepEqual(
ASRouterPreferences._transformPersonalizedCfrScores(
JSON.stringify(scores)
),
scores
);
});
});
});