@@ -26,6 +26,7 @@ import type {
2626} from '../../src/index'
2727import type { FunctionComponent , DispatchWithoutAction , ReactNode } from 'react'
2828import type { Store , AnyAction } from 'redux'
29+ import { StabilityCheck , UseSelectorOptions } from '../../src/hooks/useSelector'
2930
3031// most of these tests depend on selectors being run once, which stabilityCheck doesn't do
3132// rather than specify it every time, let's make a new "default" here
@@ -82,10 +83,7 @@ describe('React', () => {
8283 } )
8384
8485 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 )
8987 let result : number | undefined
9088 const Comp = ( ) => {
9189 const count = useNormalSelector ( selector )
@@ -324,26 +322,36 @@ describe('React', () => {
324322 )
325323
326324 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+ } )
330337 renderedItems . push ( value )
331338 return < div />
332339 }
333340
334341 rtl . render (
335342 < ProviderMock store = { store } >
336343 < Comp />
344+ < Comp2 />
337345 </ ProviderMock >
338346 )
339347
340- expect ( renderedItems . length ) . toBe ( 1 )
348+ expect ( renderedItems . length ) . toBe ( 2 )
341349
342350 rtl . act ( ( ) => {
343351 store . dispatch ( { type : '' } )
344352 } )
345353
346- expect ( renderedItems . length ) . toBe ( 1 )
354+ expect ( renderedItems . length ) . toBe ( 2 )
347355 } )
348356
349357 it ( 'calls selector exactly once on mount and on update' , ( ) => {
@@ -354,11 +362,9 @@ describe('React', () => {
354362 count : count + 1 ,
355363 } ) )
356364
357- let numCalls = 0
358- const selector = ( s : StateType ) => {
359- numCalls += 1
365+ const selector = jest . fn ( ( s : StateType ) => {
360366 return s . count
361- }
367+ } )
362368 const renderedItems : number [ ] = [ ]
363369
364370 const Comp = ( ) => {
@@ -373,14 +379,14 @@ describe('React', () => {
373379 </ ProviderMock >
374380 )
375381
376- expect ( numCalls ) . toBe ( 1 )
382+ expect ( selector ) . toHaveBeenCalledTimes ( 1 )
377383 expect ( renderedItems . length ) . toEqual ( 1 )
378384
379385 rtl . act ( ( ) => {
380386 store . dispatch ( { type : '' } )
381387 } )
382388
383- expect ( numCalls ) . toBe ( 2 )
389+ expect ( selector ) . toHaveBeenCalledTimes ( 2 )
384390 expect ( renderedItems . length ) . toEqual ( 2 )
385391 } )
386392
@@ -392,11 +398,9 @@ describe('React', () => {
392398 count : count + 1 ,
393399 } ) )
394400
395- let numCalls = 0
396- const selector = ( s : StateType ) => {
397- numCalls += 1
401+ const selector = jest . fn ( ( s : StateType ) => {
398402 return s . count
399- }
403+ } )
400404 const renderedItems : number [ ] = [ ]
401405
402406 const Child = ( ) => {
@@ -427,7 +431,7 @@ describe('React', () => {
427431 )
428432
429433 // 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 )
431435 expect ( renderedItems . length ) . toEqual ( 2 )
432436 } )
433437 } )
@@ -733,6 +737,146 @@ describe('React', () => {
733737 ) . toThrow ( )
734738 } )
735739 } )
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+ } )
736880 } )
737881
738882 describe ( 'createSelectorHook' , ( ) => {
0 commit comments