Skip to content

Commit

Permalink
fix(spy)!: spyOn reuses mock if method is already spyed on (#6464)
Browse files Browse the repository at this point in the history
Co-authored-by: Hiroshi Ogawa <[email protected]>
  • Loading branch information
sheremet-va and hi-ogawa authored Dec 9, 2024
1 parent 8e94427 commit b3e43d0
Show file tree
Hide file tree
Showing 4 changed files with 54 additions and 1 deletion.
2 changes: 1 addition & 1 deletion packages/mocker/src/automocker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export function mockObject(
const isFunction
= type.includes('Function') && typeof value === 'function'
if (
(!isFunction || value.__isMockFunction)
(!isFunction || value._isMockFunction)
&& type !== 'Object'
&& type !== 'Module'
) {
Expand Down
25 changes: 25 additions & 0 deletions packages/spy/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,7 @@ export type Mocked<T> = {
: T[P];
} & T

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

export function isMockFunction(fn: any): fn is MockInstance {
Expand All @@ -421,6 +422,20 @@ export function isMockFunction(fn: any): fn is MockInstance {
)
}

function getSpy(
obj: unknown,
method: keyof any,
accessType?: 'get' | 'set',
): MockInstance | undefined {
const desc = Object.getOwnPropertyDescriptor(obj, method)
if (desc) {
const fn = desc[accessType ?? 'value']
if (typeof fn === 'function' && vitestSpy in fn) {
return fn
}
}
}

export function spyOn<T, S extends Properties<Required<T>>>(
obj: T,
methodName: S,
Expand Down Expand Up @@ -450,6 +465,11 @@ export function spyOn<T, K extends keyof T>(
} as const
const objMethod = accessType ? { [dictionary[accessType]]: method } : method

const currentStub = getSpy(obj, method, accessType)
if (currentStub) {
return currentStub
}

const stub = tinyspy.internalSpyOn(obj, objMethod as any)

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

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

Object.defineProperty(stub, vitestSpy, {
value: true,
enumerable: false,
})

stub.getMockName = () => name || 'vi.fn()'
stub.mockName = (n) => {
name = n
Expand Down
25 changes: 25 additions & 0 deletions test/core/test/jest-mock.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,31 @@ describe('jest mock compat layer', () => {
expect(obj.property).toBe(true)
})

it('spyOn returns the same spy twice', () => {
const obj = {
method() {
return 'original'
},
}

const spy1 = vi.spyOn(obj, 'method').mockImplementation(() => 'mocked')
const spy2 = vi.spyOn(obj, 'method')

expect(vi.isMockFunction(obj.method)).toBe(true)
expect(obj.method()).toBe('mocked')
expect(spy1).toBe(spy2)

spy2.mockImplementation(() => 'mocked2')

expect(obj.method()).toBe('mocked2')

spy2.mockRestore()

expect(obj.method()).toBe('original')
expect(vi.isMockFunction(obj.method)).toBe(false)
expect(obj.method).not.toBe(spy1)
})

it('should spy on property setter (2), and mockReset should not restore original descriptor', () => {
const obj = {
_property: false,
Expand Down
3 changes: 3 additions & 0 deletions test/core/test/mocked.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ describe('mocked classes', () => {

expect(MockedC.prototype.doSomething).toHaveBeenCalledOnce()
expect(MockedC.prototype.doSomething).not.toHaveReturnedWith('A')

vi.mocked(instance.doSomething).mockRestore()
expect(instance.doSomething()).not.toBe('A')
})

test('should mock getters', () => {
Expand Down

0 comments on commit b3e43d0

Please sign in to comment.