Skip to content

Commit 24a97c2

Browse files
authored
fix: work around getMockImplementation() in Vitest (#28)
Ensure that default implementations set via `vi.fn(default_impl)` are used, even when Vitest forgets they exists, by pulling from tinyspy internals
1 parent e76e732 commit 24a97c2

File tree

3 files changed

+58
-1
lines changed

3 files changed

+58
-1
lines changed

src/fallback-implementation.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import type { AnyCallable, MockInstance } from './types.ts'
2+
3+
/** Get the fallback implementation of a mock if no matching stub is found. */
4+
export const getFallbackImplementation = <TFunc extends AnyCallable>(
5+
mock: MockInstance<TFunc>,
6+
): TFunc | undefined => {
7+
return (
8+
mock.getMockImplementation() ?? getTinyspyInternals(mock)?.getOriginal()
9+
)
10+
}
11+
12+
/** Internal state from Tinyspy, where a mock's default implementation is stored. */
13+
interface TinyspyInternals<TFunc extends AnyCallable> {
14+
getOriginal: () => TFunc | undefined
15+
}
16+
17+
/**
18+
* Get the fallback implementation out of tinyspy internals.
19+
*
20+
* This slight hack works around a bug in Vitest <= 3
21+
* where `getMockImplementation` will return `undefined` after `mockReset`,
22+
* even if a default implementation is still active.
23+
* The implementation remains present in tinyspy internal state,
24+
* which is stored on a Symbol key in the mock object.
25+
*/
26+
const getTinyspyInternals = <TFunc extends AnyCallable>(
27+
mock: MockInstance<TFunc>,
28+
): TinyspyInternals<TFunc> | undefined => {
29+
const maybeTinyspy = mock as unknown as Record<PropertyKey, unknown>
30+
31+
for (const key of Object.getOwnPropertySymbols(maybeTinyspy)) {
32+
const maybeTinyspyInternals = maybeTinyspy[key]
33+
34+
if (
35+
maybeTinyspyInternals &&
36+
typeof maybeTinyspyInternals === 'object' &&
37+
'getOriginal' in maybeTinyspyInternals &&
38+
typeof maybeTinyspyInternals.getOriginal === 'function'
39+
) {
40+
return maybeTinyspyInternals as TinyspyInternals<TFunc>
41+
}
42+
}
43+
44+
return undefined
45+
}

src/stubs.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
createBehaviorStack,
55
} from './behaviors.ts'
66
import { NotAMockFunctionError } from './errors.ts'
7+
import { getFallbackImplementation } from './fallback-implementation.ts'
78
import type {
89
AnyCallable,
910
AnyFunction,
@@ -29,7 +30,7 @@ export const configureStub = <TFunc extends AnyCallable>(
2930
}
3031

3132
const behaviors = createBehaviorStack<TFunc>()
32-
const fallbackImplementation = spy.getMockImplementation()
33+
const fallbackImplementation = getFallbackImplementation(spy)
3334

3435
const implementation = (...args: ExtractParameters<TFunc>) => {
3536
const behavior = behaviors.use(args)?.behavior ?? {

test/vitest-when.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,17 @@ describe('vitest-when', () => {
6464
expect(spy()).toEqual(100)
6565
})
6666

67+
it('should fall back to original implementation after reset', () => {
68+
const spy = vi.fn((n) => 2 * n)
69+
70+
vi.resetAllMocks()
71+
expect(spy(2)).toEqual(4)
72+
73+
subject.when(spy).calledWith(1).thenReturn(4)
74+
expect(spy(1)).toEqual(4)
75+
expect(spy(2)).toEqual(4)
76+
})
77+
6778
it('should return a number of times', () => {
6879
const spy = vi.fn()
6980

0 commit comments

Comments
 (0)