Skip to content

Commit 80b8a51

Browse files
EskiMojo14markerikson
authored andcommitted
add stability check tests
1 parent 86716db commit 80b8a51

File tree

1 file changed

+164
-20
lines changed

1 file changed

+164
-20
lines changed

test/hooks/useSelector.spec.tsx

Lines changed: 164 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import type {
2626
} from '../../src/index'
2727
import type { FunctionComponent, DispatchWithoutAction, ReactNode } from 'react'
2828
import 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

Comments
 (0)