Skip to content

Commit 49e5175

Browse files
committed
Add middleware test file from PR
1 parent b0d9b25 commit 49e5175

File tree

1 file changed

+354
-0
lines changed

1 file changed

+354
-0
lines changed
Lines changed: 354 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,354 @@
1+
import { configureStore, createAction, AnyAction } from '@reduxjs/toolkit'
2+
import {
3+
createActionListenerMiddleware,
4+
addListenerAction,
5+
removeListenerAction,
6+
When,
7+
ActionListenerMiddlewareAPI,
8+
} from '../index'
9+
10+
const middlewareApi = {
11+
getState: expect.any(Function),
12+
dispatch: expect.any(Function),
13+
stopPropagation: expect.any(Function),
14+
unsubscribe: expect.any(Function),
15+
}
16+
17+
const noop = () => {}
18+
19+
describe('createActionListenerMiddleware', () => {
20+
let store = configureStore({
21+
reducer: () => ({}),
22+
middleware: [createActionListenerMiddleware()] as const,
23+
})
24+
let reducer: jest.Mock
25+
let middleware: ReturnType<typeof createActionListenerMiddleware>
26+
27+
const testAction1 = createAction<string>('testAction1')
28+
type TestAction1 = ReturnType<typeof testAction1>
29+
const testAction2 = createAction<string>('testAction2')
30+
31+
beforeEach(() => {
32+
middleware = createActionListenerMiddleware()
33+
reducer = jest.fn(() => ({}))
34+
store = configureStore({
35+
reducer,
36+
middleware: [middleware] as const,
37+
})
38+
})
39+
40+
test('directly subscribing', () => {
41+
const listener = jest.fn((_: TestAction1) => {})
42+
43+
middleware.addListener(testAction1, listener)
44+
45+
store.dispatch(testAction1('a'))
46+
store.dispatch(testAction2('b'))
47+
store.dispatch(testAction1('c'))
48+
49+
expect(listener.mock.calls).toEqual([
50+
[testAction1('a'), middlewareApi],
51+
[testAction1('c'), middlewareApi],
52+
])
53+
})
54+
55+
test('subscribing with the same listener will not make it trigger twice (like EventTarget.addEventListener())', () => {
56+
const listener = jest.fn((_: TestAction1) => {})
57+
58+
middleware.addListener(testAction1, listener)
59+
middleware.addListener(testAction1, listener)
60+
61+
store.dispatch(testAction1('a'))
62+
store.dispatch(testAction2('b'))
63+
store.dispatch(testAction1('c'))
64+
65+
expect(listener.mock.calls).toEqual([
66+
[testAction1('a'), middlewareApi],
67+
[testAction1('c'), middlewareApi],
68+
])
69+
})
70+
71+
test('unsubscribing via callback', () => {
72+
const listener = jest.fn((_: TestAction1) => {})
73+
74+
const unsubscribe = middleware.addListener(testAction1, listener)
75+
76+
store.dispatch(testAction1('a'))
77+
unsubscribe()
78+
store.dispatch(testAction2('b'))
79+
store.dispatch(testAction1('c'))
80+
81+
expect(listener.mock.calls).toEqual([[testAction1('a'), middlewareApi]])
82+
})
83+
84+
test('directly unsubscribing', () => {
85+
const listener = jest.fn((_: TestAction1) => {})
86+
87+
middleware.addListener(testAction1, listener)
88+
89+
store.dispatch(testAction1('a'))
90+
91+
middleware.removeListener(testAction1, listener)
92+
store.dispatch(testAction2('b'))
93+
store.dispatch(testAction1('c'))
94+
95+
expect(listener.mock.calls).toEqual([[testAction1('a'), middlewareApi]])
96+
})
97+
98+
test('unsubscribing without any subscriptions does not trigger an error', () => {
99+
middleware.removeListener(testAction1, noop)
100+
})
101+
102+
test('subscribing via action', () => {
103+
const listener = jest.fn((_: TestAction1) => {})
104+
105+
store.dispatch(addListenerAction(testAction1, listener))
106+
107+
store.dispatch(testAction1('a'))
108+
store.dispatch(testAction2('b'))
109+
store.dispatch(testAction1('c'))
110+
111+
expect(listener.mock.calls).toEqual([
112+
[testAction1('a'), middlewareApi],
113+
[testAction1('c'), middlewareApi],
114+
])
115+
})
116+
117+
test('unsubscribing via callback from dispatch', () => {
118+
const listener = jest.fn((_: TestAction1) => {})
119+
120+
const unsubscribe = store.dispatch(addListenerAction(testAction1, listener))
121+
122+
store.dispatch(testAction1('a'))
123+
// TODO This return type isn't correct
124+
// @ts-expect-error
125+
unsubscribe()
126+
store.dispatch(testAction2('b'))
127+
store.dispatch(testAction1('c'))
128+
129+
expect(listener.mock.calls).toEqual([[testAction1('a'), middlewareApi]])
130+
})
131+
132+
test('unsubscribing via action', () => {
133+
const listener = jest.fn((_: TestAction1) => {})
134+
135+
middleware.addListener(testAction1, listener)
136+
137+
store.dispatch(testAction1('a'))
138+
139+
store.dispatch(removeListenerAction(testAction1, listener))
140+
store.dispatch(testAction2('b'))
141+
store.dispatch(testAction1('c'))
142+
143+
expect(listener.mock.calls).toEqual([[testAction1('a'), middlewareApi]])
144+
})
145+
146+
const unforwaredActions: [string, AnyAction][] = [
147+
['addListenerAction', addListenerAction(testAction1, noop)],
148+
['removeListenerAction', removeListenerAction(testAction1, noop)],
149+
]
150+
test.each(unforwaredActions)(
151+
'"%s" is not forwarded to the reducer',
152+
(_, action) => {
153+
reducer.mockClear()
154+
155+
store.dispatch(testAction1('a'))
156+
store.dispatch(action)
157+
store.dispatch(testAction2('b'))
158+
159+
expect(reducer.mock.calls).toEqual([
160+
[{}, testAction1('a')],
161+
[{}, testAction2('b')],
162+
])
163+
}
164+
)
165+
166+
test('"can unsubscribe via middleware api', () => {
167+
const listener = jest.fn(
168+
(
169+
action: TestAction1,
170+
api: ActionListenerMiddlewareAPI<any, any, any>
171+
) => {
172+
if (action.payload === 'b') {
173+
api.unsubscribe()
174+
}
175+
}
176+
)
177+
178+
middleware.addListener(testAction1, listener)
179+
180+
store.dispatch(testAction1('a'))
181+
store.dispatch(testAction1('b'))
182+
store.dispatch(testAction1('c'))
183+
184+
expect(listener.mock.calls).toEqual([
185+
[testAction1('a'), middlewareApi],
186+
[testAction1('b'), middlewareApi],
187+
])
188+
})
189+
190+
const whenMap: [When, string, string][] = [
191+
[undefined, 'reducer', 'listener'],
192+
['before', 'listener', 'reducer'],
193+
['after', 'reducer', 'listener'],
194+
]
195+
test.each(whenMap)(
196+
'with "when" set to %s, %s runs before %s',
197+
(when, _, shouldRunLast) => {
198+
let whoRanLast = ''
199+
200+
reducer.mockClear()
201+
reducer.mockImplementationOnce(() => {
202+
whoRanLast = 'reducer'
203+
})
204+
const listener = jest.fn(() => {
205+
whoRanLast = 'listener'
206+
})
207+
208+
middleware.addListener(testAction1, listener, when ? { when } : {})
209+
210+
store.dispatch(testAction1('a'))
211+
expect(reducer).toHaveBeenCalledTimes(1)
212+
expect(listener).toHaveBeenCalledTimes(1)
213+
expect(whoRanLast).toBe(shouldRunLast)
214+
}
215+
)
216+
217+
test('mixing "before" and "after"', () => {
218+
const calls: Function[] = []
219+
function before1() {
220+
calls.push(before1)
221+
}
222+
function before2() {
223+
calls.push(before2)
224+
}
225+
function after1() {
226+
calls.push(after1)
227+
}
228+
function after2() {
229+
calls.push(after2)
230+
}
231+
232+
middleware.addListener(testAction1, before1, { when: 'before' })
233+
middleware.addListener(testAction1, before2, { when: 'before' })
234+
middleware.addListener(testAction1, after1, { when: 'after' })
235+
middleware.addListener(testAction1, after2, { when: 'after' })
236+
237+
store.dispatch(testAction1('a'))
238+
store.dispatch(testAction2('a'))
239+
240+
expect(calls).toEqual([before1, before2, after1, after2])
241+
})
242+
243+
test('mixing "before" and "after" with stopPropagation', () => {
244+
const calls: Function[] = []
245+
function before1() {
246+
calls.push(before1)
247+
}
248+
function before2(_: any, api: any) {
249+
calls.push(before2)
250+
api.stopPropagation()
251+
}
252+
function before3() {
253+
calls.push(before3)
254+
}
255+
function after1() {
256+
calls.push(after1)
257+
}
258+
function after2() {
259+
calls.push(after2)
260+
}
261+
262+
middleware.addListener(testAction1, before1, { when: 'before' })
263+
middleware.addListener(testAction1, before2, { when: 'before' })
264+
middleware.addListener(testAction1, before3, { when: 'before' })
265+
middleware.addListener(testAction1, after1, { when: 'after' })
266+
middleware.addListener(testAction1, after2, { when: 'after' })
267+
268+
store.dispatch(testAction1('a'))
269+
store.dispatch(testAction2('a'))
270+
271+
expect(calls).toEqual([before1, before2])
272+
})
273+
274+
test('by default, actions are forwarded to the store', () => {
275+
reducer.mockClear()
276+
277+
const listener = jest.fn((_: TestAction1) => {})
278+
279+
middleware.addListener(testAction1, listener)
280+
281+
store.dispatch(testAction1('a'))
282+
283+
expect(reducer.mock.calls).toEqual([[{}, testAction1('a')]])
284+
})
285+
286+
test('calling `api.stopPropagation` in the listeners prevents actions from being forwarded to the store', () => {
287+
reducer.mockClear()
288+
289+
middleware.addListener(
290+
testAction1,
291+
(action: TestAction1, api) => {
292+
if (action.payload === 'b') {
293+
api.stopPropagation()
294+
}
295+
},
296+
{ when: 'before' }
297+
)
298+
299+
store.dispatch(testAction1('a'))
300+
store.dispatch(testAction1('b'))
301+
store.dispatch(testAction1('c'))
302+
303+
expect(reducer.mock.calls).toEqual([
304+
[{}, testAction1('a')],
305+
[{}, testAction1('c')],
306+
])
307+
})
308+
309+
test('calling `api.stopPropagation` with `when` set to "after" causes an error to be thrown', () => {
310+
reducer.mockClear()
311+
312+
middleware.addListener(
313+
testAction1,
314+
(action: TestAction1, api) => {
315+
if (action.payload === 'b') {
316+
// @ts-ignore TypeScript would already prevent this from being called with "after"
317+
api.stopPropagation()
318+
}
319+
},
320+
{ when: 'after' }
321+
)
322+
323+
store.dispatch(testAction1('a'))
324+
expect(() => {
325+
store.dispatch(testAction1('b'))
326+
}).toThrowErrorMatchingInlineSnapshot(
327+
`"stopPropagation can only be called by action listeners with the \`when\` option set to \\"before\\""`
328+
)
329+
})
330+
331+
test('calling `api.stopPropagation` asynchronously causes an error to be thrown', (finish) => {
332+
reducer.mockClear()
333+
334+
middleware.addListener(
335+
testAction1,
336+
(action: TestAction1, api) => {
337+
if (action.payload === 'b') {
338+
setTimeout(() => {
339+
expect(() => {
340+
api.stopPropagation()
341+
}).toThrowErrorMatchingInlineSnapshot(
342+
`"stopPropagation can only be called synchronously"`
343+
)
344+
finish()
345+
})
346+
}
347+
},
348+
{ when: 'before' }
349+
)
350+
351+
store.dispatch(testAction1('a'))
352+
store.dispatch(testAction1('b'))
353+
})
354+
})

0 commit comments

Comments
 (0)