source: node_modules/@reduxjs/toolkit/src/tests/serializableStateInvariantMiddleware.test.ts

Last change on this file was a762898, checked in by istevanoska <ilinastevanoska@…>, 5 months ago

Added visualizations

  • Property mode set to 100644
File size: 18.3 KB
RevLine 
[a762898]1import { noop } from '@internal/listenerMiddleware/utils'
2import { isNestedFrozen } from '@internal/serializableStateInvariantMiddleware'
3import type { Reducer } from '@reduxjs/toolkit'
4import {
5 configureStore,
6 createNextState,
7 createSerializableStateInvariantMiddleware,
8 findNonSerializableValue,
9 isPlain,
10 Tuple,
11} from '@reduxjs/toolkit'
12
13// Mocking console
14const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(noop)
15
16const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(noop)
17
18afterEach(() => {
19 vi.clearAllMocks()
20})
21
22afterAll(() => {
23 vi.restoreAllMocks()
24})
25
26describe('findNonSerializableValue', () => {
27 it('Should return false if no matching values are found', () => {
28 const obj = {
29 a: 42,
30 b: {
31 b1: 'test',
32 },
33 c: [99, { d: 123 }],
34 }
35
36 const result = findNonSerializableValue(obj)
37
38 expect(result).toBe(false)
39 })
40
41 it('Should return a keypath and the value if it finds a non-serializable value', () => {
42 function testFunction() {}
43
44 const obj = {
45 a: 42,
46 b: {
47 b1: testFunction,
48 },
49 c: [99, { d: 123 }],
50 }
51
52 const result = findNonSerializableValue(obj)
53
54 expect(result).toEqual({ keyPath: 'b.b1', value: testFunction })
55 })
56
57 it('Should return the first non-serializable value it finds', () => {
58 const map = new Map()
59 const symbol = Symbol.for('testSymbol')
60
61 const obj = {
62 a: 42,
63 b: {
64 b1: 1,
65 },
66 c: [99, { d: 123 }, map, symbol, 'test'],
67 d: symbol,
68 }
69
70 const result = findNonSerializableValue(obj)
71
72 expect(result).toEqual({ keyPath: 'c.2', value: map })
73 })
74
75 it('Should return a specific value if the root object is non-serializable', () => {
76 const value = new Map()
77 const result = findNonSerializableValue(value)
78
79 expect(result).toEqual({ keyPath: '<root>', value })
80 })
81
82 it('Should accept null as a valid value', () => {
83 const obj = {
84 a: 42,
85 b: {
86 b1: 1,
87 },
88 c: null,
89 }
90
91 const result = findNonSerializableValue(obj)
92
93 expect(result).toEqual(false)
94 })
95})
96
97describe('serializableStateInvariantMiddleware', () => {
98 it('Should log an error when a non-serializable action is dispatched', () => {
99 const reducer: Reducer = (state = 0, _action) => state + 1
100
101 const serializableStateInvariantMiddleware =
102 createSerializableStateInvariantMiddleware()
103
104 const store = configureStore({
105 reducer,
106 middleware: () => new Tuple(serializableStateInvariantMiddleware),
107 })
108
109 const symbol = Symbol.for('SOME_CONSTANT')
110 const dispatchedAction = { type: 'an-action', payload: symbol }
111
112 store.dispatch(dispatchedAction)
113
114 expect(consoleErrorSpy).toHaveBeenCalledOnce()
115
116 expect(consoleErrorSpy).toHaveBeenLastCalledWith(
117 `A non-serializable value was detected in an action, in the path: \`payload\`. Value:`,
118 symbol,
119 `\nTake a look at the logic that dispatched this action: `,
120 dispatchedAction,
121 `\n(See https://redux.js.org/faq/actions#why-should-type-be-a-string-or-at-least-serializable-why-should-my-action-types-be-constants)`,
122 `\n(To allow non-serializable values see: https://redux-toolkit.js.org/usage/usage-guide#working-with-non-serializable-data)`,
123 )
124 })
125
126 it('Should log an error when a non-serializable value is in state', () => {
127 const ACTION_TYPE = 'TEST_ACTION'
128
129 const initialState = {
130 a: 0,
131 }
132
133 const badValue = new Map()
134
135 const reducer: Reducer = (state = initialState, action) => {
136 switch (action.type) {
137 case ACTION_TYPE: {
138 return {
139 a: badValue,
140 }
141 }
142 default:
143 return state
144 }
145 }
146
147 const serializableStateInvariantMiddleware =
148 createSerializableStateInvariantMiddleware()
149
150 const store = configureStore({
151 reducer: {
152 testSlice: reducer,
153 },
154 middleware: () => new Tuple(serializableStateInvariantMiddleware),
155 })
156
157 store.dispatch({ type: ACTION_TYPE })
158
159 expect(consoleErrorSpy).toHaveBeenCalledOnce()
160
161 expect(consoleErrorSpy).toHaveBeenLastCalledWith(
162 `A non-serializable value was detected in the state, in the path: \`testSlice.a\`. Value:`,
163 badValue,
164 `\nTake a look at the reducer(s) handling this action type: TEST_ACTION.
165(See https://redux.js.org/faq/organizing-state#can-i-put-functions-promises-or-other-non-serializable-items-in-my-store-state)`,
166 )
167 })
168
169 describe('consumer tolerated structures', () => {
170 const nonSerializableValue = new Map()
171
172 const nestedSerializableObjectWithBadValue = {
173 isSerializable: true,
174 entries: (): [string, any][] => [
175 ['good-string', 'Good!'],
176 ['good-number', 1337],
177 ['bad-map-instance', nonSerializableValue],
178 ],
179 }
180
181 const serializableObject = {
182 isSerializable: true,
183 entries: (): [string, any][] => [
184 ['first', 1],
185 ['second', 'B!'],
186 ['third', nestedSerializableObjectWithBadValue],
187 ],
188 }
189
190 it('Should log an error when a non-serializable value is nested in state', () => {
191 const ACTION_TYPE = 'TEST_ACTION'
192
193 const initialState = {
194 a: 0,
195 }
196
197 const reducer: Reducer = (state = initialState, action) => {
198 switch (action.type) {
199 case ACTION_TYPE: {
200 return {
201 a: serializableObject,
202 }
203 }
204 default:
205 return state
206 }
207 }
208
209 // use default options
210 const serializableStateInvariantMiddleware =
211 createSerializableStateInvariantMiddleware()
212
213 const store = configureStore({
214 reducer: {
215 testSlice: reducer,
216 },
217 middleware: () => new Tuple(serializableStateInvariantMiddleware),
218 })
219
220 store.dispatch({ type: ACTION_TYPE })
221
222 expect(consoleErrorSpy).toHaveBeenCalledOnce()
223
224 // since default options are used, the `entries` function in `serializableObject` will cause the error
225 expect(consoleErrorSpy).toHaveBeenLastCalledWith(
226 `A non-serializable value was detected in the state, in the path: \`testSlice.a.entries\`. Value:`,
227 serializableObject.entries,
228 `\nTake a look at the reducer(s) handling this action type: TEST_ACTION.
229(See https://redux.js.org/faq/organizing-state#can-i-put-functions-promises-or-other-non-serializable-items-in-my-store-state)`,
230 )
231 })
232
233 it('Should use consumer supplied isSerializable and getEntries options to tolerate certain structures', () => {
234 const ACTION_TYPE = 'TEST_ACTION'
235
236 const initialState = {
237 a: 0,
238 }
239
240 const isSerializable = (val: any): boolean =>
241 val.isSerializable || isPlain(val)
242 const getEntries = (val: any): [string, any][] =>
243 val.isSerializable ? val.entries() : Object.entries(val)
244
245 const reducer: Reducer = (state = initialState, action) => {
246 switch (action.type) {
247 case ACTION_TYPE: {
248 return {
249 a: serializableObject,
250 }
251 }
252 default:
253 return state
254 }
255 }
256
257 const serializableStateInvariantMiddleware =
258 createSerializableStateInvariantMiddleware({
259 isSerializable,
260 getEntries,
261 })
262
263 const store = configureStore({
264 reducer: {
265 testSlice: reducer,
266 },
267 middleware: () => new Tuple(serializableStateInvariantMiddleware),
268 })
269
270 store.dispatch({ type: ACTION_TYPE })
271
272 expect(consoleErrorSpy).toHaveBeenCalledOnce()
273
274 // error reported is from a nested class instance, rather than the `entries` function `serializableObject`
275 expect(consoleErrorSpy).toHaveBeenLastCalledWith(
276 `A non-serializable value was detected in the state, in the path: \`testSlice.a.third.bad-map-instance\`. Value:`,
277 nonSerializableValue,
278 `\nTake a look at the reducer(s) handling this action type: TEST_ACTION.
279(See https://redux.js.org/faq/organizing-state#can-i-put-functions-promises-or-other-non-serializable-items-in-my-store-state)`,
280 )
281 })
282 })
283
284 it('Should use the supplied isSerializable function to determine serializability', () => {
285 const ACTION_TYPE = 'TEST_ACTION'
286
287 const initialState = {
288 a: 0,
289 }
290
291 const badValue = new Map()
292
293 const reducer: Reducer = (state = initialState, action) => {
294 switch (action.type) {
295 case ACTION_TYPE: {
296 return {
297 a: badValue,
298 }
299 }
300 default:
301 return state
302 }
303 }
304
305 const serializableStateInvariantMiddleware =
306 createSerializableStateInvariantMiddleware({
307 isSerializable: () => true,
308 })
309
310 const store = configureStore({
311 reducer: {
312 testSlice: reducer,
313 },
314 middleware: () => new Tuple(serializableStateInvariantMiddleware),
315 })
316
317 store.dispatch({ type: ACTION_TYPE })
318
319 // Supplied 'isSerializable' considers all values serializable, hence
320 // no error logging is expected:
321 expect(consoleErrorSpy).not.toHaveBeenCalled()
322 })
323
324 it('should not check serializability for ignored action types', () => {
325 let numTimesCalled = 0
326
327 const serializableStateMiddleware =
328 createSerializableStateInvariantMiddleware({
329 isSerializable: () => {
330 numTimesCalled++
331 return true
332 },
333 ignoredActions: ['IGNORE_ME'],
334 })
335
336 const store = configureStore({
337 reducer: () => ({}),
338 middleware: () => new Tuple(serializableStateMiddleware),
339 })
340
341 expect(numTimesCalled).toBe(0)
342
343 store.dispatch({ type: 'IGNORE_ME' })
344
345 // The state check only calls `isSerializable` once
346 expect(numTimesCalled).toBe(1)
347
348 store.dispatch({ type: 'ANY_OTHER_ACTION' })
349
350 // Action checks call `isSerializable` 2+ times when enabled
351 expect(numTimesCalled).toBeGreaterThanOrEqual(3)
352 })
353
354 describe('ignored action paths', () => {
355 function reducer() {
356 return 0
357 }
358 const nonSerializableValue = new Map()
359
360 it('default value: meta.arg', () => {
361 configureStore({
362 reducer,
363 middleware: () =>
364 new Tuple(createSerializableStateInvariantMiddleware()),
365 }).dispatch({ type: 'test', meta: { arg: nonSerializableValue } })
366
367 expect(consoleErrorSpy).not.toHaveBeenCalled()
368 })
369
370 it('default value can be overridden', () => {
371 configureStore({
372 reducer,
373 middleware: () =>
374 new Tuple(
375 createSerializableStateInvariantMiddleware({
376 ignoredActionPaths: [],
377 }),
378 ),
379 }).dispatch({ type: 'test', meta: { arg: nonSerializableValue } })
380
381 expect(consoleErrorSpy).toHaveBeenCalledOnce()
382
383 expect(consoleErrorSpy).toHaveBeenLastCalledWith(
384 `A non-serializable value was detected in an action, in the path: \`meta.arg\`. Value:`,
385 nonSerializableValue,
386 `\nTake a look at the logic that dispatched this action: `,
387 { type: 'test', meta: { arg: nonSerializableValue } },
388 `\n(See https://redux.js.org/faq/actions#why-should-type-be-a-string-or-at-least-serializable-why-should-my-action-types-be-constants)`,
389 `\n(To allow non-serializable values see: https://redux-toolkit.js.org/usage/usage-guide#working-with-non-serializable-data)`,
390 )
391 })
392
393 it('can specify (multiple) different values', () => {
394 configureStore({
395 reducer,
396 middleware: () =>
397 new Tuple(
398 createSerializableStateInvariantMiddleware({
399 ignoredActionPaths: ['payload', 'meta.arg'],
400 }),
401 ),
402 }).dispatch({
403 type: 'test',
404 payload: { arg: nonSerializableValue },
405 meta: { arg: nonSerializableValue },
406 })
407
408 expect(consoleErrorSpy).not.toHaveBeenCalled()
409 })
410
411 it('can specify regexp', () => {
412 configureStore({
413 reducer,
414 middleware: () =>
415 new Tuple(
416 createSerializableStateInvariantMiddleware({
417 ignoredActionPaths: [/^payload\..*$/],
418 }),
419 ),
420 }).dispatch({
421 type: 'test',
422 payload: { arg: nonSerializableValue },
423 })
424
425 expect(consoleErrorSpy).not.toHaveBeenCalled()
426 })
427 })
428
429 it('allows ignoring actions entirely', () => {
430 let numTimesCalled = 0
431
432 const serializableStateMiddleware =
433 createSerializableStateInvariantMiddleware({
434 isSerializable: () => {
435 numTimesCalled++
436 return true
437 },
438 ignoreActions: true,
439 })
440
441 const store = configureStore({
442 reducer: () => ({}),
443 middleware: () => new Tuple(serializableStateMiddleware),
444 })
445
446 expect(numTimesCalled).toBe(0)
447
448 store.dispatch({ type: 'THIS_DOESNT_MATTER' })
449
450 // `isSerializable` is called once for a state check
451 expect(numTimesCalled).toBe(1)
452
453 store.dispatch({ type: 'THIS_DOESNT_MATTER_AGAIN' })
454
455 expect(numTimesCalled).toBe(2)
456 })
457
458 it('should not check serializability for ignored slice names', () => {
459 const ACTION_TYPE = 'TEST_ACTION'
460
461 const initialState = {
462 a: 0,
463 }
464
465 const badValue = new Map()
466
467 const reducer: Reducer = (state = initialState, action) => {
468 switch (action.type) {
469 case ACTION_TYPE: {
470 return {
471 a: badValue,
472 b: {
473 c: badValue,
474 d: badValue,
475 },
476 e: { f: badValue },
477 g: {
478 h: badValue,
479 i: badValue,
480 },
481 }
482 }
483 default:
484 return state
485 }
486 }
487
488 const serializableStateInvariantMiddleware =
489 createSerializableStateInvariantMiddleware({
490 ignoredPaths: [
491 // Test for ignoring a single value
492 'testSlice.a',
493 // Test for ignoring a single nested value
494 'testSlice.b.c',
495 // Test for ignoring an object and its children
496 'testSlice.e',
497 // Test for ignoring based on RegExp
498 /^testSlice\.g\..*$/,
499 ],
500 })
501
502 const store = configureStore({
503 reducer: {
504 testSlice: reducer,
505 },
506 middleware: () => new Tuple(serializableStateInvariantMiddleware),
507 })
508
509 store.dispatch({ type: ACTION_TYPE })
510
511 expect(consoleErrorSpy).toHaveBeenCalledOnce()
512
513 // testSlice.b.d was not covered in ignoredPaths, so will still log the error
514 expect(consoleErrorSpy).toHaveBeenLastCalledWith(
515 `A non-serializable value was detected in the state, in the path: \`testSlice.b.d\`. Value:`,
516 badValue,
517 `\nTake a look at the reducer(s) handling this action type: TEST_ACTION.
518(See https://redux.js.org/faq/organizing-state#can-i-put-functions-promises-or-other-non-serializable-items-in-my-store-state)`,
519 )
520 })
521
522 it('allows ignoring state entirely', () => {
523 const badValue = new Map()
524 let numTimesCalled = 0
525 const reducer = () => badValue
526 const store = configureStore({
527 reducer,
528 middleware: () =>
529 new Tuple(
530 createSerializableStateInvariantMiddleware({
531 isSerializable: () => {
532 numTimesCalled++
533 return true
534 },
535 ignoreState: true,
536 }),
537 ),
538 })
539
540 expect(numTimesCalled).toBe(0)
541
542 store.dispatch({ type: 'test' })
543
544 expect(consoleErrorSpy).not.toHaveBeenCalled()
545
546 // Should be called twice for the action - there is an initial check for early returns, then a second and potentially 3rd for nested properties
547 expect(numTimesCalled).toBe(2)
548 })
549
550 it('never calls isSerializable if both ignoreState and ignoreActions are true', () => {
551 const badValue = new Map()
552 let numTimesCalled = 0
553 const reducer = () => badValue
554 const store = configureStore({
555 reducer,
556 middleware: () =>
557 new Tuple(
558 createSerializableStateInvariantMiddleware({
559 isSerializable: () => {
560 numTimesCalled++
561 return true
562 },
563 ignoreState: true,
564 ignoreActions: true,
565 }),
566 ),
567 })
568
569 expect(numTimesCalled).toBe(0)
570
571 store.dispatch({ type: 'TEST', payload: new Date() })
572 store.dispatch({ type: 'OTHER_THING' })
573
574 expect(numTimesCalled).toBe(0)
575 })
576
577 it('Should print a warning if execution takes too long', () => {
578 const reducer: Reducer = (state = 42, action) => {
579 return state
580 }
581
582 const serializableStateInvariantMiddleware =
583 createSerializableStateInvariantMiddleware({ warnAfter: 4 })
584
585 const store = configureStore({
586 reducer: {
587 testSlice: reducer,
588 },
589 middleware: () => new Tuple(serializableStateInvariantMiddleware),
590 })
591
592 store.dispatch({
593 type: 'SOME_ACTION',
594 payload: new Array(10_000).fill({ value: 'more' }),
595 })
596
597 expect(consoleWarnSpy).toHaveBeenCalledOnce()
598
599 expect(consoleWarnSpy).toHaveBeenLastCalledWith(
600 expect.stringMatching(
601 /^SerializableStateInvariantMiddleware took \d*ms, which is more than the warning threshold of 4ms./,
602 ),
603 )
604 })
605
606 it('Should not print a warning if "reducer" takes too long', () => {
607 const reducer: Reducer = (state = 42, action) => {
608 const started = Date.now()
609 while (Date.now() - started < 8) {}
610 return state
611 }
612
613 const serializableStateInvariantMiddleware =
614 createSerializableStateInvariantMiddleware({ warnAfter: 4 })
615
616 const store = configureStore({
617 reducer: {
618 testSlice: reducer,
619 },
620 middleware: () => new Tuple(serializableStateInvariantMiddleware),
621 })
622
623 store.dispatch({ type: 'SOME_ACTION' })
624
625 expect(consoleErrorSpy).not.toHaveBeenCalled()
626 })
627
628 it('Should cache its results', () => {
629 let numPlainChecks = 0
630 const countPlainChecks = (x: any) => {
631 numPlainChecks++
632 return isPlain(x)
633 }
634
635 const serializableStateInvariantMiddleware =
636 createSerializableStateInvariantMiddleware({
637 isSerializable: countPlainChecks,
638 })
639
640 const store = configureStore({
641 reducer: (state = [], action) => {
642 if (action.type === 'SET_STATE') return action.payload
643 return state
644 },
645 middleware: () => new Tuple(serializableStateInvariantMiddleware),
646 })
647
648 const state = createNextState([], () =>
649 new Array(50).fill(0).map((x, i) => ({ i })),
650 )
651 expect(isNestedFrozen(state)).toBe(true)
652
653 store.dispatch({
654 type: 'SET_STATE',
655 payload: state,
656 })
657 expect(numPlainChecks).toBeGreaterThan(state.length)
658
659 numPlainChecks = 0
660 store.dispatch({ type: 'NOOP' })
661 expect(numPlainChecks).toBeLessThan(10)
662 })
663})
Note: See TracBrowser for help on using the repository browser.