diff options
Diffstat (limited to '')
-rw-r--r-- | browser/components/asrouter/tests/unit/ASRouterPreferences.test.js | 480 |
1 files changed, 480 insertions, 0 deletions
diff --git a/browser/components/asrouter/tests/unit/ASRouterPreferences.test.js b/browser/components/asrouter/tests/unit/ASRouterPreferences.test.js new file mode 100644 index 0000000000..a3fe1fc5c9 --- /dev/null +++ b/browser/components/asrouter/tests/unit/ASRouterPreferences.test.js @@ -0,0 +1,480 @@ +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 + ); + }); + }); +}); |