UNPKG

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