Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions packages/spy/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,23 @@ function createMock(
try {
if (new.target) {
returnValue = Reflect.construct(implementation, args, new.target)
const implementationPrototype = (implementation as any).prototype

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any explanation why Reflect.construct just doesn't work? Does it behave the same outside of Vitest?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's is quite mind-bending, but I got the gist. Added root cause in the PR description. I'm not settled with the fix. There is still some divergence and probably need to pick some trade off.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I remember this now. I don't think there is a good solution for this, all of them break something, it just needs to be documented

if (
// prototypeMembers is used by automocking case and we want to skip that case.
prototypeMembers.length === 0
&& new.target.prototype
&& implementationPrototype
// skip when constructor returns value
&& Object.getPrototypeOf(returnValue) === new.target.prototype
) {
const { properties, descriptors } = getAllProperties(implementationPrototype)
for (const property of properties) {
if (property === 'constructor' || property in returnValue) {
continue
}
Object.defineProperty(returnValue, property, descriptors[property]!)
}
}

// jest calls this before the implementation, but we have to resolve this _after_
// because we cannot do it before the `Reflect.construct` called the custom implementation.
Expand Down
50 changes: 50 additions & 0 deletions test/unit/test/mocking/vi-fn.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -799,6 +799,56 @@
expect(Mock.mock.calls).toEqual([['test', 42]])
})

test('vi.fn(class) exposes prototype methods on constructed instances', () => {
const Mock = vi.fn(class {
method() {
return 'mocked'
}
})

expect(new Mock().method()).toBe('mocked')
})

test('vi.fn(class) does not copy prototype methods when constructor returns an object', () => {
const returned = { custom: true }
const Mock = vi.fn(class {
constructor() {
return returned as any
}

method() {
return 'mocked'
}
})

const instance = new Mock()

expect(instance).toBe(returned)
expect(instance.method).toBeUndefined()
})

test('vi.fn(class) does not leak prototype methods from once implementations', () => {
const Mock = vi.fn()
.mockImplementationOnce(class {

Check failure on line 832 in test/unit/test/mocking/vi-fn.test.ts

View workflow job for this annotation

GitHub Actions / Lint: node-latest, ubuntu-latest

Argument of type 'typeof (Anonymous class)' is not assignable to parameter of type '(...args: any[]) => any'.
first() {
return 'first'
}
})
.mockImplementation(class {

Check failure on line 837 in test/unit/test/mocking/vi-fn.test.ts

View workflow job for this annotation

GitHub Actions / Lint: node-latest, ubuntu-latest

Argument of type 'typeof (Anonymous class)' is not assignable to parameter of type '(...args: any[]) => any'.
second() {
return 'second'
}
})

const first = new Mock()
const second = new Mock()

expect(first.first()).toBe('first')
expect(first.second).toBeUndefined()
expect(second.second()).toBe('second')
expect(second.first).toBeUndefined()
})

test('vi.fn() with mockReturnValue throws when called with new', () => {
const Mock = vi.fn()
Mock.mockReturnValue(42)
Expand Down
17 changes: 17 additions & 0 deletions test/unit/test/mocking/vi-spyOn.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,23 @@ describe('vi.spyOn() state', () => {
assertStateEmpty(state)
})

test('vi.spyOn() exposes mockImplementation class prototype methods', () => {
class OriginalClass {
method() {
return 'original'
}
}
const object = { Class: OriginalClass }

vi.spyOn(object, 'Class').mockImplementation(class MockClass extends OriginalClass {
method() {
return 'mocked'
}
})

expect(new object.Class().method()).toBe('mocked')
})

test('vi.spyOn() spies and tracks overridden async calls', async () => {
const object = createObject()
const mock = vi.spyOn(object, 'async')
Expand Down
Loading