diff options
Diffstat (limited to 'browser/components/newtab/test/xpcshell/test_Store.js')
-rw-r--r-- | browser/components/newtab/test/xpcshell/test_Store.js | 453 |
1 files changed, 453 insertions, 0 deletions
diff --git a/browser/components/newtab/test/xpcshell/test_Store.js b/browser/components/newtab/test/xpcshell/test_Store.js new file mode 100644 index 0000000000..b05ad36cd6 --- /dev/null +++ b/browser/components/newtab/test/xpcshell/test_Store.js @@ -0,0 +1,453 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { Store } = ChromeUtils.importESModule( + "resource://activity-stream/lib/Store.sys.mjs" +); +const { ActivityStreamMessageChannel } = ChromeUtils.importESModule( + "resource://activity-stream/lib/ActivityStreamMessageChannel.sys.mjs" +); +const { ActivityStreamStorage } = ChromeUtils.importESModule( + "resource://activity-stream/lib/ActivityStreamStorage.sys.mjs" +); +const { sinon } = ChromeUtils.importESModule( + "resource://testing-common/Sinon.sys.mjs" +); + +// This creates the Redux top-level object. +/* globals Redux */ +Services.scriptloader.loadSubScript( + "resource://activity-stream/vendor/redux.js", + this +); + +add_task(async function test_expected_properties() { + let sandbox = sinon.createSandbox(); + let store = new Store(); + + Assert.equal(store.feeds.constructor.name, "Map", "Should create a Map"); + Assert.equal(store.feeds.size, 0, "Store should start without any feeds."); + + Assert.ok(store._store, "Has a ._store"); + Assert.ok(store.dispatch, "Has a .dispatch"); + Assert.ok(store.getState, "Has a .getState"); + + sandbox.restore(); +}); + +add_task(async function test_messagechannel() { + let sandbox = sinon.createSandbox(); + sandbox + .stub(ActivityStreamMessageChannel.prototype, "middleware") + .returns(s => next => action => next(action)); + let store = new Store(); + + info( + "Store should create a ActivityStreamMessageChannel with the right dispatcher" + ); + Assert.ok(store.getMessageChannel(), "Has a message channel"); + Assert.equal( + store.getMessageChannel().dispatch, + store.dispatch, + "MessageChannel.dispatch forwards to store.dispatch" + ); + Assert.equal( + store.getMessageChannel(), + store._messageChannel, + "_messageChannel is the member for getMessageChannel()" + ); + + store.dispatch({ type: "FOO" }); + Assert.ok( + ActivityStreamMessageChannel.prototype.middleware.calledOnce, + "Middleware called." + ); + sandbox.restore(); +}); + +add_task(async function test_initFeed_add_feeds() { + info("Store.initFeed should add an instance of the feed to .feeds"); + + let sandbox = sinon.createSandbox(); + let store = new Store(); + class Foo {} + store._prefs.set("foo", true); + await store.init(new Map([["foo", () => new Foo()]])); + store.initFeed("foo"); + + Assert.ok(store.feeds.has("foo"), "foo is set"); + Assert.ok(store.feeds.get("foo") instanceof Foo, "Got registered class"); + sandbox.restore(); +}); + +add_task(async function test_initFeed_calls_onAction() { + info("Store should call the feed's onAction with uninit action if it exists"); + + let sandbox = sinon.createSandbox(); + let store = new Store(); + let testFeed; + let createTestFeed = () => { + testFeed = { onAction: sandbox.spy() }; + return testFeed; + }; + const action = { type: "FOO" }; + store._feedFactories = new Map([["test", createTestFeed]]); + + store.initFeed("test", action); + + Assert.ok(testFeed.onAction.calledOnce, "onAction called"); + Assert.ok( + testFeed.onAction.calledWith(action), + "onAction called with test action" + ); + + info("Store should add a .store property to the feed"); + Assert.ok(testFeed.store, "Store exists"); + Assert.equal(testFeed.store, store, "Feed store is the Store"); + sandbox.restore(); +}); + +add_task(async function test_initFeed_on_init() { + info("Store should call .initFeed with each key"); + + let sandbox = sinon.createSandbox(); + let store = new Store(); + + sandbox.stub(store, "initFeed"); + store._prefs.set("foo", true); + store._prefs.set("bar", true); + await store.init( + new Map([ + ["foo", () => {}], + ["bar", () => {}], + ]) + ); + Assert.ok(store.initFeed.calledWith("foo"), "First test feed initted"); + Assert.ok(store.initFeed.calledWith("bar"), "Second test feed initted"); + sandbox.restore(); +}); + +add_task(async function test_initFeed_calls__initIndexedDB() { + info("Store should call _initIndexedDB"); + let sandbox = sinon.createSandbox(); + let store = new Store(); + + sandbox.spy(store, "_initIndexedDB"); + + let dbStub = sandbox.stub(ActivityStreamStorage.prototype, "db"); + let dbAccessed = false; + dbStub.get(() => { + dbAccessed = true; + return {}; + }); + + store._prefs.set("testfeed", true); + await store.init( + new Map([ + [ + "testfeed", + () => { + return {}; + }, + ], + ]) + ); + + Assert.ok(store._initIndexedDB.calledOnce, "_initIndexedDB called once"); + Assert.ok( + store._initIndexedDB.calledWithExactly("feeds.telemetry"), + "feeds.telemetry was passed" + ); + // Due to what appears to be a bug in sinon when using calledOnce + // with a stubbed getter, we can't just use dbStub.calledOnce here. + Assert.ok(dbAccessed, "ActivityStreamStorage was accessed"); + + info( + "Store should reset ActivityStreamStorage telemetry if opening the db fails" + ); + dbStub.rejects(); + await store.init(new Map()); + + Assert.equal( + store.dbStorage.telemetry, + null, + "Telemetry on storage was cleared" + ); + + sandbox.restore(); +}); + +add_task(async function test_disabled_feed() { + info("Store should not initialize the feed if the Pref is set to false"); + + let sandbox = sinon.createSandbox(); + let store = new Store(); + + sandbox.stub(store, "initFeed"); + store._prefs.set("foo", false); + await store.init(new Map([["foo", () => {}]])); + Assert.ok(store.initFeed.notCalled, ".initFeed not called"); + + store._prefs.set("foo", true); + + sandbox.restore(); +}); + +add_task(async function test_observe_pref_branch() { + info("Store should observe the pref branch"); + + let sandbox = sinon.createSandbox(); + let store = new Store(); + + sandbox.stub(store._prefs, "observeBranch"); + await store.init(new Map()); + Assert.ok(store._prefs.observeBranch.calledOnce, "observeBranch called once"); + Assert.ok( + store._prefs.observeBranch.calledWith(store), + "observeBranch passed the store" + ); + + sandbox.restore(); +}); + +add_task(async function test_emit_initial_event() { + info("Store should emit an initial event if provided"); + + let sandbox = sinon.createSandbox(); + let store = new Store(); + + const action = { type: "FOO" }; + sandbox.stub(store, "dispatch"); + await store.init(new Map(), action); + Assert.ok(store.dispatch.calledOnce, "Dispatch called once"); + Assert.ok(store.dispatch.calledWith(action), "Dispatch called with action"); + + sandbox.restore(); +}); + +add_task(async function test_initialize_telemetry_feed_first() { + info("Store should initialize the telemetry feed first"); + + let sandbox = sinon.createSandbox(); + let store = new Store(); + + store._prefs.set("feeds.foo", true); + store._prefs.set("feeds.telemetry", true); + const telemetrySpy = sandbox.stub().returns({}); + const fooSpy = sandbox.stub().returns({}); + // Intentionally put the telemetry feed as the second item. + const feedFactories = new Map([ + ["feeds.foo", fooSpy], + ["feeds.telemetry", telemetrySpy], + ]); + await store.init(feedFactories); + Assert.ok(telemetrySpy.calledBefore(fooSpy), "Telemetry feed initted first"); + + sandbox.restore(); +}); + +add_task(async function test_dispatch_init_load_events() { + info("Store should dispatch init/load events"); + + let sandbox = sinon.createSandbox(); + let store = new Store(); + + sandbox.stub(store.getMessageChannel(), "simulateMessagesForExistingTabs"); + await store.init(new Map(), { type: "FOO" }); + Assert.ok( + store.getMessageChannel().simulateMessagesForExistingTabs.calledOnce, + "simulateMessagesForExistingTabs called once" + ); + + sandbox.restore(); +}); + +add_task(async function test_init_before_load() { + info("Store should dispatch INIT before LOAD"); + + let sandbox = sinon.createSandbox(); + let store = new Store(); + + sandbox.stub(store.getMessageChannel(), "simulateMessagesForExistingTabs"); + sandbox.stub(store, "dispatch"); + const init = { type: "INIT" }; + const load = { type: "TAB_LOAD" }; + store + .getMessageChannel() + .simulateMessagesForExistingTabs.callsFake(() => store.dispatch(load)); + await store.init(new Map(), init); + + Assert.ok(store.dispatch.calledTwice, "Dispatch called twice"); + Assert.equal( + store.dispatch.firstCall.args[0], + init, + "First dispatch was for init event" + ); + Assert.equal( + store.dispatch.secondCall.args[0], + load, + "Second dispatch was for load event" + ); + + sandbox.restore(); +}); + +add_task(async function test_uninit_feeds() { + info("uninitFeed should not throw if no feed with that name exists"); + + let sandbox = sinon.createSandbox(); + let store = new Store(); + + try { + store.uninitFeed("does-not-exist"); + Assert.ok(true, "Didn't throw"); + } catch (e) { + Assert.ok(false, "Should not have thrown"); + } + + info( + "uninitFeed should call the feed's onAction with uninit action if it exists" + ); + let feed; + function createFeed() { + feed = { onAction: sandbox.spy() }; + return feed; + } + const action = { type: "BAR" }; + store._feedFactories = new Map([["foo", createFeed]]); + store.initFeed("foo"); + + store.uninitFeed("foo", action); + + Assert.ok(feed.onAction.calledOnce); + Assert.ok(feed.onAction.calledWith(action)); + + info("uninitFeed should remove the feed from .feeds"); + Assert.ok(!store.feeds.has("foo"), "foo is not in .feeds"); + + sandbox.restore(); +}); + +add_task(async function test_onPrefChanged() { + let sandbox = sinon.createSandbox(); + let store = new Store(); + let initFeedStub = sandbox.stub(store, "initFeed"); + let uninitFeedStub = sandbox.stub(store, "uninitFeed"); + store._prefs.set("foo", false); + store.init(new Map([["foo", () => ({})]])); + + info("onPrefChanged should initialize the feed if called with true"); + store.onPrefChanged("foo", true); + Assert.ok(initFeedStub.calledWith("foo")); + Assert.ok(!uninitFeedStub.calledOnce); + initFeedStub.resetHistory(); + uninitFeedStub.resetHistory(); + + info("onPrefChanged should uninitialize the feed if called with false"); + store.onPrefChanged("foo", false); + Assert.ok(uninitFeedStub.calledWith("foo")); + Assert.ok(!initFeedStub.calledOnce); + initFeedStub.resetHistory(); + uninitFeedStub.resetHistory(); + + info("onPrefChanged should do nothing if not an expected feed"); + store.onPrefChanged("bar", false); + + Assert.ok(!initFeedStub.calledOnce); + Assert.ok(!uninitFeedStub.calledOnce); + sandbox.restore(); +}); + +add_task(async function test_uninit() { + let sandbox = sinon.createSandbox(); + let store = new Store(); + let dispatchStub = sandbox.stub(store, "dispatch"); + const action = { type: "BAR" }; + await store.init(new Map(), null, action); + store.uninit(); + + Assert.ok(store.dispatch.calledOnce); + Assert.ok(store.dispatch.calledWith(action)); + + info("Store.uninit should clear .feeds and ._feedFactories"); + store._prefs.set("a", true); + await store.init( + new Map([ + ["a", () => ({})], + ["b", () => ({})], + ["c", () => ({})], + ]) + ); + + store.uninit(); + + Assert.equal(store.feeds.size, 0); + Assert.equal(store._feedFactories, null); + + info("Store.uninit should emit an uninit event if provided on init"); + dispatchStub.resetHistory(); + const uninitAction = { type: "BAR" }; + await store.init(new Map(), null, uninitAction); + store.uninit(); + + Assert.ok(store.dispatch.calledOnce); + Assert.ok(store.dispatch.calledWith(uninitAction)); + sandbox.restore(); +}); + +add_task(async function test_getState() { + info("Store.getState should return the redux state"); + let sandbox = sinon.createSandbox(); + let store = new Store(); + store._store = Redux.createStore((prevState = 123) => prevState); + const { getState } = store; + Assert.equal(getState(), 123); + sandbox.restore(); +}); + +/** + * addNumberReducer - a simple dummy reducer for testing that adds a number + */ +function addNumberReducer(prevState = 0, action) { + return action.type === "ADD" ? prevState + action.data : prevState; +} + +add_task(async function test_dispatch() { + info("Store.dispatch should call .onAction of each feed"); + let sandbox = sinon.createSandbox(); + let store = new Store(); + const { dispatch } = store; + const sub = { onAction: sinon.spy() }; + const action = { type: "FOO" }; + + store._prefs.set("sub", true); + await store.init(new Map([["sub", () => sub]])); + + dispatch(action); + + Assert.ok(sub.onAction.calledWith(action)); + + info("Sandbox.dispatch should call the reducers"); + + store._store = Redux.createStore(addNumberReducer); + dispatch({ type: "ADD", data: 14 }); + Assert.equal(store.getState(), 14); + + sandbox.restore(); +}); + +add_task(async function test_subscribe() { + info("Store.subscribe should subscribe to changes to the store"); + let sandbox = sinon.createSandbox(); + let store = new Store(); + const sub = sandbox.spy(); + const action = { type: "FOO" }; + + store.subscribe(sub); + store.dispatch(action); + + Assert.ok(sub.calledOnce); + sandbox.restore(); +}); |