Skip to content

Commit cf44723

Browse files
committed
attempt at actionListenerMiddleware
1 parent cb1a287 commit cf44723

File tree

3 files changed

+505
-1
lines changed

3 files changed

+505
-1
lines changed

src/createAction.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ export type _ActionCreatorWithPreparedPayload<
8181
*
8282
* @inheritdoc {redux#ActionCreator}
8383
*/
84-
interface BaseActionCreator<P, T extends string, M = never, E = never> {
84+
export interface BaseActionCreator<P, T extends string, M = never, E = never> {
8585
type: T
8686
match(action: Action<unknown>): action is PayloadAction<P, T, M, E>
8787
}
Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
import { configureStore } from '../configureStore'
2+
import {
3+
createActionListenerMiddleware,
4+
addListenerAction,
5+
removeListenerAction
6+
} from './createActionListenerMiddleware'
7+
import { createAction } from '../createAction'
8+
9+
const middlewareApi = {
10+
getState: expect.any(Function),
11+
dispatch: expect.any(Function)
12+
}
13+
14+
describe('createActionListenerMiddleware', () => {
15+
let store = configureStore({
16+
reducer: () => ({}),
17+
middleware: [createActionListenerMiddleware()] as const
18+
})
19+
let reducer: jest.Mock
20+
let middleware: ReturnType<typeof createActionListenerMiddleware>
21+
22+
const testAction1 = createAction<string>('testAction1')
23+
type TestAction1 = ReturnType<typeof testAction1>
24+
const testAction2 = createAction<string>('testAction2')
25+
26+
beforeEach(() => {
27+
middleware = createActionListenerMiddleware()
28+
reducer = jest.fn(() => ({}))
29+
store = configureStore({
30+
reducer,
31+
middleware: [middleware] as const
32+
})
33+
})
34+
35+
test('directly subscribing', () => {
36+
const listener = jest.fn((_: TestAction1) => {})
37+
38+
middleware.addListener(testAction1, listener)
39+
40+
store.dispatch(testAction1('a'))
41+
store.dispatch(testAction2('b'))
42+
store.dispatch(testAction1('c'))
43+
44+
expect(listener.mock.calls).toEqual([
45+
[testAction1('a'), middlewareApi],
46+
[testAction1('c'), middlewareApi]
47+
])
48+
})
49+
50+
test('subscribing with the same listener will not make it trigger twice (like EventTarget.addEventListener())', () => {
51+
/**
52+
* thoughts: allow to use this to override the options for a listener?
53+
* right now it's just exiting if the listener is already registered
54+
*/
55+
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('subscribing via action', () => {
99+
const listener = jest.fn((_: TestAction1) => {})
100+
101+
store.dispatch(addListenerAction(testAction1, listener))
102+
103+
store.dispatch(testAction1('a'))
104+
store.dispatch(testAction2('b'))
105+
store.dispatch(testAction1('c'))
106+
107+
expect(listener.mock.calls).toEqual([
108+
[testAction1('a'), middlewareApi],
109+
[testAction1('c'), middlewareApi]
110+
])
111+
})
112+
113+
test('unsubscribing via callback from dispatch', () => {
114+
const listener = jest.fn((_: TestAction1) => {})
115+
116+
const unsubscribe = store.dispatch(addListenerAction(testAction1, listener))
117+
118+
store.dispatch(testAction1('a'))
119+
// @ts-ignore TODO types
120+
unsubscribe()
121+
store.dispatch(testAction2('b'))
122+
store.dispatch(testAction1('c'))
123+
124+
expect(listener.mock.calls).toEqual([[testAction1('a'), middlewareApi]])
125+
})
126+
127+
test('unsubscribing via action', () => {
128+
const listener = jest.fn((_: TestAction1) => {})
129+
130+
middleware.addListener(testAction1, listener)
131+
132+
store.dispatch(testAction1('a'))
133+
134+
store.dispatch(removeListenerAction(testAction1, listener))
135+
store.dispatch(testAction2('b'))
136+
store.dispatch(testAction1('c'))
137+
138+
expect(listener.mock.calls).toEqual([[testAction1('a'), middlewareApi]])
139+
})
140+
141+
test('"condition" allows to skip the listener', () => {
142+
const listener = jest.fn((_: TestAction1) => {})
143+
144+
middleware.addListener(testAction1, listener, {
145+
condition(action) {
146+
return action.payload !== 'b'
147+
}
148+
})
149+
150+
store.dispatch(testAction1('a'))
151+
store.dispatch(testAction1('b'))
152+
store.dispatch(testAction1('c'))
153+
154+
expect(listener.mock.calls).toEqual([
155+
[testAction1('a'), middlewareApi],
156+
[testAction1('c'), middlewareApi]
157+
])
158+
})
159+
160+
test('"once" unsubscribes the listener automatically after one use', () => {
161+
const listener = jest.fn((_: TestAction1) => {})
162+
163+
middleware.addListener(testAction1, listener, {
164+
once: true
165+
})
166+
167+
store.dispatch(testAction1('a'))
168+
store.dispatch(testAction1('b'))
169+
store.dispatch(testAction1('c'))
170+
171+
expect(listener.mock.calls).toEqual([[testAction1('a'), middlewareApi]])
172+
})
173+
174+
test('combining "once" with "condition', () => {
175+
const listener = jest.fn((_: TestAction1) => {})
176+
177+
middleware.addListener(testAction1, listener, {
178+
once: true,
179+
condition(action) {
180+
return action.payload === 'b'
181+
}
182+
})
183+
184+
store.dispatch(testAction1('a'))
185+
store.dispatch(testAction1('b'))
186+
store.dispatch(testAction1('c'))
187+
188+
expect(listener.mock.calls).toEqual([[testAction1('b'), middlewareApi]])
189+
})
190+
191+
test('by default, actions are forwarded to the store', () => {
192+
reducer.mockClear()
193+
194+
const listener = jest.fn((_: TestAction1) => {})
195+
196+
middleware.addListener(testAction1, listener)
197+
198+
store.dispatch(testAction1('a'))
199+
200+
expect(reducer.mock.calls).toEqual([[{}, testAction1('a')]])
201+
})
202+
203+
test('"preventPropagation" prevents actions from being forwarded to the store', () => {
204+
reducer.mockClear()
205+
206+
const listener = jest.fn((_: TestAction1) => {})
207+
208+
middleware.addListener(testAction1, listener, { preventPropagation: true })
209+
210+
store.dispatch(testAction1('a'))
211+
212+
expect(reducer.mock.calls).toEqual([])
213+
})
214+
215+
test('combining "preventPropagation" and "condition', () => {
216+
reducer.mockClear()
217+
218+
const listener = jest.fn((_: TestAction1) => {})
219+
220+
middleware.addListener(testAction1, listener, {
221+
preventPropagation: true,
222+
condition(action) {
223+
return action.payload === 'b'
224+
}
225+
})
226+
227+
store.dispatch(testAction1('a'))
228+
store.dispatch(testAction1('b'))
229+
store.dispatch(testAction1('c'))
230+
231+
expect(reducer.mock.calls).toEqual([
232+
[{}, testAction1('a')],
233+
[{}, testAction1('c')]
234+
])
235+
})
236+
})

0 commit comments

Comments
 (0)