summaryrefslogtreecommitdiffstats
path: root/browser/components/newtab/test/unit/content-src/lib/init-store.test.js
blob: 0dd510ef1a4ac7c430b8fb276c87c890729db77d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
import {
  actionCreators as ac,
  actionTypes as at,
} from "common/Actions.sys.mjs";
import { addNumberReducer, GlobalOverrider } from "test/unit/utils";
import {
  INCOMING_MESSAGE_NAME,
  initStore,
  MERGE_STORE_ACTION,
  OUTGOING_MESSAGE_NAME,
  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);
    });
  });
});