import { addNumberReducer, FakePrefs } from "test/unit/utils"; import { createStore } from "redux"; import injector from "inject!lib/Store.jsm"; describe("Store", () => { let Store; let sandbox; let store; let dbStub; beforeEach(() => { sandbox = sinon.createSandbox(); function ActivityStreamMessageChannel(options) { this.dispatch = options.dispatch; this.createChannel = sandbox.spy(); this.destroyChannel = sandbox.spy(); this.middleware = sandbox.spy(s => next => action => next(action)); this.simulateMessagesForExistingTabs = sandbox.stub(); } dbStub = sandbox.stub().resolves(); function FakeActivityStreamStorage() { this.db = {}; sinon.stub(this, "db").get(dbStub); } ({ Store } = injector({ "lib/ActivityStreamMessageChannel.jsm": { ActivityStreamMessageChannel }, "lib/ActivityStreamPrefs.jsm": { Prefs: FakePrefs }, "lib/ActivityStreamStorage.jsm": { ActivityStreamStorage: FakeActivityStreamStorage, }, })); store = new Store(); sandbox.stub(store, "_initIndexedDB").resolves(); }); afterEach(() => { sandbox.restore(); }); it("should have a .feeds property that is a Map", () => { assert.instanceOf(store.feeds, Map); assert.equal(store.feeds.size, 0, ".feeds.size"); }); it("should have a redux store at ._store", () => { assert.ok(store._store); assert.property(store, "dispatch"); assert.property(store, "getState"); }); it("should create a ActivityStreamMessageChannel with the right dispatcher", () => { assert.ok(store.getMessageChannel()); assert.equal(store.getMessageChannel().dispatch, store.dispatch); assert.equal(store.getMessageChannel(), store._messageChannel); }); it("should connect the ActivityStreamMessageChannel's middleware", () => { store.dispatch({ type: "FOO" }); assert.calledOnce(store._messageChannel.middleware); }); describe("#initFeed", () => { it("should add an instance of the feed to .feeds", () => { class Foo {} store._prefs.set("foo", true); store.init(new Map([["foo", () => new Foo()]])); store.initFeed("foo"); assert.isTrue(store.feeds.has("foo"), "foo is set"); assert.instanceOf(store.feeds.get("foo"), Foo); }); it("should call the feed's onAction with uninit action if it exists", () => { let feed; function createFeed() { feed = { onAction: sinon.spy() }; return feed; } const action = { type: "FOO" }; store._feedFactories = new Map([["foo", createFeed]]); store.initFeed("foo", action); assert.calledOnce(feed.onAction); assert.calledWith(feed.onAction, action); }); it("should add a .store property to the feed", () => { class Foo {} store._feedFactories = new Map([["foo", () => new Foo()]]); store.initFeed("foo"); assert.propertyVal(store.feeds.get("foo"), "store", store); }); }); describe("#uninitFeed", () => { it("should not throw if no feed with that name exists", () => { assert.doesNotThrow(() => { store.uninitFeed("bar"); }); }); it("should call the feed's onAction with uninit action if it exists", () => { let feed; function createFeed() { feed = { onAction: sinon.spy() }; return feed; } const action = { type: "BAR" }; store._feedFactories = new Map([["foo", createFeed]]); store.initFeed("foo"); store.uninitFeed("foo", action); assert.calledOnce(feed.onAction); assert.calledWith(feed.onAction, action); }); it("should remove the feed from .feeds", () => { class Foo {} store._feedFactories = new Map([["foo", () => new Foo()]]); store.initFeed("foo"); store.uninitFeed("foo"); assert.isFalse(store.feeds.has("foo"), "foo is not in .feeds"); }); }); describe("onPrefChanged", () => { beforeEach(() => { sinon.stub(store, "initFeed"); sinon.stub(store, "uninitFeed"); store._prefs.set("foo", false); store.init(new Map([["foo", () => ({})]])); }); it("should initialize the feed if called with true", () => { store.onPrefChanged("foo", true); assert.calledWith(store.initFeed, "foo"); assert.notCalled(store.uninitFeed); }); it("should uninitialize the feed if called with false", () => { store.onPrefChanged("foo", false); assert.calledWith(store.uninitFeed, "foo"); assert.notCalled(store.initFeed); }); it("should do nothing if not an expected feed", () => { store.onPrefChanged("bar", false); assert.notCalled(store.initFeed); assert.notCalled(store.uninitFeed); }); }); describe("#init", () => { it("should call .initFeed with each key", async () => { sinon.stub(store, "initFeed"); store._prefs.set("foo", true); store._prefs.set("bar", true); await store.init( new Map([ ["foo", () => {}], ["bar", () => {}], ]) ); assert.calledWith(store.initFeed, "foo"); assert.calledWith(store.initFeed, "bar"); }); it("should call _initIndexedDB", async () => { await store.init(new Map()); assert.calledOnce(store._initIndexedDB); assert.calledWithExactly(store._initIndexedDB, "feeds.telemetry"); }); it("should access the db property of indexedDB", async () => { store._initIndexedDB.restore(); await store.init(new Map()); assert.calledOnce(dbStub); }); it("should reset ActivityStreamStorage telemetry if opening the db fails", async () => { store._initIndexedDB.restore(); // Force an IndexedDB error dbStub.rejects(); await store.init(new Map()); assert.calledOnce(dbStub); assert.isNull(store.dbStorage.telemetry); }); it("should not initialize the feed if the Pref is set to false", async () => { sinon.stub(store, "initFeed"); store._prefs.set("foo", false); await store.init(new Map([["foo", () => {}]])); assert.notCalled(store.initFeed); }); it("should observe the pref branch", async () => { sinon.stub(store._prefs, "observeBranch"); await store.init(new Map()); assert.calledOnce(store._prefs.observeBranch); assert.calledWith(store._prefs.observeBranch, store); }); it("should initialize the ActivityStreamMessageChannel channel", async () => { await store.init(new Map()); }); it("should emit an initial event if provided", async () => { sinon.stub(store, "dispatch"); const action = { type: "FOO" }; await store.init(new Map(), action); assert.calledOnce(store.dispatch); assert.calledWith(store.dispatch, action); }); it("should initialize the telemtry feed first", () => { 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], ]); store.init(feedFactories); assert.ok(telemetrySpy.calledBefore(fooSpy)); }); it("should dispatch init/load events", async () => { await store.init(new Map(), { type: "FOO" }); assert.calledOnce( store.getMessageChannel().simulateMessagesForExistingTabs ); }); it("should dispatch INIT before LOAD", async () => { const init = { type: "INIT" }; const load = { type: "TAB_LOAD" }; sandbox.stub(store, "dispatch"); store .getMessageChannel() .simulateMessagesForExistingTabs.callsFake(() => store.dispatch(load)); await store.init(new Map(), init); assert.calledTwice(store.dispatch); assert.equal(store.dispatch.firstCall.args[0], init); assert.equal(store.dispatch.secondCall.args[0], load); }); }); describe("#uninit", () => { it("should emit an uninit event if provided on init", () => { sinon.stub(store, "dispatch"); const action = { type: "BAR" }; store.init(new Map(), null, action); store.uninit(); assert.calledOnce(store.dispatch); assert.calledWith(store.dispatch, action); }); it("should clear .feeds and ._feedFactories", () => { store._prefs.set("a", true); store.init( new Map([ ["a", () => ({})], ["b", () => ({})], ["c", () => ({})], ]) ); store.uninit(); assert.equal(store.feeds.size, 0); assert.isNull(store._feedFactories); }); }); describe("#getState", () => { it("should return the redux state", () => { store._store = createStore((prevState = 123) => prevState); const { getState } = store; assert.equal(getState(), 123); }); }); describe("#dispatch", () => { it("should call .onAction of each feed", async () => { 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.calledWith(sub.onAction, action); }); it("should call the reducers", () => { const { dispatch } = store; store._store = createStore(addNumberReducer); dispatch({ type: "ADD", data: 14 }); assert.equal(store.getState(), 14); }); }); describe("#subscribe", () => { it("should subscribe to changes to the store", () => { const sub = sinon.spy(); const action = { type: "FOO" }; store.subscribe(sub); store.dispatch(action); assert.calledOnce(sub); }); }); });