Skip to content

Commit b3e43d0

Browse files
sheremet-vahi-ogawa
andauthoredDec 9, 2024··
fix(spy)!: spyOn reuses mock if method is already spyed on (#6464)
Co-authored-by: Hiroshi Ogawa <hi.ogawa.zz@gmail.com>
1 parent 8e94427 commit b3e43d0

File tree

4 files changed

+54
-1
lines changed

4 files changed

+54
-1
lines changed
 

‎packages/mocker/src/automocker.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ export function mockObject(
7575
const isFunction
7676
= type.includes('Function') && typeof value === 'function'
7777
if (
78-
(!isFunction || value.__isMockFunction)
78+
(!isFunction || value._isMockFunction)
7979
&& type !== 'Object'
8080
&& type !== 'Module'
8181
) {

‎packages/spy/src/index.ts

+25
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,7 @@ export type Mocked<T> = {
413413
: T[P];
414414
} & T
415415

416+
const vitestSpy = Symbol.for('vitest.spy')
416417
export const mocks: Set<MockInstance> = new Set()
417418

418419
export function isMockFunction(fn: any): fn is MockInstance {
@@ -421,6 +422,20 @@ export function isMockFunction(fn: any): fn is MockInstance {
421422
)
422423
}
423424

425+
function getSpy(
426+
obj: unknown,
427+
method: keyof any,
428+
accessType?: 'get' | 'set',
429+
): MockInstance | undefined {
430+
const desc = Object.getOwnPropertyDescriptor(obj, method)
431+
if (desc) {
432+
const fn = desc[accessType ?? 'value']
433+
if (typeof fn === 'function' && vitestSpy in fn) {
434+
return fn
435+
}
436+
}
437+
}
438+
424439
export function spyOn<T, S extends Properties<Required<T>>>(
425440
obj: T,
426441
methodName: S,
@@ -450,6 +465,11 @@ export function spyOn<T, K extends keyof T>(
450465
} as const
451466
const objMethod = accessType ? { [dictionary[accessType]]: method } : method
452467

468+
const currentStub = getSpy(obj, method, accessType)
469+
if (currentStub) {
470+
return currentStub
471+
}
472+
453473
const stub = tinyspy.internalSpyOn(obj, objMethod as any)
454474

455475
return enhanceSpy(stub) as MockInstance
@@ -523,6 +543,11 @@ function enhanceSpy<T extends Procedure>(
523543

524544
let name: string = (stub as any).name
525545

546+
Object.defineProperty(stub, vitestSpy, {
547+
value: true,
548+
enumerable: false,
549+
})
550+
526551
stub.getMockName = () => name || 'vi.fn()'
527552
stub.mockName = (n) => {
528553
name = n

‎test/core/test/jest-mock.test.ts

+25
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,31 @@ describe('jest mock compat layer', () => {
363363
expect(obj.property).toBe(true)
364364
})
365365

366+
it('spyOn returns the same spy twice', () => {
367+
const obj = {
368+
method() {
369+
return 'original'
370+
},
371+
}
372+
373+
const spy1 = vi.spyOn(obj, 'method').mockImplementation(() => 'mocked')
374+
const spy2 = vi.spyOn(obj, 'method')
375+
376+
expect(vi.isMockFunction(obj.method)).toBe(true)
377+
expect(obj.method()).toBe('mocked')
378+
expect(spy1).toBe(spy2)
379+
380+
spy2.mockImplementation(() => 'mocked2')
381+
382+
expect(obj.method()).toBe('mocked2')
383+
384+
spy2.mockRestore()
385+
386+
expect(obj.method()).toBe('original')
387+
expect(vi.isMockFunction(obj.method)).toBe(false)
388+
expect(obj.method).not.toBe(spy1)
389+
})
390+
366391
it('should spy on property setter (2), and mockReset should not restore original descriptor', () => {
367392
const obj = {
368393
_property: false,

‎test/core/test/mocked.test.ts

+3
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,9 @@ describe('mocked classes', () => {
7878

7979
expect(MockedC.prototype.doSomething).toHaveBeenCalledOnce()
8080
expect(MockedC.prototype.doSomething).not.toHaveReturnedWith('A')
81+
82+
vi.mocked(instance.doSomething).mockRestore()
83+
expect(instance.doSomething()).not.toBe('A')
8184
})
8285

8386
test('should mock getters', () => {

0 commit comments

Comments
 (0)
Please sign in to comment.