import { actionCreators as ac, actionTypes as at, } from "common/Actions.sys.mjs"; import { addNumberReducer, GlobalOverrider } from "test/unit/utils"; import { EARLY_QUEUED_ACTIONS, INCOMING_MESSAGE_NAME, initStore, MERGE_STORE_ACTION, OUTGOING_MESSAGE_NAME, queueEarlyMessageMiddleware, rehydrationMiddleware, } from "content-src/lib/init-store"; describe("initStore", () => { let globals; let store; beforeEach(() => { globals = new GlobalOverrider(); globals.set("RPMSendAsyncMessage", globals.sandbox.spy()); globals.set("RPMAddMessageListener", globals.sandbox.spy()); store = initStore({ number: addNumberReducer }); }); afterEach(() => globals.restore()); it("should create a store with the provided reducers", () => { assert.ok(store); assert.property(store.getState(), "number"); }); it("should add a listener that dispatches actions", () => { assert.calledWith(global.RPMAddMessageListener, INCOMING_MESSAGE_NAME); const [, listener] = global.RPMAddMessageListener.firstCall.args; globals.sandbox.spy(store, "dispatch"); const message = { name: INCOMING_MESSAGE_NAME, data: { type: "FOO" } }; listener(message); assert.calledWith(store.dispatch, message.data); }); it("should not throw if RPMAddMessageListener is not defined", () => { // Note: this is being set/restored by GlobalOverrider delete global.RPMAddMessageListener; assert.doesNotThrow(() => initStore({ number: addNumberReducer })); }); it("should log errors from failed messages", () => { const [, callback] = global.RPMAddMessageListener.firstCall.args; globals.sandbox.stub(global.console, "error"); globals.sandbox.stub(store, "dispatch").throws(Error("failed")); const message = { name: INCOMING_MESSAGE_NAME, data: { type: MERGE_STORE_ACTION }, }; callback(message); assert.calledOnce(global.console.error); }); it("should replace the state if a MERGE_STORE_ACTION is dispatched", () => { store.dispatch({ type: MERGE_STORE_ACTION, data: { number: 42 } }); assert.deepEqual(store.getState(), { number: 42 }); }); it("should call .send and update the local store if an AlsoToMain action is dispatched", () => { const subscriber = sinon.spy(); const action = ac.AlsoToMain({ type: "FOO" }); store.subscribe(subscriber); store.dispatch(action); assert.calledWith( global.RPMSendAsyncMessage, OUTGOING_MESSAGE_NAME, action ); assert.calledOnce(subscriber); }); it("should call .send but not update the local store if an OnlyToMain action is dispatched", () => { const subscriber = sinon.spy(); const action = ac.OnlyToMain({ type: "FOO" }); store.subscribe(subscriber); store.dispatch(action); assert.calledWith( global.RPMSendAsyncMessage, OUTGOING_MESSAGE_NAME, action ); assert.notCalled(subscriber); }); it("should not send out other types of actions", () => { store.dispatch({ type: "FOO" }); assert.notCalled(global.RPMSendAsyncMessage); }); describe("rehydrationMiddleware", () => { it("should allow NEW_TAB_STATE_REQUEST to go through", () => { const action = ac.AlsoToMain({ type: at.NEW_TAB_STATE_REQUEST }); const next = sinon.spy(); rehydrationMiddleware(store)(next)(action); assert.calledWith(next, action); }); it("should dispatch an additional NEW_TAB_STATE_REQUEST if INIT was received after a request", () => { const requestAction = ac.AlsoToMain({ type: at.NEW_TAB_STATE_REQUEST }); const next = sinon.spy(); const dispatch = rehydrationMiddleware(store)(next); dispatch(requestAction); next.resetHistory(); dispatch({ type: at.INIT }); assert.calledWith(next, requestAction); }); it("should allow MERGE_STORE_ACTION to go through", () => { const action = { type: MERGE_STORE_ACTION }; const next = sinon.spy(); rehydrationMiddleware(store)(next)(action); assert.calledWith(next, action); }); it("should not allow actions from main to go through before MERGE_STORE_ACTION was received", () => { const next = sinon.spy(); const dispatch = rehydrationMiddleware(store)(next); dispatch(ac.BroadcastToContent({ type: "FOO" })); dispatch(ac.AlsoToOneContent({ type: "FOO" }, 123)); assert.notCalled(next); }); it("should allow all local actions to go through", () => { const action = { type: "FOO" }; const next = sinon.spy(); rehydrationMiddleware(store)(next)(action); assert.calledWith(next, action); }); it("should allow actions from main to go through after MERGE_STORE_ACTION has been received", () => { const next = sinon.spy(); const dispatch = rehydrationMiddleware(store)(next); dispatch({ type: MERGE_STORE_ACTION }); next.resetHistory(); const action = ac.AlsoToOneContent({ type: "FOO" }, 123); dispatch(action); assert.calledWith(next, action); }); it("should not let startup actions go through for the preloaded about:home document", () => { globals.set("__FROM_STARTUP_CACHE__", true); const next = sinon.spy(); const dispatch = rehydrationMiddleware(store)(next); const action = ac.BroadcastToContent( { type: "FOO", meta: { isStartup: true } }, 123 ); dispatch(action); assert.notCalled(next); }); }); describe("queueEarlyMessageMiddleware", () => { it("should allow all local actions to go through", () => { const action = { type: "FOO" }; const next = sinon.spy(); queueEarlyMessageMiddleware(store)(next)(action); assert.calledWith(next, action); }); it("should allow action to main that does not belong to EARLY_QUEUED_ACTIONS to go through", () => { const action = ac.AlsoToMain({ type: "FOO" }); const next = sinon.spy(); queueEarlyMessageMiddleware(store)(next)(action); assert.calledWith(next, action); }); it(`should line up EARLY_QUEUED_ACTIONS only let them go through after it receives the action from main`, () => { EARLY_QUEUED_ACTIONS.forEach(actionType => { const testStore = initStore({ number: addNumberReducer }); const next = sinon.spy(); const dispatch = queueEarlyMessageMiddleware(testStore)(next); const action = ac.AlsoToMain({ type: actionType }); const fromMainAction = ac.AlsoToOneContent({ type: "FOO" }, 123); // Early actions should be added to the queue dispatch(action); dispatch(action); assert.notCalled(next); assert.equal(testStore.getState.earlyActionQueue.length, 2); next.resetHistory(); // Receiving action from main would empty the queue dispatch(fromMainAction); assert.calledThrice(next); assert.equal(next.firstCall.args[0], fromMainAction); assert.equal(next.secondCall.args[0], action); assert.equal(next.thirdCall.args[0], action); assert.equal(testStore.getState.earlyActionQueue.length, 0); next.resetHistory(); // New action should go through immediately dispatch(action); assert.calledOnce(next); assert.calledWith(next, action); }); }); }); });