UNPKG

14.8 kBPlain TextView Raw
1import {
2 mockConsole,
3 createConsole,
4 getLog,
5} from 'console-testing-library/pure'
6import type { Reducer } from '@reduxjs/toolkit'
7import {
8 configureStore,
9 createSerializableStateInvariantMiddleware,
10 findNonSerializableValue,
11 isPlain,
12} from '@reduxjs/toolkit'
13
14// Mocking console
15let restore = () => {}
16beforeEach(() => {
17 restore = mockConsole(createConsole())
18})
19afterEach(() => restore())
20
21describe('findNonSerializableValue', () => {
22 it('Should return false if no matching values are found', () => {
23 const obj = {
24 a: 42,
25 b: {
26 b1: 'test',
27 },
28 c: [99, { d: 123 }],
29 }
30
31 const result = findNonSerializableValue(obj)
32
33 expect(result).toBe(false)
34 })
35
36 it('Should return a keypath and the value if it finds a non-serializable value', () => {
37 function testFunction() {}
38
39 const obj = {
40 a: 42,
41 b: {
42 b1: testFunction,
43 },
44 c: [99, { d: 123 }],
45 }
46
47 const result = findNonSerializableValue(obj)
48
49 expect(result).toEqual({ keyPath: 'b.b1', value: testFunction })
50 })
51
52 it('Should return the first non-serializable value it finds', () => {
53 const map = new Map()
54 const symbol = Symbol.for('testSymbol')
55
56 const obj = {
57 a: 42,
58 b: {
59 b1: 1,
60 },
61 c: [99, { d: 123 }, map, symbol, 'test'],
62 d: symbol,
63 }
64
65 const result = findNonSerializableValue(obj)
66
67 expect(result).toEqual({ keyPath: 'c.2', value: map })
68 })
69
70 it('Should return a specific value if the root object is non-serializable', () => {
71 const value = new Map()
72 const result = findNonSerializableValue(value)
73
74 expect(result).toEqual({ keyPath: '<root>', value })
75 })
76
77 it('Should accept null as a valid value', () => {
78 const obj = {
79 a: 42,
80 b: {
81 b1: 1,
82 },
83 c: null,
84 }
85
86 const result = findNonSerializableValue(obj)
87
88 expect(result).toEqual(false)
89 })
90})
91
92describe('serializableStateInvariantMiddleware', () => {
93 it('Should log an error when a non-serializable action is dispatched', () => {
94 const reducer: Reducer = (state = 0, _action) => state + 1
95
96 const serializableStateInvariantMiddleware =
97 createSerializableStateInvariantMiddleware()
98
99 const store = configureStore({
100 reducer,
101 middleware: [serializableStateInvariantMiddleware],
102 })
103
104 const type = Symbol.for('SOME_CONSTANT')
105 const dispatchedAction = { type }
106
107 store.dispatch(dispatchedAction)
108
109 expect(getLog().log).toMatchInlineSnapshot(`
110 "A non-serializable value was detected in an action, in the path: \`type\`. Value: Symbol(SOME_CONSTANT)
111 Take a look at the logic that dispatched this action: Object {
112 \\"type\\": Symbol(SOME_CONSTANT),
113 }
114 (See https://redux.js.org/faq/actions#why-should-type-be-a-string-or-at-least-serializable-why-should-my-action-types-be-constants)
115 (To allow non-serializable values see: https://redux-toolkit.js.org/usage/usage-guide#working-with-non-serializable-data)"
116 `)
117 })
118
119 it('Should log an error when a non-serializable value is in state', () => {
120 const ACTION_TYPE = 'TEST_ACTION'
121
122 const initialState = {
123 a: 0,
124 }
125
126 const badValue = new Map()
127
128 const reducer: Reducer = (state = initialState, action) => {
129 switch (action.type) {
130 case ACTION_TYPE: {
131 return {
132 a: badValue,
133 }
134 }
135 default:
136 return state
137 }
138 }
139
140 const serializableStateInvariantMiddleware =
141 createSerializableStateInvariantMiddleware()
142
143 const store = configureStore({
144 reducer: {
145 testSlice: reducer,
146 },
147 middleware: [serializableStateInvariantMiddleware],
148 })
149
150 store.dispatch({ type: ACTION_TYPE })
151
152 expect(getLog().log).toMatchInlineSnapshot(`
153 "A non-serializable value was detected in the state, in the path: \`testSlice.a\`. Value: Map {}
154 Take a look at the reducer(s) handling this action type: TEST_ACTION.
155 (See https://redux.js.org/faq/organizing-state#can-i-put-functions-promises-or-other-non-serializable-items-in-my-store-state)"
156 `)
157 })
158
159 describe('consumer tolerated structures', () => {
160 const nonSerializableValue = new Map()
161
162 const nestedSerializableObjectWithBadValue = {
163 isSerializable: true,
164 entries: (): [string, any][] => [
165 ['good-string', 'Good!'],
166 ['good-number', 1337],
167 ['bad-map-instance', nonSerializableValue],
168 ],
169 }
170
171 const serializableObject = {
172 isSerializable: true,
173 entries: (): [string, any][] => [
174 ['first', 1],
175 ['second', 'B!'],
176 ['third', nestedSerializableObjectWithBadValue],
177 ],
178 }
179
180 it('Should log an error when a non-serializable value is nested in state', () => {
181 const ACTION_TYPE = 'TEST_ACTION'
182
183 const initialState = {
184 a: 0,
185 }
186
187 const reducer: Reducer = (state = initialState, action) => {
188 switch (action.type) {
189 case ACTION_TYPE: {
190 return {
191 a: serializableObject,
192 }
193 }
194 default:
195 return state
196 }
197 }
198
199 // use default options
200 const serializableStateInvariantMiddleware =
201 createSerializableStateInvariantMiddleware()
202
203 const store = configureStore({
204 reducer: {
205 testSlice: reducer,
206 },
207 middleware: [serializableStateInvariantMiddleware],
208 })
209
210 store.dispatch({ type: ACTION_TYPE })
211
212 // since default options are used, the `entries` function in `serializableObject` will cause the error
213 expect(getLog().log).toMatchInlineSnapshot(`
214 "A non-serializable value was detected in the state, in the path: \`testSlice.a.entries\`. Value: [Function entries]
215 Take a look at the reducer(s) handling this action type: TEST_ACTION.
216 (See https://redux.js.org/faq/organizing-state#can-i-put-functions-promises-or-other-non-serializable-items-in-my-store-state)"
217 `)
218 })
219
220 it('Should use consumer supplied isSerializable and getEntries options to tolerate certain structures', () => {
221 const ACTION_TYPE = 'TEST_ACTION'
222
223 const initialState = {
224 a: 0,
225 }
226
227 const isSerializable = (val: any): boolean =>
228 val.isSerializable || isPlain(val)
229 const getEntries = (val: any): [string, any][] =>
230 val.isSerializable ? val.entries() : Object.entries(val)
231
232 const reducer: Reducer = (state = initialState, action) => {
233 switch (action.type) {
234 case ACTION_TYPE: {
235 return {
236 a: serializableObject,
237 }
238 }
239 default:
240 return state
241 }
242 }
243
244 const serializableStateInvariantMiddleware =
245 createSerializableStateInvariantMiddleware({
246 isSerializable,
247 getEntries,
248 })
249
250 const store = configureStore({
251 reducer: {
252 testSlice: reducer,
253 },
254 middleware: [serializableStateInvariantMiddleware],
255 })
256
257 store.dispatch({ type: ACTION_TYPE })
258
259 // error reported is from a nested class instance, rather than the `entries` function `serializableObject`
260 expect(getLog().log).toMatchInlineSnapshot(`
261 "A non-serializable value was detected in the state, in the path: \`testSlice.a.third.bad-map-instance\`. Value: Map {}
262 Take a look at the reducer(s) handling this action type: TEST_ACTION.
263 (See https://redux.js.org/faq/organizing-state#can-i-put-functions-promises-or-other-non-serializable-items-in-my-store-state)"
264 `)
265 })
266 })
267
268 it('Should use the supplied isSerializable function to determine serializability', () => {
269 const ACTION_TYPE = 'TEST_ACTION'
270
271 const initialState = {
272 a: 0,
273 }
274
275 const badValue = new Map()
276
277 const reducer: Reducer = (state = initialState, action) => {
278 switch (action.type) {
279 case ACTION_TYPE: {
280 return {
281 a: badValue,
282 }
283 }
284 default:
285 return state
286 }
287 }
288
289 const serializableStateInvariantMiddleware =
290 createSerializableStateInvariantMiddleware({
291 isSerializable: () => true,
292 })
293
294 const store = configureStore({
295 reducer: {
296 testSlice: reducer,
297 },
298 middleware: [serializableStateInvariantMiddleware],
299 })
300
301 store.dispatch({ type: ACTION_TYPE })
302
303 // Supplied 'isSerializable' considers all values serializable, hence
304 // no error logging is expected:
305 expect(getLog().log).toBe('')
306 })
307
308 it('should not check serializability for ignored action types', () => {
309 let numTimesCalled = 0
310
311 const serializableStateMiddleware =
312 createSerializableStateInvariantMiddleware({
313 isSerializable: () => {
314 numTimesCalled++
315 return true
316 },
317 ignoredActions: ['IGNORE_ME'],
318 })
319
320 const store = configureStore({
321 reducer: () => ({}),
322 middleware: [serializableStateMiddleware],
323 })
324
325 expect(numTimesCalled).toBe(0)
326
327 store.dispatch({ type: 'IGNORE_ME' })
328
329 expect(numTimesCalled).toBe(0)
330
331 store.dispatch({ type: 'ANY_OTHER_ACTION' })
332
333 expect(numTimesCalled).toBeGreaterThan(0)
334 })
335
336 describe('ignored action paths', () => {
337 function reducer() {
338 return 0
339 }
340 const nonSerializableValue = new Map()
341
342 it('default value: meta.arg', () => {
343 configureStore({
344 reducer,
345 middleware: [createSerializableStateInvariantMiddleware()],
346 }).dispatch({ type: 'test', meta: { arg: nonSerializableValue } })
347
348 expect(getLog().log).toMatchInlineSnapshot(`""`)
349 })
350
351 it('default value can be overridden', () => {
352 configureStore({
353 reducer,
354 middleware: [
355 createSerializableStateInvariantMiddleware({
356 ignoredActionPaths: [],
357 }),
358 ],
359 }).dispatch({ type: 'test', meta: { arg: nonSerializableValue } })
360
361 expect(getLog().log).toMatchInlineSnapshot(`
362 "A non-serializable value was detected in an action, in the path: \`meta.arg\`. Value: Map {}
363 Take a look at the logic that dispatched this action: Object {
364 \\"meta\\": Object {
365 \\"arg\\": Map {},
366 },
367 \\"type\\": \\"test\\",
368 }
369 (See https://redux.js.org/faq/actions#why-should-type-be-a-string-or-at-least-serializable-why-should-my-action-types-be-constants)
370 (To allow non-serializable values see: https://redux-toolkit.js.org/usage/usage-guide#working-with-non-serializable-data)"
371 `)
372 })
373
374 it('can specify (multiple) different values', () => {
375 configureStore({
376 reducer,
377 middleware: [
378 createSerializableStateInvariantMiddleware({
379 ignoredActionPaths: ['payload', 'meta.arg'],
380 }),
381 ],
382 }).dispatch({
383 type: 'test',
384 payload: { arg: nonSerializableValue },
385 meta: { arg: nonSerializableValue },
386 })
387
388 expect(getLog().log).toMatchInlineSnapshot(`""`)
389 })
390 })
391
392 it('should not check serializability for ignored slice names', () => {
393 const ACTION_TYPE = 'TEST_ACTION'
394
395 const initialState = {
396 a: 0,
397 }
398
399 const badValue = new Map()
400
401 const reducer: Reducer = (state = initialState, action) => {
402 switch (action.type) {
403 case ACTION_TYPE: {
404 return {
405 a: badValue,
406 b: {
407 c: badValue,
408 d: badValue,
409 },
410 e: { f: badValue },
411 }
412 }
413 default:
414 return state
415 }
416 }
417
418 const serializableStateInvariantMiddleware =
419 createSerializableStateInvariantMiddleware({
420 ignoredPaths: [
421 // Test for ignoring a single value
422 'testSlice.a',
423 // Test for ignoring a single nested value
424 'testSlice.b.c',
425 // Test for ignoring an object and its children
426 'testSlice.e',
427 ],
428 })
429
430 const store = configureStore({
431 reducer: {
432 testSlice: reducer,
433 },
434 middleware: [serializableStateInvariantMiddleware],
435 })
436
437 store.dispatch({ type: ACTION_TYPE })
438
439 // testSlice.b.d was not covered in ignoredPaths, so will still log the error
440 expect(getLog().log).toMatchInlineSnapshot(`
441 "A non-serializable value was detected in the state, in the path: \`testSlice.b.d\`. Value: Map {}
442 Take a look at the reducer(s) handling this action type: TEST_ACTION.
443 (See https://redux.js.org/faq/organizing-state#can-i-put-functions-promises-or-other-non-serializable-items-in-my-store-state)"
444 `)
445 })
446
447 it('allows ignoring state entirely', () => {
448 const badValue = new Map()
449 const reducer = () => badValue
450 configureStore({
451 reducer,
452 middleware: [
453 createSerializableStateInvariantMiddleware({
454 ignoreState: true,
455 }),
456 ],
457 }).dispatch({ type: 'test' })
458
459 expect(getLog().log).toMatchInlineSnapshot(`""`)
460 })
461
462 it('Should print a warning if execution takes too long', () => {
463 const reducer: Reducer = (state = 42, action) => {
464 return state
465 }
466
467 const serializableStateInvariantMiddleware =
468 createSerializableStateInvariantMiddleware({ warnAfter: 4 })
469
470 const store = configureStore({
471 reducer: {
472 testSlice: reducer,
473 },
474 middleware: [serializableStateInvariantMiddleware],
475 })
476
477 store.dispatch({
478 type: 'SOME_ACTION',
479 payload: new Array(10000).fill({ value: 'more' }),
480 })
481 expect(getLog().log).toMatch(
482 /^SerializableStateInvariantMiddleware took \d*ms, which is more than the warning threshold of 4ms./
483 )
484 })
485
486 it('Should not print a warning if "reducer" takes too long', () => {
487 const reducer: Reducer = (state = 42, action) => {
488 const started = Date.now()
489 while (Date.now() - started < 8) {}
490 return state
491 }
492
493 const serializableStateInvariantMiddleware =
494 createSerializableStateInvariantMiddleware({ warnAfter: 4 })
495
496 const store = configureStore({
497 reducer: {
498 testSlice: reducer,
499 },
500 middleware: [serializableStateInvariantMiddleware],
501 })
502
503 store.dispatch({ type: 'SOME_ACTION' })
504 expect(getLog().log).toMatch('')
505 })
506})