Skip to content

Commit 5cd3283

Browse files
authored
feat: fallback to original mockImplementation if no match (#17)
1 parent 46fb4cb commit 5cd3283

File tree

5 files changed

+41
-14
lines changed

5 files changed

+41
-14
lines changed

README.md

+16-1
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ export const calculateQuestion = async (answer: number): Promise<string> => {
186186

187187
### `when(spy: TFunc, options?: WhenOptions): StubWrapper<TFunc>`
188188

189-
Configures a `vi.fn()` mock function to act as a vitest-when stub. Adds an implementation to the function that initially no-ops, and returns an API to configure behaviors for given arguments using [`.calledWith(...)`][called-with]
189+
Configures a `vi.fn()` or `vi.spyOn()` mock function to act as a vitest-when stub. Adds an implementation to the function that initially no-ops, and returns an API to configure behaviors for given arguments using [`.calledWith(...)`][called-with]
190190

191191
```ts
192192
import { vi } from 'vitest'
@@ -264,6 +264,21 @@ when(overloaded).calledWith().thenReturn(null)
264264
when<() => null>(overloaded).calledWith().thenReturn(null)
265265
```
266266

267+
#### Fallback
268+
269+
By default, if arguments do not match, a vitest-when stub will no-op and return `undefined`. You can customize this fallback by configuring your own unconditional behavior on the mock using Vitest's built-in [mock API][].
270+
271+
```ts
272+
const spy = vi.fn().mockReturnValue('you messed up!')
273+
274+
when(spy).calledWith('hello').thenReturn('world')
275+
276+
spy('hello') // "world"
277+
spy('jello') // "you messed up!"
278+
```
279+
280+
[mock API]: https://vitest.dev/api/mock.html
281+
267282
### `.thenReturn(value: TReturn)`
268283

269284
When the stubbing is satisfied, return `value`

src/debug.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
} from 'pretty-format'
55

66
import { validateSpy, getBehaviorStack } from './stubs'
7-
import type { AnyFunction } from './types'
7+
import type { AnyFunction, MockInstance } from './types'
88
import { type Behavior, BehaviorType } from './behaviors'
99

1010
export interface DebugResult {
@@ -21,11 +21,11 @@ export interface Stubbing {
2121
}
2222

2323
export const getDebug = <TFunc extends AnyFunction>(
24-
spy: TFunc,
24+
spy: TFunc | MockInstance<TFunc>,
2525
): DebugResult => {
26-
const target = validateSpy(spy)
26+
const target = validateSpy<TFunc>(spy)
2727
const name = target.getMockName()
28-
const behaviors = getBehaviorStack<TFunc>(target)
28+
const behaviors = getBehaviorStack(target)
2929
const unmatchedCalls = behaviors?.getUnmatchedCalls() ?? target.mock.calls
3030
const stubbings =
3131
behaviors?.getAll().map((entry) => ({

src/stubs.ts

+11-8
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,20 @@ interface WhenStubImplementation<TFunc extends AnyFunction> {
1616
export const configureStub = <TFunc extends AnyFunction>(
1717
maybeSpy: unknown,
1818
): BehaviorStack<TFunc> => {
19-
const spy = validateSpy(maybeSpy)
19+
const spy = validateSpy<TFunc>(maybeSpy)
2020
const existingBehaviors = getBehaviorStack(spy)
2121

2222
if (existingBehaviors) {
2323
return existingBehaviors
2424
}
2525

2626
const behaviors = createBehaviorStack<TFunc>()
27+
const fallbackImplementation = spy.getMockImplementation()
2728

2829
const implementation = (...args: Parameters<TFunc>) => {
2930
const behavior = behaviors.use(args)?.behavior ?? {
30-
type: BehaviorType.RETURN,
31-
value: undefined,
31+
type: BehaviorType.DO,
32+
callback: fallbackImplementation,
3233
}
3334

3435
switch (behavior.type) {
@@ -50,19 +51,21 @@ export const configureStub = <TFunc extends AnyFunction>(
5051
}
5152

5253
case BehaviorType.DO: {
53-
return behavior.callback(...args)
54+
return behavior.callback?.(...args)
5455
}
5556
}
5657
}
5758

5859
spy.mockImplementation(
59-
Object.assign(implementation, { [BEHAVIORS_KEY]: behaviors }),
60+
Object.assign(implementation as TFunc, { [BEHAVIORS_KEY]: behaviors }),
6061
)
6162

6263
return behaviors
6364
}
6465

65-
export const validateSpy = (maybeSpy: unknown): MockInstance => {
66+
export const validateSpy = <TFunc extends AnyFunction>(
67+
maybeSpy: unknown,
68+
): MockInstance<TFunc> => {
6669
if (
6770
typeof maybeSpy === 'function' &&
6871
'mockImplementation' in maybeSpy &&
@@ -72,14 +75,14 @@ export const validateSpy = (maybeSpy: unknown): MockInstance => {
7275
'getMockName' in maybeSpy &&
7376
typeof maybeSpy.getMockName === 'function'
7477
) {
75-
return maybeSpy as unknown as MockInstance
78+
return maybeSpy as unknown as MockInstance<TFunc>
7679
}
7780

7881
throw new NotAMockFunctionError(maybeSpy)
7982
}
8083

8184
export const getBehaviorStack = <TFunc extends AnyFunction>(
82-
spy: MockInstance,
85+
spy: MockInstance<TFunc>,
8386
): BehaviorStack<TFunc> | undefined => {
8487
const existingImplementation = spy.getMockImplementation() as
8588
| WhenStubImplementation<TFunc>

src/vitest-when.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export interface DebugOptions {
4747
}
4848

4949
export const debug = <TFunc extends AnyFunction>(
50-
spy: TFunc,
50+
spy: TFunc | MockInstance<TFunc>,
5151
options: DebugOptions = {},
5252
): DebugResult => {
5353
const log = options.log ?? true

test/vitest-when.test.ts

+9
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,15 @@ describe('vitest-when', () => {
5555
expect(spy(1, 2, 3)).toEqual(undefined)
5656
})
5757

58+
it('should fall back to original mock implementation', () => {
59+
const spy = vi.fn().mockReturnValue(100)
60+
61+
subject.when(spy).calledWith(1, 2, 3).thenReturn(4)
62+
63+
expect(spy(1, 2, 3)).toEqual(4)
64+
expect(spy()).toEqual(100)
65+
})
66+
5867
it('should return a number of times', () => {
5968
const spy = vi.fn()
6069

0 commit comments

Comments
 (0)