-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathtesting-utils.ts
145 lines (134 loc) · 4.29 KB
/
testing-utils.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
// Copyright 2022 MFB Technologies, Inc.
import { AsyncThunkAction, configureStore, Reducer } from "@reduxjs/toolkit"
import { merge } from "./utils"
/**
* Create mock state from an initial state and a series of transformer functions.
*
* The first call to this function passes in the root state
* object that will be the base for producing mock state. The second
* call passes in an array of transformers that will change various
* properties of the state object to produce a new object.
*/
export const produceMockState: <TState>(
state: TState
) => <TTransformers extends Array<(state: TState) => TState>>(
transformers: TTransformers
) => TState = state => {
return transformers => {
return merge(...transformers)(state)
}
}
/**
* Create a transformer to change a state property.
*
* The first call to this function simply passes in a type
* parameter that specifies the type of object on which properties
* are going to be set. That returns a function that accepts the key of the
* property to be changed and either a new value or a function to
* transform the old value. It then returns a function that will transform
* a state object accordingly.
*/
export const produceStateSetter: <TState extends Record<string, any>>() => <
TKey extends keyof TState
>(
key: TKey,
f: ((value: TState[TKey]) => TState[TKey]) | TState[TKey]
) => (state: TState) => TState = () => {
return (key, f) => {
return state => {
const newState = { ...state }
if (typeof f === "function") {
//we know this has resolved as a function
newState[key] = (f as any)(state[key])
} else {
newState[key] = f
}
return newState
}
}
}
type FakeSlice<TState> = {
name: string
reducer: Reducer<TState>
getInitialState: () => TState
}
type StateFromSlice<T extends FakeSlice<any>> = T extends FakeSlice<infer X>
? X
: never
type ResultFromUnwrap<
TArg extends AsyncThunkAction<any, any, any>,
U extends boolean | undefined
> = [U] extends [false]
? UnwrapPromise<ReturnType<TArg>>
: [U] extends [true]
? GetReturned<TArg>
: GetReturned<TArg>
type GetReturned<T extends AsyncThunkAction<any, any, any>> =
T extends AsyncThunkAction<infer X, any, any> ? X : never
type UnwrapPromise<T extends Promise<any>> = T extends Promise<infer X>
? X
: never
/**
* Test async thunks with an actual store.
*
* Function that takes (a) a slice and (b) an initial state object or
* a function to transform the initial state of the slice. It returns
* a function that will accept the return of an async thunk and will
* return both the update state and the result of the thunk. By default
* the result of the thunk will be unwrapped (which means it will throw
* if the thunk returns an error). This behavior can be turned off by
* passing 'false' as the second argument.
*/
export function produceAsyncThunkTester<TSlice extends FakeSlice<any>>(
slice: TSlice,
initialStateOrTransform?: TSlice extends FakeSlice<infer TState>
? TState | ((state: TState) => TState)
: never
) {
let preloadedSliceState: StateFromSlice<TSlice> | undefined
if (typeof initialStateOrTransform === "function") {
//we know this has resolved as a function
preloadedSliceState = (initialStateOrTransform as any)(
slice.getInitialState()
)
} else {
preloadedSliceState = initialStateOrTransform as
| StateFromSlice<TSlice>
| undefined
}
const store = configureStore({
reducer: {
[slice.name]: slice.reducer
},
preloadedState: preloadedSliceState
? { [slice.name]: preloadedSliceState }
: (undefined as any)
})
return async <
TArg extends AsyncThunkAction<any, any, any>,
U extends boolean | undefined
>(
arg: TArg,
unwrap?: U
): Promise<{
result: ResultFromUnwrap<TArg, U>
state: StateFromSlice<TSlice>
}> => {
const shouldUnwrap = unwrap ?? true
if (shouldUnwrap) {
const unwrappedResult = await store.dispatch(arg).unwrap()
return {
result: unwrappedResult,
state: store.getState()[slice.name]
}
} else {
const plainResult = await store.dispatch(arg)
// the return type of the function will handle setting
// the type
return {
result: plainResult as any,
state: store.getState()[slice.name]
}
}
}
}