Skip to content

Commit 261917e

Browse files
feat(overmind): add createMock for testing
1 parent 70e4ed0 commit 261917e

File tree

4 files changed

+261
-93
lines changed

4 files changed

+261
-93
lines changed

packages/node_modules/overmind/src/index.ts

Lines changed: 157 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
ResolveActions,
1818
ResolveState,
1919
Execution,
20+
ResolveMockActions,
2021
} from './internalTypes'
2122
import { proxifyEffects } from './proxyfyEffects'
2223
import {
@@ -61,6 +62,76 @@ export const makeStringifySafeMutations = (mutations: IMutation[]) => {
6162
}))
6263
}
6364

65+
function deepCopy(obj) {
66+
if (isPlainObject(obj)) {
67+
return Object.keys(obj).reduce((aggr, key) => {
68+
aggr[key] = deepCopy(obj[key])
69+
70+
return aggr
71+
}, {})
72+
} else if (Array.isArray(obj)) {
73+
return obj.map((item) => deepCopy(item))
74+
}
75+
76+
return obj
77+
}
78+
79+
export function createMock<Config extends Configuration>(
80+
config: Config,
81+
effectsCallback?: (
82+
effect: {
83+
path: string
84+
args: any[]
85+
},
86+
index: number
87+
) => any
88+
): {
89+
actions: ResolveMockActions<Config['actions']>
90+
state: ResolveState<Config['state']>
91+
} {
92+
let effectCount = 0
93+
let effectsCalled = []
94+
const mock = new Overmind(
95+
Object.assign({}, config, {
96+
state: deepCopy(config.state),
97+
}),
98+
{
99+
devtools: false,
100+
testMode: {
101+
effectsCallback: (effect) => {
102+
if (!effectsCallback) {
103+
return
104+
}
105+
106+
return effectsCallback(
107+
{
108+
path: effect.name + '.' + effect.method,
109+
args: effect.args,
110+
},
111+
effectCount++
112+
)
113+
},
114+
actionCallback: (execution) => {
115+
const effects = effectsCalled.slice()
116+
117+
effectCount = 0
118+
effectsCalled.length = 0
119+
120+
return {
121+
mutations: execution.flush().mutations,
122+
effects,
123+
}
124+
},
125+
},
126+
}
127+
)
128+
129+
return {
130+
actions: mock.actions as any,
131+
state: mock.state,
132+
}
133+
}
134+
64135
const hotReloadingCache = {}
65136

66137
// We do not use TConfig<Config> directly to type the class in order to avoid
@@ -70,6 +141,7 @@ export class Overmind<Config extends Configuration> implements Configuration {
70141
private proxyStateTree: ProxyStateTree<object>
71142
private actionReferences: Function[] = []
72143
private nextExecutionId: number = 0
144+
private options: Options
73145
initialized: Promise<any>
74146
eventHub: EventEmitter<Events>
75147
devtools: Devtools
@@ -108,6 +180,7 @@ export class Overmind<Config extends Configuration> implements Configuration {
108180
this.effects = configuration.effects || {}
109181
this.proxyStateTree = proxyStateTree
110182
this.eventHub = eventHub
183+
this.options = options
111184

112185
if (!IS_PRODUCTION && typeof window !== 'undefined') {
113186
let warning = 'OVERMIND: You are running in DEVELOPMENT mode.'
@@ -157,7 +230,7 @@ export class Overmind<Config extends Configuration> implements Configuration {
157230
nextTick && clearTimeout(nextTick)
158231
nextTick = setTimeout(flushTree, 0)
159232
})
160-
} else {
233+
} else if (!options.testMode) {
161234
eventHub.on(EventType.OPERATOR_ASYNC, (execution) => {
162235
const flushData = execution.flush()
163236
if (this.devtools && flushData) {
@@ -264,7 +337,7 @@ export class Overmind<Config extends Configuration> implements Configuration {
264337
}
265338
private createAction(name, action) {
266339
this.actionReferences.push(action)
267-
return (value?) => {
340+
const actionFunc = (value?) => {
268341
if (IS_PRODUCTION || action[IS_OPERATOR]) {
269342
return new Promise((resolve, reject) => {
270343
const execution = this.createExecution(name, action)
@@ -288,15 +361,19 @@ export class Overmind<Config extends Configuration> implements Configuration {
288361
operatorId: finalContext.execution.operatorId - 1,
289362
})
290363
if (err) reject(err)
291-
else resolve(finalContext.value)
364+
else resolve(this.options.testMode && finalContext.execution)
292365
}
293366
)
294-
: action(
295-
this.createContext(
296-
value,
297-
execution,
298-
execution.getMutationTree()
367+
: resolve(
368+
action(
369+
this.createContext(
370+
value,
371+
execution,
372+
execution.getMutationTree()
373+
)
299374
)
375+
? undefined
376+
: undefined
300377
)
301378
})
302379
} else {
@@ -332,32 +409,94 @@ export class Overmind<Config extends Configuration> implements Configuration {
332409
})
333410
})
334411

335-
const result = action(
336-
this.createContext(
337-
this.scopeValue(value, mutationTree),
338-
execution,
339-
mutationTree
340-
)
412+
const context = this.createContext(
413+
this.scopeValue(value, mutationTree),
414+
execution,
415+
mutationTree
341416
)
417+
const result = action(context)
418+
342419
this.eventHub.emit(EventType.OPERATOR_END, {
343420
...execution,
344421
isAsync: result instanceof Promise,
345422
result: undefined,
346423
})
347424
this.eventHub.emit(EventType.ACTION_END, execution)
348425

349-
return Promise.resolve(result)
426+
return Promise.resolve(this.options.testMode && execution)
350427
}
351428
}
429+
430+
if (this.options.testMode) {
431+
const actionCallback = this.options.testMode.actionCallback
432+
433+
return (value?) => (actionFunc as any)(value).then(actionCallback)
434+
}
435+
436+
return actionFunc
352437
}
353438
private trackEffects(effects = {}, execution) {
354439
if (IS_PRODUCTION) {
355440
return effects
356441
}
357442

358-
return proxifyEffects(this.effects, (effect) =>
359-
this.eventHub.emit(EventType.EFFECT, { ...execution, ...effect })
360-
)
443+
return proxifyEffects(this.effects, (effect) => {
444+
let result
445+
try {
446+
result = this.options.testMode
447+
? this.options.testMode.effectsCallback(effect)
448+
: effect.func.apply(this, effect.args)
449+
} catch (error) {
450+
// eslint-disable-next-line standard/no-callback-literal
451+
this.eventHub.emit(EventType.EFFECT, {
452+
...execution,
453+
...effect,
454+
isPending: false,
455+
error: error.message,
456+
})
457+
return
458+
}
459+
460+
if (result instanceof Promise) {
461+
// eslint-disable-next-line standard/no-callback-literal
462+
this.eventHub.emit(EventType.EFFECT, {
463+
...execution,
464+
...effect,
465+
isPending: true,
466+
error: false,
467+
})
468+
result
469+
.then((promisedResult) => {
470+
// eslint-disable-next-line standard/no-callback-literal
471+
this.eventHub.emit(EventType.EFFECT, {
472+
...execution,
473+
...effect,
474+
result: promisedResult,
475+
isPending: false,
476+
error: false,
477+
})
478+
})
479+
.catch((error) => {
480+
this.eventHub.emit(EventType.EFFECT, {
481+
...execution,
482+
...effect,
483+
isPending: false,
484+
error: error.message,
485+
})
486+
})
487+
} else {
488+
// eslint-disable-next-line standard/no-callback-literal
489+
this.eventHub.emit(EventType.EFFECT, {
490+
...execution,
491+
...effect,
492+
result,
493+
isPending: false,
494+
error: false,
495+
})
496+
}
497+
498+
return result
499+
})
361500
}
362501
private initializeDevtools(host, eventHub, proxyStateTree) {
363502
const devtools = new Devtools(

packages/node_modules/overmind/src/internalTypes.ts

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,17 @@ export type Options = {
1010
name?: string
1111
devtools?: string | boolean
1212
logProxies?: boolean
13+
testMode?: {
14+
effectsCallback: (
15+
effect: {
16+
effectId: number
17+
name: string
18+
method: string
19+
args: any[]
20+
}
21+
) => {}
22+
actionCallback: (execution: any) => void
23+
}
1324
}
1425

1526
export enum EventType {
@@ -169,15 +180,48 @@ export type ResolveActions<
169180
: {
170181
[T in keyof Actions]: Actions[T] extends TAction<any, any>
171182
? [TActionValue<Actions[T]>] extends [void]
172-
? () => Promise<ReturnType<Actions[T]>>
173-
: (value: TActionValue<Actions[T]>) => Promise<ReturnType<Actions[T]>>
183+
? () => Promise<void>
184+
: (value: TActionValue<Actions[T]>) => Promise<void>
174185
: Actions[T] extends TOperator<any, any, any>
175186
? [TOperationValue<Actions[T]>] extends [void]
176-
? () => Promise<ReturnType<Actions[T]>>
177-
: (
178-
value: TOperationValue<Actions[T]>
179-
) => Promise<ReturnType<Actions[T]>>
187+
? () => Promise<void>
188+
: (value: TOperationValue<Actions[T]>) => Promise<void>
180189
: Actions[T] extends NestedActions
181190
? ResolveActions<Actions[T]>
182191
: never
183192
}
193+
194+
type NestedMockActions =
195+
| {
196+
[key: string]:
197+
| TAction<any, any>
198+
| TOperator<any, any, any>
199+
| NestedMockActions
200+
}
201+
| undefined
202+
203+
type MockResult = {
204+
mutations: IMutation[]
205+
effects: ({
206+
path: string
207+
args: any[]
208+
})[]
209+
}
210+
211+
export type ResolveMockActions<
212+
Actions extends NestedMockActions
213+
> = Actions extends undefined
214+
? {}
215+
: {
216+
[T in keyof Actions]: Actions[T] extends TAction<any, any>
217+
? [TActionValue<Actions[T]>] extends [void]
218+
? () => Promise<MockResult>
219+
: (value: TActionValue<Actions[T]>) => Promise<MockResult>
220+
: Actions[T] extends TOperator<any, any, any>
221+
? [TOperationValue<Actions[T]>] extends [void]
222+
? () => Promise<MockResult>
223+
: (value: TOperationValue<Actions[T]>) => Promise<MockResult>
224+
: Actions[T] extends NestedMockActions
225+
? ResolveMockActions<Actions[T]>
226+
: never
227+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { createMock, TAction } from './'
2+
3+
type State = {
4+
foo: string
5+
upperFoo: string
6+
}
7+
8+
describe('Mock', () => {
9+
test('should run action tests', () => {
10+
type State = {
11+
foo: string
12+
}
13+
const state: State = {
14+
foo: 'bar',
15+
}
16+
const test: Action = ({ state }) => {
17+
state.foo = 'bar2'
18+
}
19+
const actions = { test }
20+
const config = {
21+
state,
22+
actions,
23+
}
24+
25+
type Config = {
26+
state: typeof state
27+
actions: typeof actions
28+
}
29+
30+
type Action<Input = void> = TAction<Config, Input>
31+
32+
const mock = createMock(config)
33+
34+
return mock.actions.test().then((result) =>
35+
expect(result).toEqual({
36+
mutations: [
37+
{
38+
method: 'set',
39+
path: 'foo',
40+
args: ['bar2'],
41+
},
42+
],
43+
effects: [],
44+
})
45+
)
46+
})
47+
})

0 commit comments

Comments
 (0)