init-store.test.js (5438B)
1 import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; 2 import { addNumberReducer, GlobalOverrider } from "test/unit/utils"; 3 import { 4 INCOMING_MESSAGE_NAME, 5 initStore, 6 MERGE_STORE_ACTION, 7 OUTGOING_MESSAGE_NAME, 8 rehydrationMiddleware, 9 } from "content-src/lib/init-store"; 10 11 describe("initStore", () => { 12 let globals; 13 let store; 14 beforeEach(() => { 15 globals = new GlobalOverrider(); 16 globals.set("RPMSendAsyncMessage", globals.sandbox.spy()); 17 globals.set("RPMAddMessageListener", globals.sandbox.spy()); 18 store = initStore({ number: addNumberReducer }); 19 }); 20 afterEach(() => globals.restore()); 21 it("should create a store with the provided reducers", () => { 22 assert.ok(store); 23 assert.property(store.getState(), "number"); 24 }); 25 it("should add a listener that dispatches actions", () => { 26 assert.calledWith(global.RPMAddMessageListener, INCOMING_MESSAGE_NAME); 27 const [, listener] = global.RPMAddMessageListener.firstCall.args; 28 globals.sandbox.spy(store, "dispatch"); 29 const message = { name: INCOMING_MESSAGE_NAME, data: { type: "FOO" } }; 30 31 listener(message); 32 33 assert.calledWith(store.dispatch, message.data); 34 }); 35 it("should not throw if RPMAddMessageListener is not defined", () => { 36 // Note: this is being set/restored by GlobalOverrider 37 delete global.RPMAddMessageListener; 38 39 assert.doesNotThrow(() => initStore({ number: addNumberReducer })); 40 }); 41 it("should log errors from failed messages", () => { 42 const [, callback] = global.RPMAddMessageListener.firstCall.args; 43 globals.sandbox.stub(global.console, "error"); 44 globals.sandbox.stub(store, "dispatch").throws(Error("failed")); 45 46 const message = { 47 name: INCOMING_MESSAGE_NAME, 48 data: { type: MERGE_STORE_ACTION }, 49 }; 50 callback(message); 51 52 assert.calledOnce(global.console.error); 53 }); 54 it("should replace the state if a MERGE_STORE_ACTION is dispatched", () => { 55 store.dispatch({ type: MERGE_STORE_ACTION, data: { number: 42 } }); 56 assert.deepEqual(store.getState(), { number: 42 }); 57 }); 58 it("should call .send and update the local store if an AlsoToMain action is dispatched", () => { 59 const subscriber = sinon.spy(); 60 const action = ac.AlsoToMain({ type: "FOO" }); 61 62 store.subscribe(subscriber); 63 store.dispatch(action); 64 65 assert.calledWith( 66 global.RPMSendAsyncMessage, 67 OUTGOING_MESSAGE_NAME, 68 action 69 ); 70 assert.calledOnce(subscriber); 71 }); 72 it("should call .send but not update the local store if an OnlyToMain action is dispatched", () => { 73 const subscriber = sinon.spy(); 74 const action = ac.OnlyToMain({ type: "FOO" }); 75 76 store.subscribe(subscriber); 77 store.dispatch(action); 78 79 assert.calledWith( 80 global.RPMSendAsyncMessage, 81 OUTGOING_MESSAGE_NAME, 82 action 83 ); 84 assert.notCalled(subscriber); 85 }); 86 it("should not send out other types of actions", () => { 87 store.dispatch({ type: "FOO" }); 88 assert.notCalled(global.RPMSendAsyncMessage); 89 }); 90 describe("rehydrationMiddleware", () => { 91 it("should allow NEW_TAB_STATE_REQUEST to go through", () => { 92 const action = ac.AlsoToMain({ type: at.NEW_TAB_STATE_REQUEST }); 93 const next = sinon.spy(); 94 rehydrationMiddleware(store)(next)(action); 95 assert.calledWith(next, action); 96 }); 97 it("should dispatch an additional NEW_TAB_STATE_REQUEST if INIT was received after a request", () => { 98 const requestAction = ac.AlsoToMain({ type: at.NEW_TAB_STATE_REQUEST }); 99 const next = sinon.spy(); 100 const dispatch = rehydrationMiddleware(store)(next); 101 102 dispatch(requestAction); 103 next.resetHistory(); 104 dispatch({ type: at.INIT }); 105 106 assert.calledWith(next, requestAction); 107 }); 108 it("should allow MERGE_STORE_ACTION to go through", () => { 109 const action = { type: MERGE_STORE_ACTION }; 110 const next = sinon.spy(); 111 rehydrationMiddleware(store)(next)(action); 112 assert.calledWith(next, action); 113 }); 114 it("should not allow actions from main to go through before MERGE_STORE_ACTION was received", () => { 115 const next = sinon.spy(); 116 const dispatch = rehydrationMiddleware(store)(next); 117 118 dispatch(ac.BroadcastToContent({ type: "FOO" })); 119 dispatch(ac.AlsoToOneContent({ type: "FOO" }, 123)); 120 121 assert.notCalled(next); 122 }); 123 it("should allow all local actions to go through", () => { 124 const action = { type: "FOO" }; 125 const next = sinon.spy(); 126 rehydrationMiddleware(store)(next)(action); 127 assert.calledWith(next, action); 128 }); 129 it("should allow actions from main to go through after MERGE_STORE_ACTION has been received", () => { 130 const next = sinon.spy(); 131 const dispatch = rehydrationMiddleware(store)(next); 132 133 dispatch({ type: MERGE_STORE_ACTION }); 134 next.resetHistory(); 135 136 const action = ac.AlsoToOneContent({ type: "FOO" }, 123); 137 dispatch(action); 138 assert.calledWith(next, action); 139 }); 140 it("should not let startup actions go through for the preloaded about:home document", () => { 141 globals.set("__FROM_STARTUP_CACHE__", true); 142 const next = sinon.spy(); 143 const dispatch = rehydrationMiddleware(store)(next); 144 const action = ac.BroadcastToContent( 145 { type: "FOO", meta: { isStartup: true } }, 146 123 147 ); 148 dispatch(action); 149 assert.notCalled(next); 150 }); 151 }); 152 });