Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
716e207
fix!: rewrite spying implementation to make module mocking more logical
sheremet-va Jul 30, 2025
2082369
chore: lint
sheremet-va Jul 30, 2025
f657700
fix: copy static props, restore some previous behaviours
sheremet-va Jul 30, 2025
d3f87e4
fix: do not copy getters
sheremet-va Jul 30, 2025
bfda423
fix: keep getters and setters in spy mode
sheremet-va Jul 30, 2025
86eb9b1
chore: lint
sheremet-va Jul 30, 2025
6a1ba96
fix: keep using the original name in vi.spyOn
sheremet-va Jul 30, 2025
b3f3e22
docs: add migration guide
sheremet-va Jul 30, 2025
3cc251b
chore: remove debugging comment
sheremet-va Jul 30, 2025
fde9888
test: fix browser snapshot test
sheremet-va Jul 30, 2025
9bae554
fix: don't allow overriding mock.mock
sheremet-va Jul 30, 2025
f7d0227
docs: add example of mock.mock to jest differences
sheremet-va Jul 30, 2025
97c01d8
chore: remove tinyspy from dependencies
sheremet-va Jul 30, 2025
bec02bf
refactor: cleanup
sheremet-va Jul 31, 2025
5f12b6a
docs: cleanup
sheremet-va Jul 31, 2025
0e14a62
test: more test
sheremet-va Jul 31, 2025
2a03266
docs: add mocking modules guide
sheremet-va Jul 31, 2025
b6932df
fix: don't empty array if `spy` is set to `true`
sheremet-va Jul 31, 2025
46b4846
chore: fix links
sheremet-va Jul 31, 2025
93cb84c
docs: cleanup
sheremet-va Jul 31, 2025
c1e2252
docs: cleanup
sheremet-va Jul 31, 2025
1cb2d0d
chore: add a log for console.warn
sheremet-va Aug 1, 2025
4bb8b5e
docs: add virtual modules
sheremet-va Aug 1, 2025
35c3aef
docs: update mocking docs
sheremet-va Aug 1, 2025
6c2a4d5
test: don't use using
sheremet-va Aug 1, 2025
952755d
docs: remove irrelevant comment
sheremet-va Aug 1, 2025
72a0634
docs: fix link
sheremet-va Aug 1, 2025
5a32529
docs: cleanup
sheremet-va Aug 1, 2025
34adde5
chore: cleanup
sheremet-va Aug 1, 2025
4f48ca9
Merge branch 'main' of github.com:vitest-dev/vitest into fix/mocking-…
sheremet-va Aug 1, 2025
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
140 changes: 79 additions & 61 deletions packages/mocker/src/automocker.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import type { MockedModuleType } from './registry'

type Key = string | symbol

export type CreateMockInstanceProcedure = (options?: {
prototypeMembers?: (string | symbol)[]
name?: string | symbol
originalImplementation?: (...args: any[]) => any
keepMembersImplementation?: boolean
}) => any

export interface MockObjectOptions {
type: MockedModuleType
type: 'automock' | 'autospy'
globalConstructors: GlobalConstructors
spyOn: (obj: any, prop: Key) => any
createMockInstance: CreateMockInstanceProcedure
}

export function mockObject(
Expand Down Expand Up @@ -40,7 +45,19 @@ export function mockObject(
// Modules define their exports as getters. We want to process those.
if (!isModule && descriptor.get) {
try {
Object.defineProperty(newContainer, property, descriptor)
if (options.type === 'autospy') {
Object.defineProperty(newContainer, property, descriptor)
}
else {
Object.defineProperty(newContainer, property, {
configurable: descriptor.configurable,
enumerable: descriptor.enumerable,
// automatically mock getters and setters
// https://github.com/vitest-dev/vitest/issues/8345
get: () => {},
set: descriptor.set ? () => {} : undefined,
})
}
}
catch {
// Ignore errors, just move on to the next prop.
Expand All @@ -49,7 +66,7 @@ export function mockObject(
}

// Skip special read-only props, we don't want to mess with those.
if (isSpecialProp(property, containerType)) {
if (isReadonlyProp(container[property], property)) {
continue
}

Expand Down Expand Up @@ -90,58 +107,23 @@ export function mockObject(
}

if (isFunction) {
if (!options.spyOn) {
if (!options.createMockInstance) {
throw new Error(
'[@vitest/mocker] `spyOn` is not defined. This is a Vitest error. Please open a new issue with reproduction.',
'[@vitest/mocker] `createMockInstance` is not defined. This is a Vitest error. Please open a new issue with reproduction.',
)
}
const spyOn = options.spyOn
function mockFunction(this: any) {
// detect constructor call and mock each instance's methods
// so that mock states between prototype/instances don't affect each other
// (jest reference https://github.com/jestjs/jest/blob/2c3d2409879952157433de215ae0eee5188a4384/packages/jest-mock/src/index.ts#L678-L691)
if (this instanceof newContainer[property]) {
for (const { key, descriptor } of getAllMockableProperties(
this,
false,
options.globalConstructors,
)) {
// skip getter since it's not mocked on prototype as well
if (descriptor.get) {
continue
}

const value = this[key]
const type = getType(value)
const isFunction
= type.includes('Function') && typeof value === 'function'
if (isFunction) {
// mock and delegate calls to original prototype method, which should be also mocked already
const original = this[key]
const mock = spyOn(this, key as string)
.mockImplementation(original)
const origMockReset = mock.mockReset
mock.mockRestore = mock.mockReset = () => {
origMockReset.call(mock)
mock.mockImplementation(original)
return mock
}
}
}
}
}
const mock = spyOn(newContainer, property)
if (options.type === 'automock') {
mock.mockImplementation(mockFunction)
const origMockReset = mock.mockReset
mock.mockRestore = mock.mockReset = () => {
origMockReset.call(mock)
mock.mockImplementation(mockFunction)
return mock
}
}
// tinyspy retains length, but jest doesn't.
Object.defineProperty(newContainer[property], 'length', { value: 0 })
const createMockInstance = options.createMockInstance
const currentValue = newContainer[property]
const prototypeMembers = currentValue.prototype
? collectFunctionProperties(currentValue.prototype)
: []
const mock = createMockInstance({
name: currentValue.name,
prototypeMembers,
originalImplementation: options.type === 'autospy' ? currentValue : undefined,
keepMembersImplementation: options.type === 'autospy',
})
newContainer[property] = mock
}

refs.track(value, newContainer[property])
Expand Down Expand Up @@ -184,12 +166,33 @@ function getType(value: unknown): string {
return Object.prototype.toString.apply(value).slice(8, -1)
}

function isSpecialProp(prop: Key, parentType: string) {
return (
parentType.includes('Function')
&& typeof prop === 'string'
&& ['arguments', 'callee', 'caller', 'length', 'name'].includes(prop)
)
function isReadonlyProp(object: unknown, prop: string | symbol) {
if (
prop === 'arguments'
|| prop === 'caller'
|| prop === 'callee'
|| prop === 'name'
|| prop === 'length'
) {
const typeName = getType(object)
return (
typeName === 'Function'
|| typeName === 'AsyncFunction'
|| typeName === 'GeneratorFunction'
|| typeName === 'AsyncGeneratorFunction'
)
}

if (
prop === 'source'
|| prop === 'global'
|| prop === 'ignoreCase'
|| prop === 'multiline'
) {
return getType(object) === 'RegExp'
}

return false
}

export interface GlobalConstructors {
Expand Down Expand Up @@ -251,3 +254,18 @@ function collectOwnProperties(
Object.getOwnPropertyNames(obj).forEach(collect)
Object.getOwnPropertySymbols(obj).forEach(collect)
}

function collectFunctionProperties(prototype: any) {
const properties = new Set<string | symbol>()
collectOwnProperties(prototype, (prop) => {
const descriptor = Object.getOwnPropertyDescriptor(prototype, prop)
if (!descriptor || descriptor.get) {
return
}
const type = getType(descriptor.value)
if (type.includes('Function') && !isReadonlyProp(descriptor.value, prop)) {
properties.add(prop)
}
})
return Array.from(properties)
}
7 changes: 4 additions & 3 deletions packages/mocker/src/browser/mocker.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { CreateMockInstanceProcedure } from '../automocker'
import type { MockedModule, MockedModuleType } from '../registry'
import type { ModuleMockOptions } from '../types'
import type { ModuleMockerInterceptor } from './interceptor'
Expand All @@ -16,7 +17,7 @@ export class ModuleMocker {
constructor(
private interceptor: ModuleMockerInterceptor,
private rpc: ModuleMockerRPC,
private spyOn: (obj: any, method: string | symbol) => any,
private createMockInstance: CreateMockInstanceProcedure,
private config: ModuleMockerConfig,
) {}

Expand Down Expand Up @@ -117,7 +118,7 @@ export class ModuleMocker {

public mockObject(
object: Record<string | symbol, any>,
moduleType: MockedModuleType = 'automock',
moduleType: 'automock' | 'autospy' = 'automock',
): Record<string | symbol, any> {
return mockObject({
globalConstructors: {
Expand All @@ -127,7 +128,7 @@ export class ModuleMocker {
Map,
RegExp,
},
spyOn: this.spyOn,
createMockInstance: this.createMockInstance,
type: moduleType,
}, object)
}
Expand Down
Loading
Loading