summaryrefslogtreecommitdiffstats
path: root/browser/components/newtab/test/unit/asrouter/ASRouterPreferences.test.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/newtab/test/unit/asrouter/ASRouterPreferences.test.js')
-rw-r--r--browser/components/newtab/test/unit/asrouter/ASRouterPreferences.test.js491
1 files changed, 491 insertions, 0 deletions
diff --git a/browser/components/newtab/test/unit/asrouter/ASRouterPreferences.test.js b/browser/components/newtab/test/unit/asrouter/ASRouterPreferences.test.js
new file mode 100644
index 0000000000..3ad759d6b9
--- /dev/null
+++ b/browser/components/newtab/test/unit/asrouter/ASRouterPreferences.test.js
@@ -0,0 +1,491 @@
+import {
+ _ASRouterPreferences,
+ ASRouterPreferences as ASRouterPreferencesSingleton,
+ TEST_PROVIDERS,
+} from "lib/ASRouterPreferences.jsm";
+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 SNIPPETS_USER_PREF = "browser.newtabpage.activity-stream.feeds.snippets";
+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.feeds.snippets (user preference - snippets)
+ * 4. 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 = 6;
+
+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("#getUserPreference(providerId)", () => {
+ it("should return the user preference for snippets", () => {
+ boolPrefStub.withArgs(SNIPPETS_USER_PREF).returns(true);
+ assert.isTrue(ASRouterPreferences.getUserPreference("snippets"));
+ });
+ });
+ describe("#getAllUserPreferences", () => {
+ it("should return all user preferences", () => {
+ boolPrefStub.withArgs(SNIPPETS_USER_PREF).returns(true);
+ boolPrefStub.withArgs(CFR_USER_PREF_ADDONS).returns(false);
+ boolPrefStub.withArgs(CFR_USER_PREF_FEATURES).returns(true);
+ const result = ASRouterPreferences.getAllUserPreferences();
+ assert.deepEqual(result, {
+ snippets: true,
+ 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("snippets", true);
+ assert.calledWith(setStub, SNIPPETS_USER_PREF, 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, SNIPPETS_USER_PREF);
+ 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
+ );
+ });
+ });
+});