Skip to content

Commit fc5d0d8

Browse files
committed
Add type tests
1 parent b1fbb92 commit fc5d0d8

File tree

1 file changed

+252
-39
lines changed

1 file changed

+252
-39
lines changed

packages/action-listener-middleware/src/tests/listenerMiddleware.test.ts

Lines changed: 252 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,20 @@ import {
44
createSlice,
55
AnyAction,
66
isAnyOf,
7+
PayloadAction,
78
} from '@reduxjs/toolkit'
89
import {
910
createActionListenerMiddleware,
1011
addListenerAction,
1112
removeListenerAction,
1213
When,
1314
ActionListenerMiddlewareAPI,
15+
<<<<<<< HEAD
16+
=======
17+
ActionListenerMiddleware,
18+
TypedAddListenerAction,
19+
TypedAddListener,
20+
>>>>>>> 12a04c75 (Add type tests)
1421
} from '../index'
1522

1623
const middlewareApi = {
@@ -25,6 +32,44 @@ const middlewareApi = {
2532

2633
const noop = () => {}
2734

35+
export declare type IsAny<T, True, False = never> = true | false extends (
36+
T extends never ? true : false
37+
)
38+
? True
39+
: False
40+
41+
export declare type IsUnknown<T, True, False = never> = unknown extends T
42+
? IsAny<T, False, True>
43+
: False
44+
45+
export function expectType<T>(t: T): T {
46+
return t
47+
}
48+
49+
type Equals<T, U> = IsAny<
50+
T,
51+
never,
52+
IsAny<U, never, [T] extends [U] ? ([U] extends [T] ? any : never) : never>
53+
>
54+
export function expectExactType<T>(t: T) {
55+
return <U extends Equals<T, U>>(u: U) => {}
56+
}
57+
58+
type EnsureUnknown<T extends any> = IsUnknown<T, any, never>
59+
export function expectUnknown<T extends EnsureUnknown<T>>(t: T) {
60+
return t
61+
}
62+
63+
type EnsureAny<T extends any> = IsAny<T, any, never>
64+
export function expectExactAny<T extends EnsureAny<T>>(t: T) {
65+
return t
66+
}
67+
68+
type IsNotAny<T> = IsAny<T, never, any>
69+
export function expectNotAny<T extends IsNotAny<T>>(t: T): T {
70+
return t
71+
}
72+
2873
describe('createActionListenerMiddleware', () => {
2974
let store = configureStore({
3075
reducer: () => 42,
@@ -42,16 +87,21 @@ describe('createActionListenerMiddleware', () => {
4287
increment(state) {
4388
state.value += 1
4489
},
90+
// Use the PayloadAction type to declare the contents of `action.payload`
91+
incrementByAmount: (state, action: PayloadAction<number>) => {
92+
state.value += action.payload
93+
},
4594
},
4695
})
47-
const { increment } = counterSlice.actions
96+
const { increment, incrementByAmount } = counterSlice.actions
4897

4998
function delay(ms: number) {
5099
return new Promise((resolve) => setTimeout(resolve, ms))
51100
}
52101

53102
let reducer: jest.Mock
54103
let middleware: ReturnType<typeof createActionListenerMiddleware>
104+
// let middleware: ActionListenerMiddleware<CounterState> //: ReturnType<typeof createActionListenerMiddleware>
55105

56106
const testAction1 = createAction<string>('testAction1')
57107
type TestAction1 = ReturnType<typeof testAction1>
@@ -103,32 +153,6 @@ describe('createActionListenerMiddleware', () => {
103153
expect(resultAction).toBe(originalAction)
104154
})
105155

106-
test.skip('Allows dispatching a thunk without TS errors', () => {
107-
const store = configureStore({
108-
reducer: counterSlice.reducer,
109-
middleware: (gDM) => gDM().prepend(middleware),
110-
})
111-
store.dispatch(increment())
112-
113-
let testState = 0
114-
115-
middleware.addListener(
116-
(action: any, state: any) => {
117-
return increment.match(action) && state > 1
118-
},
119-
(action, listenerApi) => {
120-
// TODO Can't get the thunk dispatch types to carry through
121-
// listenerApi.dispatch((dispatch, getState) => {
122-
// testState = getState()
123-
// })
124-
}
125-
)
126-
127-
store.dispatch(increment())
128-
129-
expect(testState).toBe(2)
130-
})
131-
132156
test('directly subscribing', () => {
133157
const listener = jest.fn((_: TestAction1) => {})
134158

@@ -335,10 +359,14 @@ describe('createActionListenerMiddleware', () => {
335359

336360
test('"can unsubscribe via middleware api', () => {
337361
const listener = jest.fn(
362+
<<<<<<< HEAD
338363
(
339364
action: TestAction1,
340365
api: ActionListenerMiddlewareAPI<any, any, any>
341366
) => {
367+
=======
368+
(action: TestAction1, api: ActionListenerMiddlewareAPI<any, any>) => {
369+
>>>>>>> 12a04c75 (Add type tests)
342370
if (action.payload === 'b') {
343371
api.unsubscribe()
344372
}
@@ -486,38 +514,38 @@ describe('createActionListenerMiddleware', () => {
486514

487515
middleware.addListener(() => {
488516
throw new Error('Predicate Panic!')
489-
}, firstListener);
517+
}, firstListener)
490518

491519
middleware.addListener(matcher, secondListener)
492520

493521
store.dispatch(testAction1('a'))
494-
expect(firstListener).not.toHaveBeenCalled();
522+
expect(firstListener).not.toHaveBeenCalled()
495523
expect(secondListener.mock.calls).toEqual([
496524
[testAction1('a'), middlewareApi],
497525
])
498526
})
499527

500528
test('Notifies listener errors to `onError`, if provided', () => {
501-
const onError = jest.fn();
529+
const onError = jest.fn()
502530
middleware = createActionListenerMiddleware({
503-
onError
531+
onError,
504532
})
505533
reducer = jest.fn(() => ({}))
506534
store = configureStore({
507535
reducer,
508536
middleware: (gDM) => gDM().prepend(middleware),
509537
})
510-
511-
const listenerError = new Error('Boom!');
512-
513-
const matcher = (action: any) => true
514-
538+
539+
const listenerError = new Error('Boom!')
540+
541+
const matcher = (action: any): action is any => true
542+
515543
middleware.addListener(matcher, () => {
516-
throw listenerError;
517-
});
544+
throw listenerError
545+
})
518546

519547
store.dispatch(testAction1('a'))
520-
expect(onError).toBeCalledWith(listenerError);
548+
expect(onError).toBeCalledWith(listenerError)
521549
})
522550

523551
test('condition method resolves promise when the predicate succeeds', async () => {
@@ -602,4 +630,189 @@ describe('createActionListenerMiddleware', () => {
602630

603631
expect(finalCount).toBe(2)
604632
})
633+
634+
describe('Type tests', () => {
635+
const middleware = createActionListenerMiddleware()
636+
const store = configureStore({
637+
reducer: counterSlice.reducer,
638+
middleware: (gDM) => gDM().prepend(middleware),
639+
})
640+
641+
test.skip('State args default to unknown', () => {
642+
middleware.addListener(
643+
(action, currentState, previousState): action is AnyAction => {
644+
expectUnknown(currentState)
645+
expectUnknown(previousState)
646+
return true
647+
},
648+
(action, listenerApi) => {}
649+
)
650+
651+
middleware.addListener(increment.match, (action, listenerApi) => {
652+
const listenerState = listenerApi.getState()
653+
expectUnknown(listenerState)
654+
listenerApi.dispatch((dispatch, getState) => {
655+
const thunkState = getState()
656+
expectUnknown(thunkState)
657+
})
658+
})
659+
660+
store.dispatch(
661+
addListenerAction(
662+
(action, currentState, previousState): action is AnyAction => {
663+
expectUnknown(currentState)
664+
expectUnknown(previousState)
665+
return true
666+
},
667+
(action, listenerApi) => {
668+
const listenerState = listenerApi.getState()
669+
expectUnknown(listenerState)
670+
listenerApi.dispatch((dispatch, getState) => {
671+
const thunkState = getState()
672+
expectUnknown(thunkState)
673+
})
674+
}
675+
)
676+
)
677+
678+
store.dispatch(
679+
addListenerAction(increment.match, (action, listenerApi) => {
680+
const listenerState = listenerApi.getState()
681+
expectUnknown(listenerState)
682+
// TODO Can't get the thunk dispatch types to carry through
683+
listenerApi.dispatch((dispatch, getState) => {
684+
const thunkState = getState()
685+
expectUnknown(thunkState)
686+
})
687+
})
688+
)
689+
})
690+
691+
test.skip('Action type is inferred from args', () => {
692+
middleware.addListener('abcd', (action, listenerApi) => {
693+
expectType<{ type: 'abcd' }>(action)
694+
})
695+
696+
middleware.addListener(incrementByAmount, (action, listenerApi) => {
697+
expectType<PayloadAction<number>>(action)
698+
})
699+
700+
middleware.addListener(incrementByAmount.match, (action, listenerApi) => {
701+
expectType<PayloadAction<number>>(action)
702+
})
703+
704+
store.dispatch(
705+
addListenerAction('abcd', (action, listenerApi) => {
706+
expectType<{ type: 'abcd' }>(action)
707+
})
708+
)
709+
710+
store.dispatch(
711+
addListenerAction(incrementByAmount, (action, listenerApi) => {
712+
expectType<PayloadAction<number>>(action)
713+
})
714+
)
715+
716+
store.dispatch(
717+
addListenerAction(incrementByAmount.match, (action, listenerApi) => {
718+
expectType<PayloadAction<number>>(action)
719+
})
720+
)
721+
})
722+
723+
test.skip('Can create a pre-typed middleware', () => {
724+
const typedMiddleware = createActionListenerMiddleware<CounterState>()
725+
726+
typedMiddleware.addListener(
727+
(action, currentState, previousState): action is AnyAction => {
728+
expectNotAny(currentState)
729+
expectNotAny(previousState)
730+
expectExactType<CounterState>(currentState)
731+
expectExactType<CounterState>(previousState)
732+
return true
733+
},
734+
(action, listenerApi) => {}
735+
)
736+
737+
typedMiddleware.addListener(incrementByAmount, (action, listenerApi) => {
738+
const listenerState = listenerApi.getState()
739+
expectExactType<CounterState>(listenerState)
740+
// TODO Can't get the thunk dispatch types to carry through
741+
listenerApi.dispatch((dispatch, getState) => {
742+
const thunkState = listenerApi.getState()
743+
expectExactType<CounterState>(thunkState)
744+
})
745+
})
746+
})
747+
748+
test.skip('Can create pre-typed versions of addListener and addListenerAction', () => {
749+
const typedAddListener =
750+
middleware.addListener as TypedAddListener<CounterState>
751+
const typedAddListenerAction =
752+
addListenerAction as TypedAddListenerAction<CounterState>
753+
754+
typedAddListener(
755+
(action, currentState, previousState): action is AnyAction => {
756+
expectNotAny(currentState)
757+
expectNotAny(previousState)
758+
expectExactType<CounterState>(currentState)
759+
expectExactType<CounterState>(previousState)
760+
return true
761+
},
762+
(action, listenerApi) => {
763+
const listenerState = listenerApi.getState()
764+
expectExactType<CounterState>(listenerState)
765+
// TODO Can't get the thunk dispatch types to carry through
766+
listenerApi.dispatch((dispatch, getState) => {
767+
const thunkState = listenerApi.getState()
768+
expectExactType<CounterState>(thunkState)
769+
})
770+
}
771+
)
772+
773+
typedAddListener(incrementByAmount.match, (action, listenerApi) => {
774+
const listenerState = listenerApi.getState()
775+
expectExactType<CounterState>(listenerState)
776+
// TODO Can't get the thunk dispatch types to carry through
777+
listenerApi.dispatch((dispatch, getState) => {
778+
const thunkState = listenerApi.getState()
779+
expectExactType<CounterState>(thunkState)
780+
})
781+
})
782+
783+
store.dispatch(
784+
typedAddListenerAction(
785+
(action, currentState, previousState): action is AnyAction => {
786+
expectNotAny(currentState)
787+
expectNotAny(previousState)
788+
expectExactType<CounterState>(currentState)
789+
expectExactType<CounterState>(previousState)
790+
return true
791+
},
792+
(action, listenerApi) => {
793+
const listenerState = listenerApi.getState()
794+
expectExactType<CounterState>(listenerState)
795+
listenerApi.dispatch((dispatch, getState) => {
796+
const thunkState = listenerApi.getState()
797+
expectExactType<CounterState>(thunkState)
798+
})
799+
}
800+
)
801+
)
802+
803+
store.dispatch(
804+
typedAddListenerAction(
805+
incrementByAmount.match,
806+
(action, listenerApi) => {
807+
const listenerState = listenerApi.getState()
808+
expectExactType<CounterState>(listenerState)
809+
listenerApi.dispatch((dispatch, getState) => {
810+
const thunkState = listenerApi.getState()
811+
expectExactType<CounterState>(thunkState)
812+
})
813+
}
814+
)
815+
)
816+
})
817+
})
605818
})

0 commit comments

Comments
 (0)