@@ -26,6 +26,7 @@ import type {
26
26
} from '../../src/index'
27
27
import type { FunctionComponent , DispatchWithoutAction , ReactNode } from 'react'
28
28
import type { Store , AnyAction } from 'redux'
29
+ import { StabilityCheck , UseSelectorOptions } from '../../src/hooks/useSelector'
29
30
30
31
// most of these tests depend on selectors being run once, which stabilityCheck doesn't do
31
32
// rather than specify it every time, let's make a new "default" here
@@ -82,10 +83,7 @@ describe('React', () => {
82
83
} )
83
84
84
85
it ( 'selects the state and renders the component when the store updates' , ( ) => {
85
- type MockParams = [ NormalStateType ]
86
- const selector : jest . Mock < number , MockParams > = jest . fn (
87
- ( s ) => s . count
88
- )
86
+ const selector = jest . fn ( ( s : NormalStateType ) => s . count )
89
87
let result : number | undefined
90
88
const Comp = ( ) => {
91
89
const count = useNormalSelector ( selector )
@@ -324,26 +322,36 @@ describe('React', () => {
324
322
)
325
323
326
324
const Comp = ( ) => {
327
- const value = useSelector < StateType , string [ ] > ( ( s ) => {
328
- return Object . keys ( s )
329
- } , shallowEqual )
325
+ const value = useSelector (
326
+ ( s : StateType ) => Object . keys ( s ) ,
327
+ shallowEqual
328
+ )
329
+ renderedItems . push ( value )
330
+ return < div />
331
+ }
332
+
333
+ const Comp2 = ( ) => {
334
+ const value = useSelector ( ( s : StateType ) => Object . keys ( s ) , {
335
+ equalityFn : shallowEqual ,
336
+ } )
330
337
renderedItems . push ( value )
331
338
return < div />
332
339
}
333
340
334
341
rtl . render (
335
342
< ProviderMock store = { store } >
336
343
< Comp />
344
+ < Comp2 />
337
345
</ ProviderMock >
338
346
)
339
347
340
- expect ( renderedItems . length ) . toBe ( 1 )
348
+ expect ( renderedItems . length ) . toBe ( 2 )
341
349
342
350
rtl . act ( ( ) => {
343
351
store . dispatch ( { type : '' } )
344
352
} )
345
353
346
- expect ( renderedItems . length ) . toBe ( 1 )
354
+ expect ( renderedItems . length ) . toBe ( 2 )
347
355
} )
348
356
349
357
it ( 'calls selector exactly once on mount and on update' , ( ) => {
@@ -354,11 +362,9 @@ describe('React', () => {
354
362
count : count + 1 ,
355
363
} ) )
356
364
357
- let numCalls = 0
358
- const selector = ( s : StateType ) => {
359
- numCalls += 1
365
+ const selector = jest . fn ( ( s : StateType ) => {
360
366
return s . count
361
- }
367
+ } )
362
368
const renderedItems : number [ ] = [ ]
363
369
364
370
const Comp = ( ) => {
@@ -373,14 +379,14 @@ describe('React', () => {
373
379
</ ProviderMock >
374
380
)
375
381
376
- expect ( numCalls ) . toBe ( 1 )
382
+ expect ( selector ) . toHaveBeenCalledTimes ( 1 )
377
383
expect ( renderedItems . length ) . toEqual ( 1 )
378
384
379
385
rtl . act ( ( ) => {
380
386
store . dispatch ( { type : '' } )
381
387
} )
382
388
383
- expect ( numCalls ) . toBe ( 2 )
389
+ expect ( selector ) . toHaveBeenCalledTimes ( 2 )
384
390
expect ( renderedItems . length ) . toEqual ( 2 )
385
391
} )
386
392
@@ -392,11 +398,9 @@ describe('React', () => {
392
398
count : count + 1 ,
393
399
} ) )
394
400
395
- let numCalls = 0
396
- const selector = ( s : StateType ) => {
397
- numCalls += 1
401
+ const selector = jest . fn ( ( s : StateType ) => {
398
402
return s . count
399
- }
403
+ } )
400
404
const renderedItems : number [ ] = [ ]
401
405
402
406
const Child = ( ) => {
@@ -427,7 +431,7 @@ describe('React', () => {
427
431
)
428
432
429
433
// Selector first called on Comp mount, and then re-invoked after mount due to useLayoutEffect dispatching event
430
- expect ( numCalls ) . toBe ( 2 )
434
+ expect ( selector ) . toHaveBeenCalledTimes ( 2 )
431
435
expect ( renderedItems . length ) . toEqual ( 2 )
432
436
} )
433
437
} )
@@ -733,6 +737,146 @@ describe('React', () => {
733
737
) . toThrow ( )
734
738
} )
735
739
} )
740
+
741
+ describe ( 'Development mode checks' , ( ) => {
742
+ describe ( 'selector result stability check' , ( ) => {
743
+ const selector = jest . fn ( ( state : NormalStateType ) => state . count )
744
+
745
+ const consoleSpy = jest
746
+ . spyOn ( console , 'warn' )
747
+ . mockImplementation ( ( ) => { } )
748
+ afterEach ( ( ) => {
749
+ consoleSpy . mockClear ( )
750
+ selector . mockClear ( )
751
+ } )
752
+ afterAll ( ( ) => {
753
+ consoleSpy . mockRestore ( )
754
+ } )
755
+
756
+ const RenderSelector = ( {
757
+ selector,
758
+ options,
759
+ } : {
760
+ selector : ( state : NormalStateType ) => number
761
+ options ?: UseSelectorOptions < number >
762
+ } ) => {
763
+ useSelector ( selector , options )
764
+ return null
765
+ }
766
+
767
+ it ( 'calls a selector twice, and warns in console if it returns a different result' , ( ) => {
768
+ rtl . render (
769
+ < Provider store = { normalStore } >
770
+ < RenderSelector selector = { selector } />
771
+ </ Provider >
772
+ )
773
+
774
+ expect ( selector ) . toHaveBeenCalledTimes ( 2 )
775
+
776
+ expect ( consoleSpy ) . not . toHaveBeenCalled ( )
777
+
778
+ rtl . cleanup ( )
779
+
780
+ const unstableSelector = jest . fn ( ( ) => Math . random ( ) )
781
+
782
+ rtl . render (
783
+ < Provider store = { normalStore } >
784
+ < RenderSelector selector = { unstableSelector } />
785
+ </ Provider >
786
+ )
787
+
788
+ expect ( selector ) . toHaveBeenCalledTimes ( 2 )
789
+
790
+ expect ( consoleSpy ) . toHaveBeenCalledWith (
791
+ expect . stringContaining (
792
+ 'returned a different result when called with the same parameters'
793
+ ) ,
794
+ expect . objectContaining ( {
795
+ state : expect . objectContaining ( {
796
+ count : 0 ,
797
+ } ) ,
798
+ selected : expect . any ( Number ) ,
799
+ selected2 : expect . any ( Number ) ,
800
+ } )
801
+ )
802
+ } )
803
+ it ( 'by default will only check on first selector call' , ( ) => {
804
+ rtl . render (
805
+ < Provider store = { normalStore } >
806
+ < RenderSelector selector = { selector } />
807
+ </ Provider >
808
+ )
809
+
810
+ expect ( selector ) . toHaveBeenCalledTimes ( 2 )
811
+
812
+ rtl . act ( ( ) => {
813
+ normalStore . dispatch ( { type : '' } )
814
+ } )
815
+
816
+ expect ( selector ) . toHaveBeenCalledTimes ( 3 )
817
+ } )
818
+ it ( 'disables check if context or hook specifies' , ( ) => {
819
+ rtl . render (
820
+ < Provider store = { normalStore } stabilityCheck = "never" >
821
+ < RenderSelector selector = { selector } />
822
+ </ Provider >
823
+ )
824
+
825
+ expect ( selector ) . toHaveBeenCalledTimes ( 1 )
826
+
827
+ rtl . cleanup ( )
828
+
829
+ selector . mockClear ( )
830
+
831
+ rtl . render (
832
+ < Provider store = { normalStore } >
833
+ < RenderSelector
834
+ selector = { selector }
835
+ options = { { stabilityCheck : 'never' } }
836
+ />
837
+ </ Provider >
838
+ )
839
+
840
+ expect ( selector ) . toHaveBeenCalledTimes ( 1 )
841
+ } )
842
+ it ( 'always runs check if context or hook specifies' , ( ) => {
843
+ rtl . render (
844
+ < Provider store = { normalStore } stabilityCheck = "always" >
845
+ < RenderSelector selector = { selector } />
846
+ </ Provider >
847
+ )
848
+
849
+ expect ( selector ) . toHaveBeenCalledTimes ( 2 )
850
+
851
+ rtl . act ( ( ) => {
852
+ normalStore . dispatch ( { type : '' } )
853
+ } )
854
+
855
+ expect ( selector ) . toHaveBeenCalledTimes ( 4 )
856
+
857
+ rtl . cleanup ( )
858
+
859
+ selector . mockClear ( )
860
+
861
+ rtl . render (
862
+ < Provider store = { normalStore } >
863
+ < RenderSelector
864
+ selector = { selector }
865
+ options = { { stabilityCheck : 'always' } }
866
+ />
867
+ </ Provider >
868
+ )
869
+
870
+ expect ( selector ) . toHaveBeenCalledTimes ( 2 )
871
+
872
+ rtl . act ( ( ) => {
873
+ normalStore . dispatch ( { type : '' } )
874
+ } )
875
+
876
+ expect ( selector ) . toHaveBeenCalledTimes ( 4 )
877
+ } )
878
+ } )
879
+ } )
736
880
} )
737
881
738
882
describe ( 'createSelectorHook' , ( ) => {
0 commit comments