From 716e2077b226dc4689225c25a2c74638c59eb3ab Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 30 Jul 2025 17:23:57 +0200 Subject: [PATCH 01/29] fix!: rewrite spying implementation to make module mocking more logical --- packages/mocker/src/automocker.ts | 115 +- packages/spy/src/index.ts | 1174 +++++++---------- packages/spy/src/types.ts | 449 +++++++ packages/vitest/src/integrations/vi.ts | 8 +- .../src/runtime/moduleRunner/moduleMocker.ts | 6 +- .../test/child-specific.child_process.test.ts | 4 +- test/core/test/mocking/vi-fn.test.ts | 635 +++++++++ test/core/test/mocking/vi-mockObject.test.ts | 156 +++ test/core/test/mocking/vi-spyOn.test.ts | 618 +++++++++ 9 files changed, 2415 insertions(+), 750 deletions(-) create mode 100644 packages/spy/src/types.ts create mode 100644 test/core/test/mocking/vi-fn.test.ts create mode 100644 test/core/test/mocking/vi-mockObject.test.ts create mode 100644 test/core/test/mocking/vi-spyOn.test.ts diff --git a/packages/mocker/src/automocker.ts b/packages/mocker/src/automocker.ts index 256dbb952310..e40fda594008 100644 --- a/packages/mocker/src/automocker.ts +++ b/packages/mocker/src/automocker.ts @@ -5,7 +5,12 @@ type Key = string | symbol export interface MockObjectOptions { type: MockedModuleType globalConstructors: GlobalConstructors - spyOn: (obj: any, prop: Key) => any + createMockInstance: (options?: { + prototypeMembers?: (string | symbol)[] + name?: string | symbol + originalImplementation?: (...args: any[]) => any + keepMembersImplementation?: boolean + }) => any } export function mockObject( @@ -49,7 +54,7 @@ export function mockObject( } // Skip special read-only props, we don't want to mess with those. - if (isSpecialProp(property, containerType)) { + if (isReadonlyProp(property, containerType)) { continue } @@ -90,58 +95,22 @@ 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 prototypeMembers = newContainer[property].prototype + ? collectFunctionProperties(newContainer[property].prototype) + : [] + const mock = createMockInstance({ + name: property, + prototypeMembers, + originalImplementation: options.type === 'autospy' ? newContainer[property] : undefined, + keepMembersImplementation: options.type === 'autospy', + }) + newContainer[property] = mock } refs.track(value, newContainer[property]) @@ -184,12 +153,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) { + 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 { @@ -251,3 +241,14 @@ function collectOwnProperties( Object.getOwnPropertyNames(obj).forEach(collect) Object.getOwnPropertySymbols(obj).forEach(collect) } + +function collectFunctionProperties(prototype: any) { + const properties = new Set() + collectOwnProperties(prototype, (key) => { + const type = getType(prototype[key]) + if (type.includes('Function') && !isReadonlyProp(prototype[key], type)) { + properties.add(key) + } + }) + return Array.from(properties) +} diff --git a/packages/spy/src/index.ts b/packages/spy/src/index.ts index 1a2d8edd54cb..f908051de7ea 100644 --- a/packages/spy/src/index.ts +++ b/packages/spy/src/index.ts @@ -1,466 +1,218 @@ -import type { SpyInternalImpl } from 'tinyspy' -import * as tinyspy from 'tinyspy' - -interface MockResultReturn { - type: 'return' - /** - * The value that was returned from the function. If function returned a Promise, then this will be a resolved value. - */ - value: T -} -interface MockResultIncomplete { - type: 'incomplete' - value: undefined -} -interface MockResultThrow { - type: 'throw' - /** - * An error that was thrown during function execution. - */ - value: any +import type { + Classes, + Constructable, + Methods, + Mock, + MockConfig, + MockContext, + MockFnContext, + MockResult, + MockReturnType, + MockSettledResult, + Procedure, + Properties, +} from './types' + +export function isMockFunction(fn: any): fn is Mock { + return ( + typeof fn === 'function' && '_isMockFunction' in fn && fn._isMockFunction === true + ) } -interface MockSettledResultFulfilled { - type: 'fulfilled' - value: T -} +const MOCK_RESTORE = new Set<() => void>() +// Jest keeps the state in a separate WeakMap which is good for memory, +// but it makes the state slower to access and return different values +// if you stored it before calling `mockClear` where it will be recreated +const REGISTERED_MOCKS = new Set() +const MOCK_CONFIGS = new WeakMap() + +export function createMockInstance( + { + originalImplementation, + restore, + mockImplementation, + prototypeMembers, + prototypeState, + prototypeConfig, + keepMembersImplementation, + name, + }: { + originalImplementation?: Procedure | Constructable + mockImplementation?: Procedure | Constructable + restore?: () => void + prototypeMembers?: (string | symbol)[] + keepMembersImplementation?: boolean + prototypeState?: MockContext + prototypeConfig?: MockConfig + name?: string | symbol + } = {}, +): Mock { + if (restore) { + MOCK_RESTORE.add(restore) + } -interface MockSettledResultRejected { - type: 'rejected' - value: any -} + const config = getDefaultConfig(originalImplementation) + const state = getDefaultState() -export type MockResult - = | MockResultReturn - | MockResultThrow - | MockResultIncomplete -export type MockSettledResult - = | MockSettledResultFulfilled - | MockSettledResultRejected - -type MockParameters = T extends Constructable - ? ConstructorParameters - : T extends Procedure - ? Parameters : never - -type MockReturnType = T extends Constructable - ? void - : T extends Procedure - ? ReturnType : never - -type MockFnContext = T extends Constructable - ? InstanceType - : ThisParameterType - -export interface MockContext { - /** - * This is an array containing all arguments for each call. One item of the array is the arguments of that call. - * - * @see https://vitest.dev/api/mock#mock-calls - * @example - * const fn = vi.fn() - * - * fn('arg1', 'arg2') - * fn('arg3') - * - * fn.mock.calls === [ - * ['arg1', 'arg2'], // first call - * ['arg3'], // second call - * ] - */ - calls: MockParameters[] - /** - * This is an array containing all instances that were instantiated when mock was called with a `new` keyword. Note that this is an actual context (`this`) of the function, not a return value. - * @see https://vitest.dev/api/mock#mock-instances - */ - instances: MockResultReturn[] - /** - * An array of `this` values that were used during each call to the mock function. - * @see https://vitest.dev/api/mock#mock-contexts - */ - contexts: MockFnContext[] - /** - * The order of mock's execution. This returns an array of numbers which are shared between all defined mocks. - * - * @see https://vitest.dev/api/mock#mock-invocationcallorder - * @example - * const fn1 = vi.fn() - * const fn2 = vi.fn() - * - * fn1() - * fn2() - * fn1() - * - * fn1.mock.invocationCallOrder === [1, 3] - * fn2.mock.invocationCallOrder === [2] - */ - invocationCallOrder: number[] - /** - * This is an array containing all values that were `returned` from the function. - * - * The `value` property contains the returned value or thrown error. If the function returned a `Promise`, then `result` will always be `'return'` even if the promise was rejected. - * - * @see https://vitest.dev/api/mock#mock-results - * @example - * const fn = vi.fn() - * .mockReturnValueOnce('result') - * .mockImplementationOnce(() => { throw new Error('thrown error') }) - * - * const result = fn() - * - * try { - * fn() - * } - * catch {} - * - * fn.mock.results === [ - * { - * type: 'return', - * value: 'result', - * }, - * { - * type: 'throw', - * value: Error, - * }, - * ] - */ - results: MockResult>[] - /** - * An array containing all values that were `resolved` or `rejected` from the function. - * - * This array will be empty if the function was never resolved or rejected. - * - * @see https://vitest.dev/api/mock#mock-settledresults - * @example - * const fn = vi.fn().mockResolvedValueOnce('result') - * - * const result = fn() - * - * fn.mock.settledResults === [] - * fn.mock.results === [ - * { - * type: 'return', - * value: Promise<'result'>, - * }, - * ] - * - * await result - * - * fn.mock.settledResults === [ - * { - * type: 'fulfilled', - * value: 'result', - * }, - * ] - */ - settledResults: MockSettledResult>>[] - /** - * This contains the arguments of the last call. If spy wasn't called, will return `undefined`. - * @see https://vitest.dev/api/mock#mock-lastcall - */ - lastCall: MockParameters | undefined - /** @internal */ - _state: (state?: InternalState) => InternalState -} + const mock = createMock( + { config, state, name, prototypeState, prototypeConfig, keepMembersImplementation }, + originalImplementation, + prototypeMembers, + ) + MOCK_CONFIGS.set(mock, config) + REGISTERED_MOCKS.add(mock) -interface InternalState { - implementation: Procedure | Constructable | undefined - onceImplementations: (Procedure | Constructable)[] - implementationChangedTemporarily: boolean -} + mock._isMockFunction = true + mock.getMockImplementation = () => { + return config.onceMockImplementations[0] || config.mockImplementation + } -type Procedure = (...args: any[]) => any -// pick a single function type from function overloads, unions, etc... -type NormalizedProcedure = T extends Constructable - ? ({ - new (...args: ConstructorParameters): InstanceType - }) - | ({ - (this: InstanceType, ...args: ConstructorParameters): void + Object.defineProperty(mock, 'mock', { + configurable: true, + enumerable: true, + get: () => state, + set: (newState: MockContext) => { + if (!newState || typeof newState !== 'object') { + return + } + + state.calls = newState.calls + state.contexts = newState.contexts + state.instances = newState.instances + state.invocationCallOrder = newState.invocationCallOrder + state.results = newState.results + state.settledResults = newState.settledResults + }, }) - : T extends Procedure - ? (...args: Parameters) => ReturnType - : never -type Methods = keyof { - [K in keyof T as T[K] extends Procedure ? K : never]: T[K]; -} -type Properties = { - [K in keyof T]: T[K] extends Procedure ? never : K; -}[keyof T] -& (string | symbol) -type Classes = { - [K in keyof T]: T[K] extends new (...args: any[]) => any ? K : never; -}[keyof T] -& (string | symbol) - -/* -cf. https://typescript-eslint.io/rules/method-signature-style/ - -Typescript assignability is different between - { foo: (f: T) => U } (this is "method-signature-style") -and - { foo(f: T): U } - -Jest uses the latter for `MockInstance.mockImplementation` etc... and it allows assignment such as: - const boolFn: Jest.Mock<() => boolean> = jest.fn<() => true>(() => true) -*/ -/* eslint-disable ts/method-signature-style */ -export interface MockInstance extends Disposable { - /** - * Use it to return the name assigned to the mock with the `.mockName(name)` method. By default, it will return `vi.fn()`. - * @see https://vitest.dev/api/mock#getmockname - */ - getMockName(): string - /** - * Sets the internal mock name. This is useful for identifying the mock when an assertion fails. - * @see https://vitest.dev/api/mock#mockname - */ - mockName(name: string): this - /** - * Current context of the mock. It stores information about all invocation calls, instances, and results. - */ - mock: MockContext - /** - * Clears all information about every call. After calling it, all properties on `.mock` will return to their initial state. This method does not reset implementations. It is useful for cleaning up mocks between different assertions. - * - * To automatically call this method before each test, enable the [`clearMocks`](https://vitest.dev/config/#clearmocks) setting in the configuration. - * @see https://vitest.dev/api/mock#mockclear - */ - mockClear(): this - /** - * Does what `mockClear` does and resets inner implementation to the original function. This also resets all "once" implementations. - * - * Note that resetting a mock from `vi.fn()` will set implementation to an empty function that returns `undefined`. - * Resetting a mock from `vi.fn(impl)` will set implementation to `impl`. It is useful for completely resetting a mock to its default state. - * - * To automatically call this method before each test, enable the [`mockReset`](https://vitest.dev/config/#mockreset) setting in the configuration. - * @see https://vitest.dev/api/mock#mockreset - */ - mockReset(): this - /** - * Does what `mockReset` does and restores original descriptors of spied-on objects. - * - * Note that restoring mock from `vi.fn()` will set implementation to an empty function that returns `undefined`. Restoring a `vi.fn(impl)` will restore implementation to `impl`. - * @see https://vitest.dev/api/mock#mockrestore - */ - mockRestore(): void - /** - * Returns current permanent mock implementation if there is one. - * - * If mock was created with `vi.fn`, it will consider passed down method as a mock implementation. - * - * If mock was created with `vi.spyOn`, it will return `undefined` unless a custom implementation was provided. - */ - getMockImplementation(): NormalizedProcedure | undefined - /** - * Accepts a function to be used as the mock implementation. TypeScript expects the arguments and return type to match those of the original function. - * @see https://vitest.dev/api/mock#mockimplementation - * @example - * const increment = vi.fn().mockImplementation(count => count + 1); - * expect(increment(3)).toBe(4); - */ - mockImplementation(fn: NormalizedProcedure): this - /** - * Accepts a function to be used as the mock implementation. TypeScript expects the arguments and return type to match those of the original function. This method can be chained to produce different results for multiple function calls. - * - * When the mocked function runs out of implementations, it will invoke the default implementation set with `vi.fn(() => defaultValue)` or `.mockImplementation(() => defaultValue)` if they were called. - * @see https://vitest.dev/api/mock#mockimplementationonce - * @example - * const fn = vi.fn(count => count).mockImplementationOnce(count => count + 1); - * expect(fn(3)).toBe(4); - * expect(fn(3)).toBe(3); - */ - mockImplementationOnce(fn: NormalizedProcedure): this - /** - * Overrides the original mock implementation temporarily while the callback is being executed. - * - * Note that this method takes precedence over the [`mockImplementationOnce`](https://vitest.dev/api/mock#mockimplementationonce). - * @see https://vitest.dev/api/mock#withimplementation - * @example - * const myMockFn = vi.fn(() => 'original') - * - * myMockFn.withImplementation(() => 'temp', () => { - * myMockFn() // 'temp' - * }) - * - * myMockFn() // 'original' - */ - withImplementation(fn: NormalizedProcedure, cb: () => T2): T2 extends Promise ? Promise : this - - /** - * Use this if you need to return the `this` context from the method without invoking the actual implementation. - * @see https://vitest.dev/api/mock#mockreturnthis - */ - mockReturnThis(): this - /** - * Accepts a value that will be returned whenever the mock function is called. TypeScript will only accept values that match the return type of the original function. - * @see https://vitest.dev/api/mock#mockreturnvalue - * @example - * const mock = vi.fn() - * mock.mockReturnValue(42) - * mock() // 42 - * mock.mockReturnValue(43) - * mock() // 43 - */ - mockReturnValue(value: MockReturnType): this - /** - * Accepts a value that will be returned whenever the mock function is called. TypeScript will only accept values that match the return type of the original function. - * - * When the mocked function runs out of implementations, it will invoke the default implementation set with `vi.fn(() => defaultValue)` or `.mockImplementation(() => defaultValue)` if they were called. - * @example - * const myMockFn = vi - * .fn() - * .mockReturnValue('default') - * .mockReturnValueOnce('first call') - * .mockReturnValueOnce('second call') - * - * // 'first call', 'second call', 'default' - * console.log(myMockFn(), myMockFn(), myMockFn()) - */ - mockReturnValueOnce(value: MockReturnType): this - /** - * Accepts a value that will be resolved when the async function is called. TypeScript will only accept values that match the return type of the original function. - * @example - * const asyncMock = vi.fn().mockResolvedValue(42) - * asyncMock() // Promise<42> - */ - mockResolvedValue(value: Awaited>): this - /** - * Accepts a value that will be resolved during the next function call. TypeScript will only accept values that match the return type of the original function. If chained, each consecutive call will resolve the specified value. - * @example - * const myMockFn = vi - * .fn() - * .mockResolvedValue('default') - * .mockResolvedValueOnce('first call') - * .mockResolvedValueOnce('second call') - * - * // Promise<'first call'>, Promise<'second call'>, Promise<'default'> - * console.log(myMockFn(), myMockFn(), myMockFn()) - */ - mockResolvedValueOnce(value: Awaited>): this - /** - * Accepts an error that will be rejected when async function is called. - * @example - * const asyncMock = vi.fn().mockRejectedValue(new Error('Async error')) - * await asyncMock() // throws Error<'Async error'> - */ - mockRejectedValue(error: unknown): this - /** - * Accepts a value that will be rejected during the next function call. If chained, each consecutive call will reject the specified value. - * @example - * const asyncMock = vi - * .fn() - * .mockResolvedValueOnce('first call') - * .mockRejectedValueOnce(new Error('Async error')) - * - * await asyncMock() // first call - * await asyncMock() // throws Error<'Async error'> - */ - mockRejectedValueOnce(error: unknown): this -} -/* eslint-enable ts/method-signature-style */ + mock.mockImplementation = function mockImplementation(implementation) { + config.mockImplementation = implementation + return mock + } -export interface Mock extends MockInstance { - new (...args: MockParameters): T extends Constructable ? InstanceType : MockReturnType - (...args: MockParameters): MockReturnType -} + mock.mockImplementationOnce = function mockImplementationOnce(implementation) { + config.onceMockImplementations.push(implementation) + return mock + } -type PartialMaybePromise = T extends Promise> - ? Promise>> - : Partial + mock.withImplementation = function withImplementation(implementation, callback) { + const previousImplementation = config.mockImplementation + const previousOnceImplementations = config.onceMockImplementations -export interface PartialMock - extends MockInstance< - (...args: Parameters) => PartialMaybePromise> - > { - new (...args: Parameters): ReturnType - (...args: Parameters): ReturnType -} + const reset = () => { + config.mockImplementation = previousImplementation + config.onceMockImplementations = previousOnceImplementations + } -export type MaybeMockedConstructor = T extends new ( - ...args: Array -) => infer R - ? Mock<(...args: ConstructorParameters) => R> - : T -export type MockedFunction = Mock & { - [K in keyof T]: T[K]; -} -export type PartiallyMockedFunction = PartialMock & { - [K in keyof T]: T[K]; -} -export type MockedFunctionDeep = Mock - & MockedObjectDeep -export type PartiallyMockedFunctionDeep = PartialMock - & MockedObjectDeep -export type MockedObject = MaybeMockedConstructor & { - [K in Methods]: T[K] extends Procedure ? MockedFunction : T[K]; -} & { [K in Properties]: T[K] } -export type MockedObjectDeep = MaybeMockedConstructor & { - [K in Methods]: T[K] extends Procedure ? MockedFunctionDeep : T[K]; -} & { [K in Properties]: MaybeMockedDeep } - -export type MaybeMockedDeep = T extends Procedure - ? MockedFunctionDeep - : T extends object - ? MockedObjectDeep - : T - -export type MaybePartiallyMockedDeep = T extends Procedure - ? PartiallyMockedFunctionDeep - : T extends object - ? MockedObjectDeep - : T - -export type MaybeMocked = T extends Procedure - ? MockedFunction - : T extends object - ? MockedObject - : T - -export type MaybePartiallyMocked = T extends Procedure - ? PartiallyMockedFunction - : T extends object - ? MockedObject - : T - -interface Constructable { - new (...args: any[]): any -} + config.mockImplementation = implementation + config.onceMockImplementations = [] + + const returnValue = callback() + + if (typeof returnValue === 'object' && typeof (returnValue as Promise)?.then === 'function') { + return (returnValue as Promise).then(() => { + reset() + return mock + }) as any + } + else { + reset() + } + return mock + } -export type MockedClass = MockInstance< - (...args: ConstructorParameters) => InstanceType -> & { - prototype: T extends { prototype: any } ? Mocked : never -} & T + mock.mockReturnThis = function mockReturnThis() { + return mock.mockImplementation(function (this: any) { + return this + }) + } -export type Mocked = { - [P in keyof T]: T[P] extends Procedure - ? MockInstance - : T[P] extends Constructable - ? MockedClass - : T[P]; -} & T + mock.mockReturnValue = function mockReturnValue(value) { + return mock.mockImplementation(() => value) + } -export const mocks: Set> = new Set() + mock.mockReturnValueOnce = function mockReturnValueOnce(value) { + return mock.mockImplementationOnce(() => value) + } -export function isMockFunction(fn: any): fn is MockInstance { - return ( - typeof fn === 'function' && '_isMockFunction' in fn && fn._isMockFunction - ) + mock.mockResolvedValue = function mockResolvedValue(value) { + return mock.mockImplementation(() => Promise.resolve(value)) + } + + mock.mockResolvedValueOnce = function mockResolvedValueOnce(value) { + return mock.mockImplementationOnce(() => Promise.resolve(value)) + } + + mock.mockRejectedValue = function mockRejectedValue(value) { + return mock.mockImplementation(() => Promise.reject(value)) + } + + mock.mockRejectedValueOnce = function mockRejectedValueOnce(value) { + return mock.mockImplementationOnce(() => Promise.reject(value)) + } + + mock.mockClear = function mockClear() { + state.calls = [] + state.contexts = [] + state.instances = [] + state.invocationCallOrder = [] + state.results = [] + state.settledResults = [] + return mock + } + + mock.mockReset = function mockReset() { + mock.mockClear() + config.mockImplementation = undefined + config.mockName = 'vi.fn()' + config.onceMockImplementations = [] + return mock + } + + mock.mockRestore = function mockRestore() { + mock.mockReset() + return restore?.() + } + + mock.mockName = function mockName(name: string) { + if (typeof name === 'string') { + config.mockName = name + } + return mock + } + + mock.getMockName = function getMockName() { + return config.mockName || 'vi.fn()' + } + + if (Symbol.dispose) { + mock[Symbol.dispose] = mock.mockRestore + } + + if (mockImplementation) { + mock.mockImplementation(mockImplementation as any) // TODO: typess + } + + return mock +} + +export function fn( + mockImplementation?: T, +): Mock { + return createMockInstance({ mockImplementation }) as Mock } -export function spyOn>>( +export function spyOn>>( obj: T, methodName: S, - accessType: 'get' + accessor: 'get' ): Mock<() => T[S]> -export function spyOn>>( +export function spyOn>>( obj: T, methodName: G, - accessType: 'set' + accessor: 'set' ): Mock<(arg: T[G]) => void> -export function spyOn> | Methods>>( +export function spyOn> | Methods>>( obj: T, methodName: M ): Required[M] extends { new (...args: infer A): infer R } @@ -468,303 +220,357 @@ export function spyOn> | Methods>>( : T[M] extends Procedure ? Mock : never -export function spyOn( - obj: T, - method: K, - accessType?: 'get' | 'set', +export function spyOn( + object: T, + key: K, + accessor?: 'get' | 'set', ): Mock { - const dictionary = { - get: 'getter', - set: 'setter', - } as const - const objMethod = accessType ? { [dictionary[accessType]]: method } : method + assert( + object != null, + 'The vi.spyOn() function could not find an object to spy upon. The first argument must be defined.', + ) - let state: InternalState | undefined + assert( + typeof object === 'object' || typeof object === 'function', + 'Vitest cannot spy on a primitive value.', + ) - const descriptor = getDescriptor(obj, method) - const fn = descriptor && descriptor[accessType || 'value'] + const [originalDescriptorObject, originalDescriptor] = getDescriptor(object, key) || [] + assert( + originalDescriptor || key in object, + `The property "${String(key)}" is not defined on the ${typeof object}.`, + ) + let accessType: 'get' | 'set' | 'value' = accessor || 'value' + let ssr = false + + // vite ssr support - actual function is stored inside a getter + if ( + accessType === 'value' + && originalDescriptor + && originalDescriptor.value == null + && originalDescriptor.get + ) { + accessType = 'get' + ssr = true + } - // inherit implementations if it was already mocked - if (isMockFunction(fn)) { - state = fn.mock._state() + let original: Procedure | undefined + + if (originalDescriptor) { + original = originalDescriptor[accessType] + } + else if (accessType !== 'value') { + original = () => object[key] + } + else { + original = object[key] as unknown as Procedure } - try { - const stub = tinyspy.internalSpyOn(obj, objMethod as any) + if (typeof original === 'function' && '_isMockFunction' in original && original._isMockFunction) { + return original as any as Mock + } - const spy = enhanceSpy(stub) as Mock + const reassign = (cb: any) => { + const { value, ...desc } = originalDescriptor || { + configurable: true, + writable: true, + } + if (accessType !== 'value') { + delete desc.writable // getter/setter can't have writable attribute at all + } + ;(desc as PropertyDescriptor)[accessType] = cb + Object.defineProperty(object, key, desc) + } - if (state) { - spy.mock._state(state) + const restore = () => { + // if method is defined on the prototype, we can just remove it from + // the current object instead of redefining a copy of it + if (originalDescriptorObject !== object) { + Reflect.deleteProperty(object, key) } + else if (originalDescriptor && !original) { + Object.defineProperty(object, key, originalDescriptor) + } + else { + reassign(original) + } + } - return spy + const mock = createMockInstance({ + restore, + originalImplementation: original, + }) + + try { + reassign( + ssr + ? () => mock + : mock, + ) } catch (error) { if ( error instanceof TypeError && Symbol.toStringTag - && (obj as any)[Symbol.toStringTag] === 'Module' + && (object as any)[Symbol.toStringTag] === 'Module' && (error.message.includes('Cannot redefine property') || error.message.includes('Cannot replace module namespace') || error.message.includes('can\'t redefine non-configurable property')) ) { throw new TypeError( - `Cannot spy on export "${String(objMethod)}". Module namespace is not configurable in ESM. See: https://vitest.dev/guide/browser/#limitations`, + `Cannot spy on export "${String(key)}". Module namespace is not configurable in ESM. See: https://vitest.dev/guide/browser/#limitations`, { cause: error }, ) } throw error } -} - -let callOrder = 0 -function enhanceSpy( - spy: SpyInternalImpl, MockReturnType>, -): MockInstance { - type TReturns = MockReturnType - - const stub = spy as unknown as MockInstance - - let implementation: NormalizedProcedure | undefined - - let onceImplementations: NormalizedProcedure[] = [] - let implementationChangedTemporarily = false - - let instances: any[] = [] - let contexts: any[] = [] - let invocations: number[] = [] - - const state = tinyspy.getInternalState(spy) + return mock +} - const mockContext: MockContext = { - get calls() { - return state.calls as MockParameters[] - }, - get contexts() { - return contexts - }, - get instances() { - return instances - }, - get invocationCallOrder() { - return invocations - }, - get results() { - return state.results.map(([callType, value]) => { - const type - = callType === 'error' ? ('throw' as const) : ('return' as const) - return { type, value } - }) - }, - get settledResults() { - return state.resolves.map(([callType, value]) => { - const type - = callType === 'error' ? ('rejected' as const) : ('fulfilled' as const) - return { type, value } - }) - }, - get lastCall() { - return state.calls[state.calls.length - 1] as MockParameters - }, - _state(state) { - if (state) { - implementation = state.implementation as NormalizedProcedure - onceImplementations = state.onceImplementations as NormalizedProcedure[] - implementationChangedTemporarily = state.implementationChangedTemporarily - } - return { - implementation, - onceImplementations, - implementationChangedTemporarily, - } - }, +function getDescriptor(obj: any, method: string | symbol | number): [any, PropertyDescriptor] | undefined { + const objDescriptor = Object.getOwnPropertyDescriptor(obj, method) + if (objDescriptor) { + return [obj, objDescriptor] } - - function mockCall(this: unknown, ...args: any) { - if (!new.target) { - instances.push(this) - contexts.push(this) - } - invocations.push(++callOrder) - const impl = implementationChangedTemporarily - ? implementation! - : onceImplementations.shift() - || implementation - || state.getOriginal() - || function () {} - if (new.target) { - try { - const result = Reflect.construct(impl, args, new.target) - instances.push(result) - contexts.push(result) - return result - } - catch (error) { - instances.push(undefined) - contexts.push(undefined) - - if (error instanceof TypeError && error.message.includes('is not a constructor')) { - throw new TypeError(`The spy implementation did not use 'function' or 'class', see https://vitest.dev/api/vi#vi-spyon for examples.`, { - cause: error, - }) - } - - throw error - } + let currentProto = Object.getPrototypeOf(obj) + while (currentProto !== null) { + const descriptor = Object.getOwnPropertyDescriptor(currentProto, method) + if (descriptor) { + return [currentProto, descriptor] } - return (impl as Procedure).apply(this, args) - } - - let name: string = (stub as any).name - - stub.getMockName = () => name || 'vi.fn()' - stub.mockName = (n) => { - name = n - return stub + currentProto = Object.getPrototypeOf(currentProto) } +} - stub.mockClear = () => { - state.reset() - instances = [] - contexts = [] - invocations = [] - return stub +function assert(condition: any, message: string): asserts condition { + if (!condition) { + throw new Error(message) } +} - stub.mockReset = () => { - stub.mockClear() - implementation = undefined - onceImplementations = [] - return stub - } +let invocationCallCounter = 1 - stub.mockRestore = () => { - stub.mockReset() - state.restore() - return stub - } +function addCalls(args: unknown[], state: MockContext, prototypeState?: MockContext) { + state.calls.push(args) + prototypeState?.calls.push(args) +} - if (Symbol.dispose) { - stub[Symbol.dispose] = () => stub.mockRestore() - } +function increaseInvocationOrder(order: number, state: MockContext, prototypeState?: MockContext) { + state.invocationCallOrder.push(order) + prototypeState?.invocationCallOrder.push(order) +} - stub.getMockImplementation = () => - implementationChangedTemporarily ? implementation : (onceImplementations.at(0) || implementation) - stub.mockImplementation = (fn: NormalizedProcedure) => { - implementation = fn - state.willCall(mockCall) - return stub - } +function addResult(result: MockResult, state: MockContext, prototypeState?: MockContext) { + state.results.push(result) + prototypeState?.results.push(result) +} - stub.mockImplementationOnce = (fn: NormalizedProcedure) => { - onceImplementations.push(fn) - return stub - } +function addSettledResult(result: MockSettledResult, state: MockContext, prototypeState?: MockContext) { + state.settledResults.push(result) + prototypeState?.settledResults.push(result) +} - function withImplementation(fn: NormalizedProcedure, cb: () => void): MockInstance - function withImplementation(fn: NormalizedProcedure, cb: () => Promise): Promise> - function withImplementation(fn: NormalizedProcedure, cb: () => void | Promise): MockInstance | Promise> { - const originalImplementation = implementation +function addInstance(instance: MockReturnType, state: MockContext, prototypeState?: MockContext) { + const instanceIndex = state.instances.push(instance) + const instancePrototypeIndex = prototypeState?.instances.push(instance) + return [instanceIndex, instancePrototypeIndex] as const +} - implementation = fn - state.willCall(mockCall) - implementationChangedTemporarily = true +function addContext(context: MockFnContext, state: MockContext, prototypeState?: MockContext) { + const contextIndex = state.contexts.push(context) + const contextPrototypeIndex = prototypeState?.contexts.push(context) + return [contextIndex, contextPrototypeIndex] as const +} - const reset = () => { - implementation = originalImplementation - implementationChangedTemporarily = false - } +function createMock( + { + state, + config, + name: mockName, + prototypeState, + prototypeConfig, + keepMembersImplementation, + }: { + prototypeState?: MockContext + prototypeConfig?: MockConfig + state: MockContext + config: MockConfig + name?: string | symbol + keepMembersImplementation?: boolean + }, + original?: Procedure | Constructable, + prototypeMethods: (string | symbol)[] = [], +) { + const name = (mockName || original?.name || 'Mock') as string + const namedObject: Record = { + // to keep the name of the function intact + [name]: (function (this: any, ...args: any[]) { + addCalls(args, state, prototypeState) + increaseInvocationOrder(invocationCallCounter++, state, prototypeState) + + const result = { + type: 'incomplete', + value: undefined, + } as MockResult + + const settledResult = { + type: 'incomplete', + value: undefined, + } as MockSettledResult + + addResult(result, state, prototypeState) + addSettledResult(settledResult, state, prototypeState) + + const [instanceIndex, instancePrototypeIndex] = addInstance(new.target ? undefined : this, state, prototypeState) + const [contextIndex, contextPrototypeIndex] = addContext(new.target ? undefined : this, state, prototypeState) + + const implementation: Procedure | Constructable = config.onceMockImplementations.shift() + || config.mockImplementation + || prototypeConfig?.onceMockImplementations.shift() + || prototypeConfig?.mockImplementation + || original + || function () {} - const result = cb() + let returnValue + let thrownValue + let didThrow = false - if (typeof result === 'object' && result && typeof result.then === 'function') { - return result.then(() => { - reset() - return stub - }) - } + try { + if (new.target) { + returnValue = Reflect.construct(implementation, args, new.target) + + for (const prop of prototypeMethods) { + const prototypeMock = returnValue[prop] + const isMock = isMockFunction(prototypeMock) + const prototypeState = isMock ? prototypeMock.mock : undefined + const prototypeConfig = isMock ? MOCK_CONFIGS.get(prototypeMock) : undefined + returnValue[prop] = createMockInstance({ + originalImplementation: keepMembersImplementation + ? prototypeConfig?.mockOriginal + : undefined, + prototypeState, + prototypeConfig, + keepMembersImplementation, + }) + } + } + else { + returnValue = (implementation as Procedure).call(this, args) + } + } + catch (error: any) { + thrownValue = error + didThrow = true + throw error + } + finally { + if (didThrow) { + result.type = 'throw' + result.value = thrownValue - reset() + settledResult.type = 'rejected' + settledResult.value = thrownValue + } + else { + result.type = 'return' + result.value = returnValue + + if (new.target) { + state.contexts[contextIndex - 1] = returnValue + state.instances[instanceIndex - 1] = returnValue + + if (contextPrototypeIndex != null && prototypeState) { + prototypeState.contexts[contextPrototypeIndex - 1] = returnValue + } + if (instancePrototypeIndex != null && prototypeState) { + prototypeState.instances[instancePrototypeIndex - 1] = returnValue + } + } + + if (returnValue instanceof Promise) { + returnValue.then( + (settledValue) => { + settledResult.type = 'fulfilled' + settledResult.value = settledValue + }, + (rejectedValue) => { + settledResult.type = 'rejected' + settledResult.value = rejectedValue + }, + ) + } + else { + settledResult.type = 'fulfilled' + settledResult.value = returnValue + } + } + } - return stub + return returnValue + }) as Mock, } + return namedObject[name] +} - stub.withImplementation = withImplementation - - stub.mockReturnThis = () => - stub.mockImplementation(function (this: MockFnContext) { - return this - } as NormalizedProcedure) - - stub.mockReturnValue = (val: TReturns) => stub.mockImplementation(function () { - return val - } as NormalizedProcedure) - stub.mockReturnValueOnce = (val: TReturns) => stub.mockImplementationOnce(function () { - return val - } as NormalizedProcedure) - - stub.mockResolvedValue = (val: Awaited) => - stub.mockImplementation(function () { - return Promise.resolve(val) - } as NormalizedProcedure) - - stub.mockResolvedValueOnce = (val: Awaited) => - stub.mockImplementationOnce(function () { - return Promise.resolve(val) - } as NormalizedProcedure) - - stub.mockRejectedValue = (val: unknown) => - stub.mockImplementation(function () { - return Promise.reject(val) - } as NormalizedProcedure) - - stub.mockRejectedValueOnce = (val: unknown) => - stub.mockImplementationOnce(function () { - return Promise.reject(val) - } as NormalizedProcedure) - - Object.defineProperty(stub, 'mock', { - get: () => mockContext, - }) - - state.willCall(mockCall) - - mocks.add(stub) +function getDefaultConfig(original?: Procedure | Constructable): MockConfig { + return { + mockImplementation: undefined, + mockOriginal: original, + mockName: 'vi.fn()', + onceMockImplementations: [], + } +} - return stub as any +function getDefaultState(): MockContext { + const state = { + calls: [], + contexts: [], + instances: [], + invocationCallOrder: [], + settledResults: [], + results: [], + get lastCall() { + return state.calls.at(-1) + }, + } + return state } -export function fn( - implementation?: T, -): Mock { - const inernalSpy = tinyspy.internalSpyOn({ - spy: implementation || function spy() {} as T, - }, 'spy') - const enhancedSpy = enhanceSpy(inernalSpy) - if (implementation) { - enhancedSpy.mockImplementation(implementation) +export function restoreAllMocks(): void { + for (const restore of MOCK_RESTORE) { + restore() } + MOCK_RESTORE.clear() +} - return enhancedSpy as any +export function clearAllMocks(): void { + REGISTERED_MOCKS.forEach(mock => mock.mockClear()) } -function getDescriptor( - obj: any, - method: string | symbol | number, -): PropertyDescriptor | undefined { - const objDescriptor = Object.getOwnPropertyDescriptor(obj, method) - if (objDescriptor) { - return objDescriptor - } - let currentProto = Object.getPrototypeOf(obj) - while (currentProto !== null) { - const descriptor = Object.getOwnPropertyDescriptor(currentProto, method) - if (descriptor) { - return descriptor - } - currentProto = Object.getPrototypeOf(currentProto) - } +export function resetAllMocks(): void { + REGISTERED_MOCKS.forEach(mock => mock.mockReset()) } + +export type { + MaybeMocked, + MaybeMockedConstructor, + MaybeMockedDeep, + MaybePartiallyMocked, + MaybePartiallyMockedDeep, + Mock, + MockContext, + Mocked, + MockedClass, + MockedFunction, + MockedFunctionDeep, + MockedObject, + MockedObjectDeep, + MockInstance, + MockResult, + MockSettledResult, + PartiallyMockedFunction, + PartiallyMockedFunctionDeep, + PartialMock, +} from './types' diff --git a/packages/spy/src/types.ts b/packages/spy/src/types.ts new file mode 100644 index 000000000000..b4ec1c91671b --- /dev/null +++ b/packages/spy/src/types.ts @@ -0,0 +1,449 @@ +interface MockResultReturn { + type: 'return' + /** + * The value that was returned from the function. If function returned a Promise, then this will be a resolved value. + */ + value: T +} +interface MockResultIncomplete { + type: 'incomplete' + value: undefined +} +interface MockResultThrow { + type: 'throw' + /** + * An error that was thrown during function execution. + */ + value: any +} + +interface MockSettledResultIncomplete { + type: 'incomplete' + value: undefined +} + +interface MockSettledResultFulfilled { + type: 'fulfilled' + value: T +} + +interface MockSettledResultRejected { + type: 'rejected' + value: any +} + +export type MockResult + = | MockResultReturn + | MockResultThrow + | MockResultIncomplete +export type MockSettledResult + = | MockSettledResultFulfilled + | MockSettledResultRejected + | MockSettledResultIncomplete + +export type MockParameters = T extends Constructable + ? ConstructorParameters + : T extends Procedure + ? Parameters : never + +export type MockReturnType = T extends Constructable + ? void + : T extends Procedure + ? ReturnType : never + +export type MockFnContext = T extends Constructable + ? InstanceType + : ThisParameterType + +export interface MockContext { + /** + * This is an array containing all arguments for each call. One item of the array is the arguments of that call. + * + * @see https://vitest.dev/api/mock#mock-calls + * @example + * const fn = vi.fn() + * + * fn('arg1', 'arg2') + * fn('arg3') + * + * fn.mock.calls === [ + * ['arg1', 'arg2'], // first call + * ['arg3'], // second call + * ] + */ + calls: MockParameters[] + /** + * This is an array containing all instances that were instantiated when mock was called with a `new` keyword. Note that this is an actual context (`this`) of the function, not a return value. + * @see https://vitest.dev/api/mock#mock-instances + */ + instances: MockReturnType[] + /** + * An array of `this` values that were used during each call to the mock function. + * @see https://vitest.dev/api/mock#mock-contexts + */ + contexts: MockFnContext[] + /** + * The order of mock's execution. This returns an array of numbers which are shared between all defined mocks. + * + * @see https://vitest.dev/api/mock#mock-invocationcallorder + * @example + * const fn1 = vi.fn() + * const fn2 = vi.fn() + * + * fn1() + * fn2() + * fn1() + * + * fn1.mock.invocationCallOrder === [1, 3] + * fn2.mock.invocationCallOrder === [2] + */ + invocationCallOrder: number[] + /** + * This is an array containing all values that were `returned` from the function. + * + * The `value` property contains the returned value or thrown error. If the function returned a `Promise`, then `result` will always be `'return'` even if the promise was rejected. + * + * @see https://vitest.dev/api/mock#mock-results + * @example + * const fn = vi.fn() + * .mockReturnValueOnce('result') + * .mockImplementationOnce(() => { throw new Error('thrown error') }) + * + * const result = fn() + * + * try { + * fn() + * } + * catch {} + * + * fn.mock.results === [ + * { + * type: 'return', + * value: 'result', + * }, + * { + * type: 'throw', + * value: Error, + * }, + * ] + */ + results: MockResult>[] + /** + * An array containing all values that were `resolved` or `rejected` from the function. + * + * This array will be empty if the function was never resolved or rejected. + * + * @see https://vitest.dev/api/mock#mock-settledresults + * @example + * const fn = vi.fn().mockResolvedValueOnce('result') + * + * const result = fn() + * + * fn.mock.settledResults === [] + * fn.mock.results === [ + * { + * type: 'return', + * value: Promise<'result'>, + * }, + * ] + * + * await result + * + * fn.mock.settledResults === [ + * { + * type: 'fulfilled', + * value: 'result', + * }, + * ] + */ + settledResults: MockSettledResult>>[] + /** + * This contains the arguments of the last call. If spy wasn't called, will return `undefined`. + * @see https://vitest.dev/api/mock#mock-lastcall + */ + lastCall: MockParameters | undefined +} + +export type Procedure = (...args: any[]) => any +// pick a single function type from function overloads, unions, etc... +export type NormalizedProcedure = T extends Constructable + ? ({ + new (...args: ConstructorParameters): InstanceType + }) + | ({ + (this: InstanceType, ...args: ConstructorParameters): void + }) + : T extends Procedure + ? (...args: Parameters) => ReturnType + : never + +export type Methods = keyof { + [K in keyof T as T[K] extends Procedure ? K : never]: T[K]; +} +export type Properties = { + [K in keyof T]: T[K] extends Procedure ? never : K; +}[keyof T] +& (string | symbol) +export type Classes = { + [K in keyof T]: T[K] extends new (...args: any[]) => any ? K : never; +}[keyof T] +& (string | symbol) + +/* +cf. https://typescript-eslint.io/rules/method-signature-style/ + +Typescript assignability is different between + { foo: (f: T) => U } (this is "method-signature-style") +and + { foo(f: T): U } + +Jest uses the latter for `MockInstance.mockImplementation` etc... and it allows assignment such as: + const boolFn: Jest.Mock<() => boolean> = jest.fn<() => true>(() => true) +*/ +/* eslint-disable ts/method-signature-style */ +export interface MockInstance extends Disposable { + /** + * Use it to return the name assigned to the mock with the `.mockName(name)` method. By default, it will return `vi.fn()`. + * @see https://vitest.dev/api/mock#getmockname + */ + getMockName(): string + /** + * Sets the internal mock name. This is useful for identifying the mock when an assertion fails. + * @see https://vitest.dev/api/mock#mockname + */ + mockName(name: string): this + /** + * Current context of the mock. It stores information about all invocation calls, instances, and results. + */ + mock: MockContext + /** + * Clears all information about every call. After calling it, all properties on `.mock` will return to their initial state. This method does not reset implementations. It is useful for cleaning up mocks between different assertions. + * + * To automatically call this method before each test, enable the [`clearMocks`](https://vitest.dev/config/#clearmocks) setting in the configuration. + * @see https://vitest.dev/api/mock#mockclear + */ + mockClear(): this + /** + * Does what `mockClear` does and resets inner implementation to the original function. This also resets all "once" implementations. + * + * Note that resetting a mock from `vi.fn()` will set implementation to an empty function that returns `undefined`. + * Resetting a mock from `vi.fn(impl)` will set implementation to `impl`. It is useful for completely resetting a mock to its default state. + * + * To automatically call this method before each test, enable the [`mockReset`](https://vitest.dev/config/#mockreset) setting in the configuration. + * @see https://vitest.dev/api/mock#mockreset + */ + mockReset(): this + /** + * Does what `mockReset` does and restores original descriptors of spied-on objects. + * + * Note that restoring mock from `vi.fn()` will set implementation to an empty function that returns `undefined`. Restoring a `vi.fn(impl)` will restore implementation to `impl`. + * @see https://vitest.dev/api/mock#mockrestore + */ + mockRestore(): void + /** + * Returns current permanent mock implementation if there is one. + * + * If mock was created with `vi.fn`, it will consider passed down method as a mock implementation. + * + * If mock was created with `vi.spyOn`, it will return `undefined` unless a custom implementation was provided. + */ + getMockImplementation(): NormalizedProcedure | undefined + /** + * Accepts a function to be used as the mock implementation. TypeScript expects the arguments and return type to match those of the original function. + * @see https://vitest.dev/api/mock#mockimplementation + * @example + * const increment = vi.fn().mockImplementation(count => count + 1); + * expect(increment(3)).toBe(4); + */ + mockImplementation(fn: NormalizedProcedure): this + /** + * Accepts a function to be used as the mock implementation. TypeScript expects the arguments and return type to match those of the original function. This method can be chained to produce different results for multiple function calls. + * + * When the mocked function runs out of implementations, it will invoke the default implementation set with `vi.fn(() => defaultValue)` or `.mockImplementation(() => defaultValue)` if they were called. + * @see https://vitest.dev/api/mock#mockimplementationonce + * @example + * const fn = vi.fn(count => count).mockImplementationOnce(count => count + 1); + * expect(fn(3)).toBe(4); + * expect(fn(3)).toBe(3); + */ + mockImplementationOnce(fn: NormalizedProcedure): this + /** + * Overrides the original mock implementation temporarily while the callback is being executed. + * + * Note that this method takes precedence over the [`mockImplementationOnce`](https://vitest.dev/api/mock#mockimplementationonce). + * @see https://vitest.dev/api/mock#withimplementation + * @example + * const myMockFn = vi.fn(() => 'original') + * + * myMockFn.withImplementation(() => 'temp', () => { + * myMockFn() // 'temp' + * }) + * + * myMockFn() // 'original' + */ + withImplementation(fn: NormalizedProcedure, cb: () => T2): T2 extends Promise ? Promise : this + + /** + * Use this if you need to return the `this` context from the method without invoking the actual implementation. + * @see https://vitest.dev/api/mock#mockreturnthis + */ + mockReturnThis(): this + /** + * Accepts a value that will be returned whenever the mock function is called. TypeScript will only accept values that match the return type of the original function. + * @see https://vitest.dev/api/mock#mockreturnvalue + * @example + * const mock = vi.fn() + * mock.mockReturnValue(42) + * mock() // 42 + * mock.mockReturnValue(43) + * mock() // 43 + */ + mockReturnValue(value: MockReturnType): this + /** + * Accepts a value that will be returned whenever the mock function is called. TypeScript will only accept values that match the return type of the original function. + * + * When the mocked function runs out of implementations, it will invoke the default implementation set with `vi.fn(() => defaultValue)` or `.mockImplementation(() => defaultValue)` if they were called. + * @example + * const myMockFn = vi + * .fn() + * .mockReturnValue('default') + * .mockReturnValueOnce('first call') + * .mockReturnValueOnce('second call') + * + * // 'first call', 'second call', 'default' + * console.log(myMockFn(), myMockFn(), myMockFn()) + */ + mockReturnValueOnce(value: MockReturnType): this + /** + * Accepts a value that will be resolved when the async function is called. TypeScript will only accept values that match the return type of the original function. + * @example + * const asyncMock = vi.fn().mockResolvedValue(42) + * asyncMock() // Promise<42> + */ + mockResolvedValue(value: Awaited>): this + /** + * Accepts a value that will be resolved during the next function call. TypeScript will only accept values that match the return type of the original function. If chained, each consecutive call will resolve the specified value. + * @example + * const myMockFn = vi + * .fn() + * .mockResolvedValue('default') + * .mockResolvedValueOnce('first call') + * .mockResolvedValueOnce('second call') + * + * // Promise<'first call'>, Promise<'second call'>, Promise<'default'> + * console.log(myMockFn(), myMockFn(), myMockFn()) + */ + mockResolvedValueOnce(value: Awaited>): this + /** + * Accepts an error that will be rejected when async function is called. + * @example + * const asyncMock = vi.fn().mockRejectedValue(new Error('Async error')) + * await asyncMock() // throws Error<'Async error'> + */ + mockRejectedValue(error: unknown): this + /** + * Accepts a value that will be rejected during the next function call. If chained, each consecutive call will reject the specified value. + * @example + * const asyncMock = vi + * .fn() + * .mockResolvedValueOnce('first call') + * .mockRejectedValueOnce(new Error('Async error')) + * + * await asyncMock() // first call + * await asyncMock() // throws Error<'Async error'> + */ + mockRejectedValueOnce(error: unknown): this +} +/* eslint-enable ts/method-signature-style */ + +export interface Mock extends MockInstance { + new (...args: MockParameters): T extends Constructable ? InstanceType : MockReturnType + (...args: MockParameters): MockReturnType + /** @internal */ + _isMockFunction: true + /** @internal */ + _protoImplementation?: Procedure | Constructable +} + +type PartialMaybePromise = T extends Promise> + ? Promise>> + : Partial + +export interface PartialMock + extends MockInstance< + (...args: Parameters) => PartialMaybePromise> + > { + new (...args: Parameters): ReturnType + (...args: Parameters): ReturnType +} + +export type MaybeMockedConstructor = T extends new ( + ...args: Array +) => infer R + ? Mock<(...args: ConstructorParameters) => R> + : T +export type MockedFunction = Mock & { + [K in keyof T]: T[K]; +} +export type PartiallyMockedFunction = PartialMock & { + [K in keyof T]: T[K]; +} +export type MockedFunctionDeep = Mock + & MockedObjectDeep +export type PartiallyMockedFunctionDeep = PartialMock + & MockedObjectDeep +export type MockedObject = MaybeMockedConstructor & { + [K in Methods]: T[K] extends Procedure ? MockedFunction : T[K]; +} & { [K in Properties]: T[K] } +export type MockedObjectDeep = MaybeMockedConstructor & { + [K in Methods]: T[K] extends Procedure ? MockedFunctionDeep : T[K]; +} & { [K in Properties]: MaybeMockedDeep } + +export type MaybeMockedDeep = T extends Procedure + ? MockedFunctionDeep + : T extends object + ? MockedObjectDeep + : T + +export type MaybePartiallyMockedDeep = T extends Procedure + ? PartiallyMockedFunctionDeep + : T extends object + ? MockedObjectDeep + : T + +export type MaybeMocked = T extends Procedure + ? MockedFunction + : T extends object + ? MockedObject + : T + +export type MaybePartiallyMocked = T extends Procedure + ? PartiallyMockedFunction + : T extends object + ? MockedObject + : T + +export interface Constructable { + new (...args: any[]): any +} + +export type MockedClass = MockInstance< + (...args: ConstructorParameters) => InstanceType +> & { + prototype: T extends { prototype: any } ? Mocked : never +} & T + +export type Mocked = { + [P in keyof T]: T[P] extends Procedure + ? MockInstance + : T[P] extends Constructable + ? MockedClass + : T[P]; +} & T + +export interface MockConfig { + mockImplementation: Procedure | undefined + mockOriginal: Procedure | Constructable | undefined + mockName: string + onceMockImplementations: Array +} diff --git a/packages/vitest/src/integrations/vi.ts b/packages/vitest/src/integrations/vi.ts index c8030ccd3a03..93511ac6ebe4 100644 --- a/packages/vitest/src/integrations/vi.ts +++ b/packages/vitest/src/integrations/vi.ts @@ -9,7 +9,7 @@ import type { import type { RuntimeOptions, SerializedConfig } from '../runtime/config' import type { VitestMocker } from '../runtime/moduleRunner/moduleMocker' import type { MockFactoryWithHelper, MockOptions } from '../types/mocker' -import { fn, isMockFunction, mocks, spyOn } from '@vitest/spy' +import { fn, isMockFunction, resetAllMocks, restoreAllMocks, clearAllMocks, spyOn } from '@vitest/spy' import { assertTypes, createSimpleStackTrace } from '@vitest/utils' import { getWorkerState, isChildProcess, resetModules, waitForImportsToResolve } from '../runtime/utils' import { parseSingleStack } from '../utils/source-map' @@ -656,17 +656,17 @@ function createVitest(): VitestUtils { }, clearAllMocks() { - [...mocks].reverse().forEach(spy => spy.mockClear()) + clearAllMocks() return utils }, resetAllMocks() { - [...mocks].reverse().forEach(spy => spy.mockReset()) + resetAllMocks() return utils }, restoreAllMocks() { - [...mocks].reverse().forEach(spy => spy.mockRestore()) + restoreAllMocks() return utils }, diff --git a/packages/vitest/src/runtime/moduleRunner/moduleMocker.ts b/packages/vitest/src/runtime/moduleRunner/moduleMocker.ts index f673f678e0ae..40e02b77b96a 100644 --- a/packages/vitest/src/runtime/moduleRunner/moduleMocker.ts +++ b/packages/vitest/src/runtime/moduleRunner/moduleMocker.ts @@ -278,8 +278,8 @@ export class VitestMocker { mockExports: Record = {}, behavior: MockedModuleType = 'automock', ): Record { - const spyOn = this.spyModule?.spyOn - if (!spyOn) { + const createMockInstance = this.spyModule?.createMockInstance + if (!createMockInstance) { throw this.createError( '[vitest] `spyModule` is not defined. This is a Vitest error. Please open a new issue with reproduction.', ) @@ -287,7 +287,7 @@ export class VitestMocker { return mockObject( { globalConstructors: this.primitives, - spyOn, + createMockInstance, type: behavior, }, object, diff --git a/test/core/test/child-specific.child_process.test.ts b/test/core/test/child-specific.child_process.test.ts index daec289c9ee1..38a18bc3819b 100644 --- a/test/core/test/child-specific.child_process.test.ts +++ b/test/core/test/child-specific.child_process.test.ts @@ -2,12 +2,12 @@ import { isMainThread, threadId } from 'node:worker_threads' import { expect, test } from 'vitest' test('has access to child_process API', ({ task, skip }) => { - skip(task.file.pool !== 'child_process', 'Run only in child_process pool') + skip(task.file.pool !== 'forks', 'Run only in child_process pool') expect(process.send).toBeDefined() }) test('doesn\'t have access to threads API', ({ task, skip }) => { - skip(task.file.pool !== 'child_process', 'Run only in child_process pool') + skip(task.file.pool !== 'forks', 'Run only in child_process pool') expect(isMainThread).toBe(true) expect(threadId).toBe(0) }) diff --git a/test/core/test/mocking/vi-fn.test.ts b/test/core/test/mocking/vi-fn.test.ts new file mode 100644 index 000000000000..cbae8fbea0b1 --- /dev/null +++ b/test/core/test/mocking/vi-fn.test.ts @@ -0,0 +1,635 @@ +import type { MockContext } from 'vitest' +import { describe, expect, test, vi } from 'vitest' + +test('vi.fn() returns undefined by default', () => { + const mock = vi.fn() + expect(mock()).toBe(undefined) +}) + +test('vi.fn() calls implementation if it was passed down', () => { + const mock = vi.fn(() => 3) + expect(mock()).toBe(3) +}) + +describe('vi.fn() state', () => { + // TODO: test when calls is not empty + test('vi.fn() clears calls without a custom implementation', () => { + const mock = vi.fn() + const state = mock.mock + + assertStateEmpty(state) + + mock() + + expect(state.calls).toEqual([[]]) + expect(state.results).toEqual([{ type: 'return', value: undefined }]) + expect(state.settledResults).toEqual([{ type: 'fulfilled', value: undefined }]) + expect(state.contexts).toEqual([undefined]) + expect(state.instances).toEqual([undefined]) + expect(state.lastCall).toEqual([]) + + mock.mockClear() + + assertStateEmpty(state) + + mock() + + expect(state.calls).toEqual([[]]) + expect(state.results).toEqual([{ type: 'return', value: undefined }]) + expect(state.settledResults).toEqual([{ type: 'fulfilled', value: undefined }]) + expect(state.contexts).toEqual([undefined]) + expect(state.instances).toEqual([undefined]) + expect(state.lastCall).toEqual([]) + + vi.clearAllMocks() + + assertStateEmpty(state) + }) + + test('vi.fn() clears calls with a custom sync function implementation', () => { + const mock = vi.fn(() => 42) + const state = mock.mock + + assertStateEmpty(state) + + mock() + + expect(state.calls).toEqual([[]]) + expect(state.results).toEqual([{ type: 'return', value: 42 }]) + expect(state.settledResults).toEqual([{ type: 'fulfilled', value: 42 }]) + expect(state.contexts).toEqual([undefined]) + expect(state.instances).toEqual([undefined]) + expect(state.lastCall).toEqual([]) + + mock.mockClear() + + assertStateEmpty(state) + + mock() + + expect(state.calls).toEqual([[]]) + expect(state.results).toEqual([{ type: 'return', value: 42 }]) + expect(state.settledResults).toEqual([{ type: 'fulfilled', value: 42 }]) + expect(state.contexts).toEqual([undefined]) + expect(state.instances).toEqual([undefined]) + expect(state.lastCall).toEqual([]) + + vi.clearAllMocks() + + assertStateEmpty(state) + }) + + test('vi.fn() clears calls with a custom sync function implementation with context', () => { + const mock = vi.fn(() => 42) + const state = mock.mock + + assertStateEmpty(state) + + mock.call('context') + + expect(state.calls).toEqual([[]]) + expect(state.results).toEqual([{ type: 'return', value: 42 }]) + expect(state.settledResults).toEqual([{ type: 'fulfilled', value: 42 }]) + expect(state.contexts).toEqual(['context']) + expect(state.instances).toEqual(['context']) + expect(state.lastCall).toEqual([]) + + mock.mockClear() + + assertStateEmpty(state) + + mock.call('context') + + expect(state.calls).toEqual([[]]) + expect(state.results).toEqual([{ type: 'return', value: 42 }]) + expect(state.settledResults).toEqual([{ type: 'fulfilled', value: 42 }]) + expect(state.contexts).toEqual(['context']) + expect(state.instances).toEqual(['context']) + expect(state.lastCall).toEqual([]) + + vi.clearAllMocks() + + assertStateEmpty(state) + }) + + test('vi.fn() clears calls with a custom sync prototype function implementation', () => { + const mock = vi.fn(function (this: any) { + this.value = 42 + return 'return-string' + }) + const state = mock.mock + + assertStateEmpty(state) + + mock.call({}) + + expect(state.calls).toEqual([[]]) + expect(state.results).toEqual([{ type: 'return', value: 'return-string' }]) + expect(state.settledResults).toEqual([{ type: 'fulfilled', value: 'return-string' }]) + expect(state.contexts).toEqual([{ value: 42 }]) + expect(state.instances).toEqual([{ value: 42 }]) + expect(state.lastCall).toEqual([]) + + mock.mockClear() + + assertStateEmpty(state) + + mock.call({}) + + expect(state.calls).toEqual([[]]) + expect(state.results).toEqual([{ type: 'return', value: 'return-string' }]) + expect(state.settledResults).toEqual([{ type: 'fulfilled', value: 'return-string' }]) + expect(state.contexts).toEqual([{ value: 42 }]) + expect(state.instances).toEqual([{ value: 42 }]) + expect(state.lastCall).toEqual([]) + + vi.clearAllMocks() + + assertStateEmpty(state) + }) + + test('vi.fn() clears calls with a custom sync class implementation', () => { + const Mock = vi.fn(class { + public value: number + constructor() { + this.value = 42 + } + }) + const state = Mock.mock + + assertStateEmpty(state) + + const mock1 = new Mock() + + expect(state.calls).toEqual([[]]) + expect(state.results).toEqual([{ type: 'return', value: expect.any(Mock) }]) + expect(state.results).toEqual([{ type: 'return', value: { value: 42 } }]) + expect(state.settledResults).toEqual([{ type: 'fulfilled', value: { value: 42 } }]) + expect(state.contexts).toEqual([mock1]) + expect(state.instances).toEqual([mock1]) + expect(state.lastCall).toEqual([]) + + Mock.mockClear() + + assertStateEmpty(state) + + const mock2 = new Mock() + + expect(state.calls).toEqual([[]]) + expect(state.results).toEqual([{ type: 'return', value: mock2 }]) + expect(state.settledResults).toEqual([{ type: 'fulfilled', value: mock2 }]) + expect(state.contexts).toEqual([mock2]) + expect(state.instances).toEqual([mock2]) + expect(state.lastCall).toEqual([]) + + vi.clearAllMocks() + + assertStateEmpty(state) + }) + + test('vi.fn() clears calls with a custom async function implementation', async () => { + const mock = vi.fn(() => Promise.resolve(42)) + const state = mock.mock + + assertStateEmpty(state) + + const promise = mock() + + expect(state.calls).toEqual([[]]) + expect(state.settledResults).toEqual([{ type: 'incomplete', value: undefined }]) + expect(state.results).toEqual([{ type: 'return', value: expect.any(Promise) }]) + expect(state.contexts).toEqual([undefined]) + expect(state.instances).toEqual([undefined]) + expect(state.lastCall).toEqual([]) + + await promise + + expect(state.settledResults).toEqual([{ type: 'fulfilled', value: 42 }]) + + mock.mockClear() + + assertStateEmpty(state) + + const promise2 = mock() + + expect(state.calls).toEqual([[]]) + expect(state.settledResults).toEqual([{ type: 'incomplete', value: undefined }]) + expect(state.results).toEqual([{ type: 'return', value: expect.any(Promise) }]) + expect(state.contexts).toEqual([undefined]) + expect(state.instances).toEqual([undefined]) + expect(state.lastCall).toEqual([]) + + await promise2 + + expect(state.settledResults).toEqual([{ type: 'fulfilled', value: 42 }]) + + vi.clearAllMocks() + + assertStateEmpty(state) + }) +}) + +describe('vi.fn() configuration', () => { + test('vi.fn() resets the original mock implementation', () => { + const mock = vi.fn(() => 42) + expect(mock()).toBe(42) + mock.mockReset() + expect(mock()).toBe(undefined) + }) + + test('vi.fn() resets the mock implementation', () => { + const mock = vi.fn().mockImplementation(() => 42) + expect(mock()).toBe(42) + mock.mockReset() + expect(mock()).toBe(undefined) + }) + + test('vi.fn() returns undefined as a mock implementation', () => { + const mock = vi.fn() + expect(mock.getMockImplementation()).toBe(undefined) + }) + + test('vi.fn() returns implementation if it was set', () => { + const implementation = () => 42 + const mock = vi.fn(implementation) + expect(mock.getMockImplementation()).toBe(implementation) + }) + + test('vi.fn() returns mockImplementation if it was set', () => { + const implementation = () => 42 + const mock = vi.fn().mockImplementation(implementation) + expect(mock.getMockImplementation()).toBe(implementation) + }) + + test('vi.fn() returns mockOnceImplementation if it was set', () => { + const implementation = () => 42 + const mock = vi.fn().mockImplementationOnce(implementation) + expect(mock.getMockImplementation()).toBe(implementation) + }) + + test('vi.fn() returns withImplementation if it was set', () => { + const implementation = () => 42 + const mock = vi.fn() + mock.withImplementation(implementation, () => { + expect(mock.getMockImplementation()).toBe(implementation) + }) + }) + + test('vi.fn() has a name', () => { + const mock = vi.fn() + expect(mock.getMockName()).toBe('vi.fn()') + mock.mockName('test') + expect(mock.getMockName()).toBe('test') + mock.mockReset() + expect(mock.getMockName()).toBe('vi.fn()') + mock.mockName('test') + expect(mock.getMockName()).toBe('test') + vi.resetAllMocks() + expect(mock.getMockName()).toBe('vi.fn()') + }) + + test('vi.fn() can reassign different implementations', () => { + const mock = vi.fn(() => 42) + expect(mock()).toBe(42) + mock.mockReturnValueOnce(100) + .mockReturnValueOnce(55) + expect(mock()).toBe(100) + expect(mock()).toBe(55) + expect(mock()).toBe(42) + mock.mockReturnValue(66) + expect(mock()).toBe(66) + }) +}) + +describe('vi.fn() restoration', () => { + test('vi.fn() resets the original implementation in mock.mockRestore()', () => { + const mock = vi.fn(() => 'hello') + expect(mock()).toBe('hello') + mock.mockRestore() + expect(mock()).toBe(undefined) + }) + + test('vi.fn() doesn\'t resets the added implementation in mock.mockRestore()', () => { + const mock = vi.fn().mockImplementation(() => 'hello') + expect(mock()).toBe('hello') + mock.mockRestore() + expect(mock()).toBe(undefined) + }) + + test('vi.fn() doesn\'t restore the original implementation in vi.restoreAllMocks()', () => { + const mock = vi.fn(() => 'hello') + expect(mock()).toBe('hello') + vi.restoreAllMocks() + expect(mock()).toBe('hello') + }) + + test('vi.fn() doesn\'t restore the added implementation in vi.restoreAllMocks()', () => { + const mock = vi.fn().mockImplementation(() => 'hello') + expect(mock()).toBe('hello') + vi.restoreAllMocks() + expect(mock()).toBe('hello') + }) +}) + +describe('vi.fn() implementations', () => { + test('vi.fn() can throw an error in original implementation', () => { + const mock = vi.fn(() => { + throw new Error('hello world') + }) + + expect(() => mock()).toThrowError('hello world') + expect(mock.mock.results).toEqual([ + { type: 'throw', value: new Error('hello world') }, + ]) + }) + + test('vi.fn() can throw an error in custom implementation', () => { + const mock = vi.fn().mockImplementation(() => { + throw new Error('hello world') + }) + + expect(() => mock()).toThrowError('hello world') + expect(mock.mock.results).toEqual([ + { type: 'throw', value: new Error('hello world') }, + ]) + }) + + test('vi.fn() with mockReturnThis on a function', () => { + const context = {} + const mock = vi.fn() + mock.mockReturnThis() + expect(mock.call(context)).toBe(context) + }) + + test('vi.fn() with mockReturnThis on a class', () => { + const Mock = vi.fn(class {}) + Mock.mockReturnThis() + const mock = new Mock() + expect(mock, 'has no effect on return value').toBeInstanceOf(Mock) + expect(Mock.mock.contexts).toEqual([mock]) + expect(Mock.mock.instances).toEqual([mock]) + }) + + test('vi.fn() with mockReturnValue', () => { + const mock = vi.fn() + mock.mockReturnValue(42) + expect(mock()).toBe(42) + expect(mock()).toBe(42) + expect(mock()).toBe(42) + mock.mockReset() + expect(mock()).toBe(undefined) + }) + + test('vi.fn() with mockReturnValue overriding original mock', () => { + const mock = vi.fn(() => 42) + mock.mockReturnValue(100) + expect(mock()).toBe(100) + expect(mock()).toBe(100) + expect(mock()).toBe(100) + mock.mockReset() + expect(mock()).toBe(undefined) + }) + + test('vi.fn() with mockReturnValue overriding another mock', () => { + const mock = vi.fn().mockImplementation(() => 42) + mock.mockReturnValue(100) + expect(mock()).toBe(100) + expect(mock()).toBe(100) + expect(mock()).toBe(100) + mock.mockReset() + expect(mock()).toBe(undefined) + }) + + test('vi.fn() with mockReturnValueOnce', () => { + const mock = vi.fn() + mock.mockReturnValueOnce(42) + expect(mock()).toBe(42) + expect(mock()).toBe(undefined) + expect(mock()).toBe(undefined) + mock.mockReturnValueOnce(42) + mock.mockReset() + expect(mock()).toBe(undefined) + }) + + test('vi.fn() with mockReturnValueOnce overriding original mock', () => { + const mock = vi.fn(() => 42) + mock.mockReturnValueOnce(100) + expect(mock()).toBe(100) + expect(mock()).toBe(42) + expect(mock()).toBe(42) + mock.mockReset() + expect(mock()).toBe(undefined) + }) + + test('vi.fn() with mockReturnValueOnce overriding another mock', () => { + const mock = vi.fn().mockImplementation(() => 42) + mock.mockReturnValueOnce(100) + expect(mock()).toBe(100) + expect(mock()).toBe(42) + expect(mock()).toBe(42) + mock.mockReset() + expect(mock()).toBe(undefined) + }) + + test('vi.fn() with mockResolvedValue', async () => { + const mock = vi.fn() + mock.mockResolvedValue(42) + await expect(mock()).resolves.toBe(42) + await expect(mock()).resolves.toBe(42) + await expect(mock()).resolves.toBe(42) + expect(mock.mock.settledResults).toEqual([ + { type: 'fulfilled', value: 42 }, + { type: 'fulfilled', value: 42 }, + { type: 'fulfilled', value: 42 }, + ]) + mock.mockReset() + expect(mock()).toBe(undefined) + }) + + test('vi.fn() with mockResolvedValue overriding original mock', async () => { + const mock = vi.fn(() => Promise.resolve(42)) + mock.mockResolvedValue(100) + await expect(mock()).resolves.toBe(100) + await expect(mock()).resolves.toBe(100) + await expect(mock()).resolves.toBe(100) + expect(mock.mock.settledResults).toEqual([ + { type: 'fulfilled', value: 100 }, + { type: 'fulfilled', value: 100 }, + { type: 'fulfilled', value: 100 }, + ]) + mock.mockReset() + expect(mock()).toBe(undefined) + }) + + test('vi.fn() with mockResolvedValue overriding another mock', async () => { + const mock = vi.fn().mockImplementation(() => 42) + mock.mockResolvedValue(100) + await expect(mock()).resolves.toBe(100) + await expect(mock()).resolves.toBe(100) + await expect(mock()).resolves.toBe(100) + expect(mock.mock.settledResults).toEqual([ + { type: 'fulfilled', value: 100 }, + { type: 'fulfilled', value: 100 }, + { type: 'fulfilled', value: 100 }, + ]) + mock.mockReset() + expect(mock()).toBe(undefined) + }) + + test('vi.fn() with mockResolvedValueOnce', async () => { + const mock = vi.fn() + mock.mockResolvedValueOnce(42) + await expect(mock()).resolves.toBe(42) + expect(mock()).toBe(undefined) + expect(mock()).toBe(undefined) + mock.mockResolvedValueOnce(42) + mock.mockReset() + expect(mock()).toBe(undefined) + }) + + test('vi.fn() with mockResolvedValueOnce overriding original mock', async () => { + const mock = vi.fn(() => Promise.resolve(42)) + mock.mockResolvedValueOnce(100) + await expect(mock()).resolves.toBe(100) + await expect(mock()).resolves.toBe(42) + await expect(mock()).resolves.toBe(42) + mock.mockReset() + expect(mock()).toBe(undefined) + }) + + test('vi.fn() with mockResolvedValueOnce overriding another mock', async () => { + const mock = vi.fn().mockImplementation(() => Promise.resolve(42)) + mock.mockResolvedValueOnce(100) + await expect(mock()).resolves.toBe(100) + await expect(mock()).resolves.toBe(42) + await expect(mock()).resolves.toBe(42) + mock.mockReset() + expect(mock()).toBe(undefined) + }) + + test('vi.fn() with mockRejectedValue', async () => { + const mock = vi.fn() + mock.mockRejectedValue(42) + await expect(mock()).rejects.toBe(42) + await expect(mock()).rejects.toBe(42) + await expect(mock()).rejects.toBe(42) + expect(mock.mock.settledResults).toEqual([ + { type: 'rejected', value: 42 }, + { type: 'rejected', value: 42 }, + { type: 'rejected', value: 42 }, + ]) + mock.mockReset() + expect(mock()).toBe(undefined) + }) + + test('vi.fn() with mockRejectedValue overriding original mock', async () => { + const mock = vi.fn(() => Promise.resolve(42)) + mock.mockRejectedValue(100) + await expect(mock()).rejects.toBe(100) + await expect(mock()).rejects.toBe(100) + await expect(mock()).rejects.toBe(100) + expect(mock.mock.settledResults).toEqual([ + { type: 'rejected', value: 100 }, + { type: 'rejected', value: 100 }, + { type: 'rejected', value: 100 }, + ]) + mock.mockReset() + expect(mock()).toBe(undefined) + }) + + test('vi.fn() with mockRejectedValue overriding another mock', async () => { + const mock = vi.fn().mockImplementation(() => Promise.resolve(42)) + mock.mockRejectedValue(100) + await expect(mock()).rejects.toBe(100) + await expect(mock()).rejects.toBe(100) + await expect(mock()).rejects.toBe(100) + expect(mock.mock.settledResults).toEqual([ + { type: 'rejected', value: 100 }, + { type: 'rejected', value: 100 }, + { type: 'rejected', value: 100 }, + ]) + mock.mockReset() + expect(mock()).toBe(undefined) + }) + + test('vi.fn() with mockRejectedValueOnce', async () => { + const mock = vi.fn() + mock.mockRejectedValueOnce(42) + await expect(mock()).rejects.toBe(42) + expect(mock()).toBe(undefined) + expect(mock()).toBe(undefined) + mock.mockRejectedValueOnce(42) + mock.mockReset() + expect(mock()).toBe(undefined) + }) + + test('vi.fn() with mockRejectedValueOnce overriding original mock', async () => { + const mock = vi.fn(() => Promise.resolve(42)) + mock.mockRejectedValueOnce(100) + await expect(mock()).rejects.toBe(100) + await expect(mock()).resolves.toBe(42) + await expect(mock()).resolves.toBe(42) + mock.mockReset() + expect(mock()).toBe(undefined) + }) + + test('vi.fn() with mockRejectedValueOnce overriding another mock', async () => { + const mock = vi.fn().mockImplementation(() => Promise.resolve(42)) + mock.mockRejectedValueOnce(100) + await expect(mock()).rejects.toBe(100) + await expect(mock()).resolves.toBe(42) + await expect(mock()).resolves.toBe(42) + mock.mockReset() + expect(mock()).toBe(undefined) + }) + + test('vi.fn() throws an error if new is called on arrow function', () => { + const Mock = vi.fn(() => {}) + expect(() => new Mock()).toThrowError() + }) + + test('vi.fn() throws an error if new is not called on a class', () => { + const Mock = vi.fn(class _Mock {}) + expect(() => Mock()).toThrowError( + `Class constructor _Mock cannot be invoked without 'new'`, + ) + }) + + test('vi.fn() respects new target in a function', () => { + let target!: unknown + let callArgs!: unknown[] + const Mock = vi.fn(function (this: any, ...args: unknown[]) { + target = new.target + callArgs = args + }) + const _example = new Mock('test', 42) + expect(target).toBeTypeOf('function') + expect(callArgs).toEqual(['test', 42]) + expect(Mock.mock.calls).toEqual([['test', 42]]) + }) + + test('vi.fn() respects new target in a class', () => { + let target!: unknown + let callArgs!: unknown[] + const Mock = vi.fn(class { + constructor(...args: any[]) { + target = new.target + callArgs = args + } + }) + const _example = new Mock('test', 42) + expect(target).toBeTypeOf('function') + expect(callArgs).toEqual(['test', 42]) + expect(Mock.mock.calls).toEqual([['test', 42]]) + }) +}) + +function assertStateEmpty(state: MockContext) { + expect(state.calls).toHaveLength(0) + expect(state.results).toHaveLength(0) + expect(state.settledResults).toHaveLength(0) + expect(state.contexts).toHaveLength(0) + expect(state.instances).toHaveLength(0) + expect(state.lastCall).toBe(undefined) + expect(state.invocationCallOrder).toEqual([]) +} diff --git a/test/core/test/mocking/vi-mockObject.test.ts b/test/core/test/mocking/vi-mockObject.test.ts new file mode 100644 index 000000000000..540fc2b50631 --- /dev/null +++ b/test/core/test/mocking/vi-mockObject.test.ts @@ -0,0 +1,156 @@ +import { expect, test, vi } from 'vitest' + +test('vi.mockObject() mocks methods', () => { + const mocked = mockModule() + + expect(mocked.method.name).toBe('method') + expect(mocked.Class.name).toBe('Class') + expect(mocked.method()).toBe(undefined) + expect(new mocked.Class()).toBeInstanceOf(mocked.Class) +}) + +test('when properties are spied, they keep the implementation', () => { + const module = mockModule('autospy') + expect(module.method()).toBe(42) + expect(module.method).toHaveBeenCalled() + + const instance = new module.Class() + expect(instance.method()).toBe(42) + expect(instance.method).toHaveBeenCalled() + expect(module.Class.prototype.method).toHaveBeenCalledTimes(1) +}) + +test('vi.restoreAllMocks() does not affect mocks', () => { + const mocked = mockModule() + + vi.restoreAllMocks() + + expect(mocked.method()).toBe(undefined) + expect(new mocked.Class()).toBeInstanceOf(mocked.Class) +}) + +test('vi.mockRestore() does not affect mocks', () => { + const mocked = mockModule() + + vi.mocked(mocked.method).mockRestore() + + expect(mocked.method()).toBe(undefined) + expect(new mocked.Class()).toBeInstanceOf(mocked.Class) +}) + +test('vi.mockRestore() on respied method does not restore it to the original', async ({ annotate }) => { + await annotate('https://github.com/vitest-dev/vitest/issues/8319', 'issue') + + const mocked = mockModule() + const spy = vi.spyOn(mocked, 'method') + + expect(mocked.method()).toBe(undefined) + + spy.mockRestore() + + expect(mocked.method()).toBe(undefined) +}) + +test('instance mocks are independently tracked, but prototype shares the state', () => { + const { Class } = mockModule() + const t1 = new Class() + const t2 = new Class() + t1.method() + expect(t1.method).toHaveBeenCalledTimes(1) + t2.method() + expect(t1.method).toHaveBeenCalledTimes(1) + expect(t2.method).toHaveBeenCalledTimes(1) + expect(Class.prototype.method).toHaveBeenCalledTimes(2) + + vi.resetAllMocks() + t1.method() + expect(t1.method).toHaveBeenCalledTimes(1) + t2.method() + expect(t1.method).toHaveBeenCalledTimes(1) + expect(t2.method).toHaveBeenCalledTimes(1) + expect(Class.prototype.method).toHaveBeenCalledTimes(2) + + vi.mocked(t1.method).mockReturnValue(100) + t1.method() + expect(t1.method).toHaveBeenCalledTimes(2) + // tracks methods even when t1.method implementation is overriden + expect(Class.prototype.method).toHaveBeenCalledTimes(3) +}) + +test('instance methods inherit the implementation, but can override the local ones', () => { + const { Class } = mockModule() + const t1 = vi.mocked(new Class()) + const t2 = vi.mocked(new Class()) + + t1.method.mockReturnValue(100) + expect(t1.method()).toBe(100) + expect(t1.method).toHaveBeenCalled() + expect(t2.method).not.toHaveBeenCalled() + + expect(Class.prototype.method).toHaveBeenCalledTimes(1) + + Class.prototype.method.mockReturnValue(200) + expect(t1.method()).toBe(100) + expect(t2.method()).toBe(200) + + expect(Class.prototype.method).toHaveBeenCalledTimes(3) + + vi.resetAllMocks() + + expect(t1.method()).toBe(undefined) + expect(t2.method()).toBe(undefined) + + Class.prototype.method.mockReturnValue(300) + + expect(t1.method()).toBe(300) + expect(t2.method()).toBe(300) +}) + +test('vi.mockReset() does not break inherited properties', () => { + const { Class } = mockModule() + const instance1 = new Class() + + expect(instance1.method()).toBe(undefined) + + expect(instance1.method).toHaveBeenCalledTimes(1) + expect(Class.prototype.method).toHaveBeenCalledTimes(1) + + vi.mocked(instance1.method).mockReturnValue(100) + + expect(instance1.method()).toBe(100) + + expect(instance1.method).toHaveBeenCalledTimes(2) + expect(Class.prototype.method).toHaveBeenCalledTimes(2) + + vi.resetAllMocks() + + expect(instance1.method()).toBe(undefined) + + expect(instance1.method).toHaveBeenCalledTimes(1) + expect(Class.prototype.method).toHaveBeenCalledTimes(1) + + const instance2 = new Class() + const instance3 = new Class() + + instance2.method() + + expect(instance2.method).not.toBe(instance3.method) + + expect(instance2.method).toHaveBeenCalledTimes(1) + expect(instance3.method).toHaveBeenCalledTimes(0) +}) + +function mockModule(type: 'automock' | 'autospy' = 'automock') { + return vi.mockObject({ + [Symbol.toStringTag]: 'Module', + __esModule: true, + method() { + return 42 + }, + Class: class { + method() { + return 42 + } + }, + }, { spy: type === 'autospy' }) +} diff --git a/test/core/test/mocking/vi-spyOn.test.ts b/test/core/test/mocking/vi-spyOn.test.ts new file mode 100644 index 000000000000..c9fdf6ebf775 --- /dev/null +++ b/test/core/test/mocking/vi-spyOn.test.ts @@ -0,0 +1,618 @@ +import type { MockContext } from 'vitest' +import { describe, expect, test, vi } from 'vitest' + +describe('vi.spyOn() state', () => { + test('vi.spyOn() spies on an object and tracks the calls', () => { + const object = createObject() + const mock = vi.spyOn(object, 'method') + + expect(object.method).toBe(mock) + expect(vi.isMockFunction(object.method)).toBe(true) + + const state = mock.mock + + assertStateEmpty(state) + + object.method() + expect(state.calls).toEqual([[]]) + expect(state.results).toEqual([{ type: 'return', value: 42 }]) + expect(state.settledResults).toEqual([{ type: 'fulfilled', value: 42 }]) + expect(state.instances).toEqual([object]) + expect(state.contexts).toEqual([object]) + expect(state.lastCall).toEqual([]) + expect(state.invocationCallOrder).toEqual([expect.any(Number)]) + + mock.mockClear() + assertStateEmpty(state) + + object.method() + expect(state.calls).toEqual([[]]) + expect(state.results).toEqual([{ type: 'return', value: 42 }]) + expect(state.settledResults).toEqual([{ type: 'fulfilled', value: 42 }]) + expect(state.instances).toEqual([object]) + expect(state.contexts).toEqual([object]) + expect(state.lastCall).toEqual([]) + expect(state.invocationCallOrder).toEqual([expect.any(Number)]) + + vi.clearAllMocks() + assertStateEmpty(state) + }) + + test('vi.spyOn() spies and tracks overriden sync calls', () => { + const object = createObject() + const mock = vi.spyOn(object, 'method') + mock.mockImplementation(() => 100) + const state = mock.mock + + assertStateEmpty(state) + + object.method() + expect(state.calls).toEqual([[]]) + expect(state.results).toEqual([{ type: 'return', value: 100 }]) + expect(state.settledResults).toEqual([{ type: 'fulfilled', value: 100 }]) + expect(state.instances).toEqual([object]) + expect(state.contexts).toEqual([object]) + expect(state.lastCall).toEqual([]) + expect(state.invocationCallOrder).toEqual([expect.any(Number)]) + + mock.mockClear() + assertStateEmpty(state) + + object.method() + expect(state.calls).toEqual([[]]) + expect(state.results).toEqual([{ type: 'return', value: 100 }]) + expect(state.settledResults).toEqual([{ type: 'fulfilled', value: 100 }]) + expect(state.instances).toEqual([object]) + expect(state.contexts).toEqual([object]) + expect(state.lastCall).toEqual([]) + expect(state.invocationCallOrder).toEqual([expect.any(Number)]) + + vi.clearAllMocks() + assertStateEmpty(state) + }) + + test('vi.spyOn() spies and tracks overriden sync calls with context', () => { + const object = createObject() + const mock = vi.spyOn(object, 'method') + mock.mockImplementation(() => 100) + const state = mock.mock + const context = {} + + assertStateEmpty(state) + + object.method.call(context) + expect(state.calls).toEqual([[]]) + expect(state.results).toEqual([{ type: 'return', value: 100 }]) + expect(state.settledResults).toEqual([{ type: 'fulfilled', value: 100 }]) + expect(state.instances).toEqual([context]) + expect(state.contexts).toEqual([context]) + expect(state.lastCall).toEqual([]) + expect(state.invocationCallOrder).toEqual([expect.any(Number)]) + + mock.mockClear() + assertStateEmpty(state) + + object.method.call(context) + expect(state.calls).toEqual([[]]) + expect(state.results).toEqual([{ type: 'return', value: 100 }]) + expect(state.settledResults).toEqual([{ type: 'fulfilled', value: 100 }]) + expect(state.instances).toEqual([context]) + expect(state.contexts).toEqual([context]) + expect(state.lastCall).toEqual([]) + expect(state.invocationCallOrder).toEqual([expect.any(Number)]) + + vi.clearAllMocks() + assertStateEmpty(state) + }) + + test('vi.spyOn() spies and tracks overriden sync prototype calls with context', () => { + const object = createObject() + const mock = vi.spyOn(object, 'method') + mock.mockImplementation(function (this: any) { + this.value = 42 + return 100 + }) + const state = mock.mock + const context = {} + + assertStateEmpty(state) + + object.method.call(context) + expect(state.calls).toEqual([[]]) + expect(state.results).toEqual([{ type: 'return', value: 100 }]) + expect(state.settledResults).toEqual([{ type: 'fulfilled', value: 100 }]) + expect(state.instances).toEqual([{ value: 42 }]) + expect(state.contexts).toEqual([{ value: 42 }]) + expect(state.lastCall).toEqual([]) + expect(state.invocationCallOrder).toEqual([expect.any(Number)]) + + mock.mockClear() + assertStateEmpty(state) + + object.method.call(context) + expect(state.calls).toEqual([[]]) + expect(state.results).toEqual([{ type: 'return', value: 100 }]) + expect(state.settledResults).toEqual([{ type: 'fulfilled', value: 100 }]) + expect(state.instances).toEqual([{ value: 42 }]) + expect(state.contexts).toEqual([{ value: 42 }]) + expect(state.lastCall).toEqual([]) + expect(state.invocationCallOrder).toEqual([expect.any(Number)]) + + vi.clearAllMocks() + assertStateEmpty(state) + }) + + test('vi.spyOn() spies and tracks overriden sync class calls with context', () => { + const object = createObject() + const mock = vi.spyOn(object, 'Class') + mock.mockImplementation(class { + public value: number + constructor() { + this.value = 42 + } + }) + const state = mock.mock + + assertStateEmpty(state) + + const instance1 = new object.Class() + expect(state.calls).toEqual([[]]) + expect(state.results).toEqual([{ type: 'return', value: instance1 }]) + expect(state.settledResults).toEqual([{ type: 'fulfilled', value: instance1 }]) + expect(state.instances).toEqual([instance1]) + expect(state.contexts).toEqual([instance1]) + expect(state.lastCall).toEqual([]) + expect(state.invocationCallOrder).toEqual([expect.any(Number)]) + + mock.mockClear() + assertStateEmpty(state) + + const instance2 = new object.Class() + expect(state.calls).toEqual([[]]) + expect(state.results).toEqual([{ type: 'return', value: instance2 }]) + expect(state.settledResults).toEqual([{ type: 'fulfilled', value: instance2 }]) + expect(state.instances).toEqual([instance2]) + expect(state.contexts).toEqual([instance2]) + expect(state.lastCall).toEqual([]) + expect(state.invocationCallOrder).toEqual([expect.any(Number)]) + + vi.clearAllMocks() + assertStateEmpty(state) + }) + + test('vi.spyOn() spies and tracks overriden async calls', async () => { + const object = createObject() + const mock = vi.spyOn(object, 'async') + mock.mockImplementation(() => Promise.resolve(100)) + const state = mock.mock + + assertStateEmpty(state) + + const promise1 = object.async() + expect(state.calls).toEqual([[]]) + expect(state.results).toEqual([{ type: 'return', value: expect.any(Promise) }]) + expect(state.settledResults).toEqual([{ type: 'incomplete', value: undefined }]) + expect(state.instances).toEqual([object]) + expect(state.contexts).toEqual([object]) + expect(state.lastCall).toEqual([]) + expect(state.invocationCallOrder).toEqual([expect.any(Number)]) + + await promise1 + expect(state.settledResults).toEqual([{ type: 'fulfilled', value: 100 }]) + + mock.mockClear() + assertStateEmpty(state) + + const promise2 = object.async() + expect(state.calls).toEqual([[]]) + expect(state.results).toEqual([{ type: 'return', value: expect.any(Promise) }]) + expect(state.settledResults).toEqual([{ type: 'incomplete', value: undefined }]) + expect(state.instances).toEqual([object]) + expect(state.contexts).toEqual([object]) + expect(state.lastCall).toEqual([]) + expect(state.invocationCallOrder).toEqual([expect.any(Number)]) + + await promise2 + expect(state.settledResults).toEqual([{ type: 'fulfilled', value: 100 }]) + + vi.clearAllMocks() + assertStateEmpty(state) + }) + + test('vi.spyOn() doesn\'t loose context', () => { + const instances: any[] = [] + const Names = function Names(this: any) { + instances.push(this) + this.array = [1] + } as { + (): void + new (): typeof obj + } + const obj = { + array: [], + Names, + } + + vi.spyOn(obj, 'Names') + + const s = new obj.Names() + + expect(obj.array).toEqual([]) + expect(s.array).toEqual([1]) + expect(instances[0]).toEqual({ array: [1] }) + + obj.Names() + + expect(obj.array).toEqual([1]) + }) +}) + +describe('vi.spyOn() settings', () => { + test('vi.spyOn() when spying on a method spy returns the same spy', () => { + const object = createObject() + const spy1 = vi.spyOn(object, 'method') + const spy2 = vi.spyOn(object, 'method') + expect(spy1).toBe(spy2) + + object.method() + expect(spy2.mock.calls).toEqual(spy2.mock.calls) + }) + + test('vi.spyOn() when spying on a getter spy returns the same spy', () => { + const object = createObject() + const spy1 = vi.spyOn(object, 'getter', 'get') + const spy2 = vi.spyOn(object, 'getter', 'get') + expect(spy1).toBe(spy2) + + object.method() + expect(spy2.mock.calls).toEqual(spy2.mock.calls) + }) + + test('vi.spyOn() when spying on a setter spy returns the same spy', () => { + const object = createObject() + const spy1 = vi.spyOn(object, 'getter', 'set') + const spy2 = vi.spyOn(object, 'getter', 'set') + expect(spy1).toBe(spy2) + + object.method() + expect(spy2.mock.calls).toEqual(spy2.mock.calls) + }) + + test('vi.spyOn() can spy on multiple class instances without intervention', () => { + class Example { + method() { + return 42 + } + } + + const example1 = new Example() + const example2 = new Example() + + const mock1 = vi.spyOn(example1, 'method') + const mock2 = vi.spyOn(example2, 'method') + + example1.method() + expect(mock1.mock.calls).toHaveLength(1) + expect(mock2.mock.calls).toHaveLength(0) + + example1.method() + expect(mock1.mock.calls).toHaveLength(2) + expect(mock2.mock.calls).toHaveLength(0) + + example2.method() + expect(mock1.mock.calls).toHaveLength(2) + expect(mock2.mock.calls).toHaveLength(1) + }) + + test('vi.spyOn() can spy on a prototype', () => { + class Example { + method() { + return 42 + } + } + + const example = new Example() + const spy = vi.spyOn(example, 'method') + expect(example.method()).toBe(42) + expect(spy.mock.calls).toEqual([[]]) + expect(vi.isMockFunction(Example.prototype.method)).toBe(false) + }) + + test('vi.spyOn() can spy on inherited methods', () => { + class Bar { + _bar = 'bar' + get bar(): string { + return this._bar + } + + set bar(bar: string) { + this._bar = bar + } + } + class Foo extends Bar {} + const foo = new Foo() + vi.spyOn(foo, 'bar', 'get').mockImplementation(() => 'foo') + expect(foo.bar).toEqual('foo') + // foo.bar setter is inherited from Bar, so we can set it + expect(() => { + foo.bar = 'baz' + }).not.toThrowError() + expect(foo.bar).toEqual('foo') + }) + + test('vi.spyOn() inherits overriden methods', () => { + class Bar { + _bar = 'bar' + get bar(): string { + return this._bar + } + + set bar(bar: string) { + this._bar = bar + } + } + class Foo extends Bar { + get bar(): string { + return `${super.bar}-foo` + } + } + const foo = new Foo() + expect(foo.bar).toEqual('bar-foo') + vi.spyOn(foo, 'bar', 'get').mockImplementation(() => 'foo') + expect(foo.bar).toEqual('foo') + // foo.bar setter is not inherited from Bar + expect(() => { + // @ts-expect-error bar cannot be overriden + foo.bar = 'baz' + }).toThrowError() + expect(foo.bar).toEqual('foo') + }) + + test('vi.spyOn().mockReset() resets the implementation', () => { + const object = createObject() + const spy = vi.spyOn(object, 'method').mockImplementation(() => 100) + expect(object.method()).toBe(100) + spy.mockReset() + expect(object.method()).toBe(42) + }) + + test('vi.spyOn() resets the implementation in resetAllMocks', () => { + const object = createObject() + vi.spyOn(object, 'method').mockImplementation(() => 100) + expect(object.method()).toBe(100) + vi.resetAllMocks() + expect(object.method()).toBe(42) + }) + + test('vi.spyOn() returns undefined as mockImplementation', () => { + const object = createObject() + const spy = vi.spyOn(object, 'method') + expect(spy.getMockImplementation()).toBe(undefined) + }) + + test('vi.spyOn() returns implementation if it was set', () => { + const implementation = () => 42 + const object = createObject() + const spy = vi.spyOn(object, 'method').mockImplementation(implementation) + expect(spy.getMockImplementation()).toBe(implementation) + spy.mockReset() + expect(spy.getMockImplementation()).toBe(undefined) + }) + + test('vi.spyOn() returns mockOnceImplementation if it was set', () => { + const implementation = () => 42 + const object = createObject() + const spy = vi.spyOn(object, 'method').mockImplementationOnce(implementation) + expect(spy.getMockImplementation()).toBe(implementation) + }) + + test('vi.spyOn() returns withImplementation if it was set', () => { + const implementation = () => 42 + const object = createObject() + const spy = vi.spyOn(object, 'method') + spy.withImplementation(implementation, () => { + expect(spy.getMockImplementation()).toBe(implementation) + }) + }) + + test('vi.spyOn() has a name', () => { + const object = createObject() + const spy = vi.spyOn(object, 'method') + expect(spy.getMockName()).toBe('vi.fn()') + spy.mockName('test') + expect(spy.getMockName()).toBe('test') + spy.mockReset() + expect(spy.getMockName()).toBe('vi.fn()') + spy.mockName('test') + expect(spy.getMockName()).toBe('test') + vi.resetAllMocks() + expect(spy.getMockName()).toBe('vi.fn()') + }) +}) + +describe('vi.spyOn() restoration', () => { + test('vi.spyOn() cannot spy on undefined or null', () => { + expect(() => vi.spyOn(undefined as any, 'test')).toThrowError('The vi.spyOn() function could not find an object to spy upon. The first argument must be defined.') + expect(() => vi.spyOn(null as any, 'test')).toThrowError('The vi.spyOn() function could not find an object to spy upon. The first argument must be defined.') + }) + + test('vi.spyOn() cannot spy on a primitive value', () => { + expect(() => vi.spyOn('string' as any, 'toString')).toThrowError('Vitest cannot spy on a primitive value.') + expect(() => vi.spyOn(0 as any, 'toString')).toThrowError('Vitest cannot spy on a primitive value.') + expect(() => vi.spyOn(true as any, 'toString')).toThrowError('Vitest cannot spy on a primitive value.') + expect(() => vi.spyOn(1n as any, 'toString')).toThrowError('Vitest cannot spy on a primitive value.') + expect(() => vi.spyOn(Symbol.toStringTag as any, 'toString')).toThrowError('Vitest cannot spy on a primitive value.') + }) + + test('vi.spyOn() cannot spy on non-existing property', () => { + expect(() => vi.spyOn({} as any, 'never')).toThrowError('The property "never" is not defined on the object.') + }) + + test('vi.spyOn() restores the original method when .mockRestore() is called', () => { + const object = createObject() + const spy = vi.spyOn(object, 'method') + object.method() + expect(vi.isMockFunction(object.method)).toBe(true) + expect(spy.mock.calls).toHaveLength(1) + spy.mockRestore() + expect(vi.isMockFunction(object.method)).toBe(false) + expect(spy.mock.calls).toHaveLength(0) + }) + + test('vi.spyOn() restores the original method when vi.restoreAllMocks() is called', () => { + const object = createObject() + const spy = vi.spyOn(object, 'method') + object.method() + expect(vi.isMockFunction(object.method)).toBe(true) + expect(spy.mock.calls).toHaveLength(1) + vi.restoreAllMocks() + expect(vi.isMockFunction(object.method)).toBe(false) + // unlike vi.mockRestore(), the state is not cleared + // this is important for module mocking + expect(spy.mock.calls).toHaveLength(1) + }) + + test('vi.spyOn() can respy the metthod with new state when vi.restoreAllMocks() is called', () => { + const object = createObject() + const spy1 = vi.spyOn(object, 'method').mockImplementation(() => 100) + + expect(object.method()).toBe(100) + expect(spy1.mock.calls).toHaveLength(1) + vi.restoreAllMocks() + + const spy2 = vi.spyOn(object, 'method').mockImplementation(() => 33) + expect(object.method()).toBe(33) + expect(spy2.mock.calls).toHaveLength(1) + }) + + test('vi.spyOn() restores the original getter when .mockRestore() is called', () => { + const object = createObject() + const spy = vi.spyOn(object, 'getter', 'get').mockImplementation(() => 100) + + expect(object.getter).toBe(100) + expect(spy.mock.calls).toHaveLength(1) + spy.mockRestore() + + expect(spy.mock.calls).toHaveLength(0) + expect(object.getter).toBe(42) + }) + + test('vi.spyOn() restores the original getter when vi.restoreAllMocks() is called', () => { + const object = createObject() + const spy = vi.spyOn(object, 'getter', 'get').mockImplementation(() => 100) + + expect(object.getter).toBe(100) + expect(spy.mock.calls).toHaveLength(1) + vi.restoreAllMocks() + + // unlike vi.mockRestore(), the state is not cleared + // this is important for module mocking + expect(spy.mock.calls).toHaveLength(1) + expect(object.getter).toBe(42) + }) + + test('vi.spyOn() can respy the getter with new state when vi.restoreAllMocks() is called', () => { + const object = createObject() + const spy1 = vi.spyOn(object, 'getter', 'get').mockImplementation(() => 100) + + expect(object.getter).toBe(100) + expect(spy1.mock.calls).toHaveLength(1) + vi.restoreAllMocks() + + const spy2 = vi.spyOn(object, 'getter', 'get').mockImplementation(() => 33) + expect(object.getter).toBe(33) + expect(spy2.mock.calls).toHaveLength(1) + }) + + test('vi.spyOn() restores the original setter when .mockRestore() is called', () => { + const object = createObject() + const spy = vi.spyOn(object, 'getter', 'set').mockImplementation(() => { + // do nothing + }) + + object.getter = 100 + + expect(object.getter).toBe(42) // getter was not overriden + expect(spy.mock.calls).toHaveLength(1) + spy.mockRestore() + + object.getter = 33 + + expect(spy.mock.calls).toHaveLength(0) + expect(object.getter).toBe(33) + }) + + test('vi.spyOn() restores the original getter when vi.restoreAllMocks() is called', () => { + const object = createObject() + const spy = vi.spyOn(object, 'getter', 'set').mockImplementation(() => { + // do nothing + }) + + object.getter = 100 + + expect(object.getter).toBe(42) // getter was not overriden + expect(spy.mock.calls).toHaveLength(1) + vi.restoreAllMocks() + + // unlike vi.mockRestore(), the state is not cleared + // this is important for module mocking + expect(spy.mock.calls).toHaveLength(1) + + object.getter = 33 + + expect(object.getter).toBe(33) + }) + + test('vi.spyOn() can respy the getter with new state when vi.restoreAllMocks() is called', () => { + const object = createObject() + const spy1 = vi.spyOn(object, 'getter', 'set').mockImplementation(() => { + // do nothing + }) + + object.getter = 100 + + expect(object.getter).toBe(42) + expect(spy1.mock.calls).toHaveLength(1) + vi.restoreAllMocks() + + let called = false + const spy2 = vi.spyOn(object, 'getter', 'set').mockImplementation(() => { + called = true + }) + + object.getter = 84 + + expect(called).toBe(true) + expect(object.getter).toBe(42) + expect(spy2.mock.calls).toHaveLength(1) + }) +}) + +function assertStateEmpty(state: MockContext) { + expect(state.calls).toHaveLength(0) + expect(state.results).toHaveLength(0) + expect(state.settledResults).toHaveLength(0) + expect(state.contexts).toHaveLength(0) + expect(state.instances).toHaveLength(0) + expect(state.lastCall).toBe(undefined) + expect(state.invocationCallOrder).toEqual([]) +} + +function createObject() { + let getterValue = 42 + return { + Class: class {}, + method() { + return 42 + }, + async() { + return Promise.resolve(42) + }, + get getter() { + return getterValue + }, + set getter(value: number) { + getterValue = value + }, + } +} From 208236940a202f24ab8f18036bf0582be56f0567 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 30 Jul 2025 17:27:48 +0200 Subject: [PATCH 02/29] chore: lint --- packages/vitest/src/integrations/vi.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vitest/src/integrations/vi.ts b/packages/vitest/src/integrations/vi.ts index 93511ac6ebe4..2fc175fedd01 100644 --- a/packages/vitest/src/integrations/vi.ts +++ b/packages/vitest/src/integrations/vi.ts @@ -9,7 +9,7 @@ import type { import type { RuntimeOptions, SerializedConfig } from '../runtime/config' import type { VitestMocker } from '../runtime/moduleRunner/moduleMocker' import type { MockFactoryWithHelper, MockOptions } from '../types/mocker' -import { fn, isMockFunction, resetAllMocks, restoreAllMocks, clearAllMocks, spyOn } from '@vitest/spy' +import { clearAllMocks, fn, isMockFunction, resetAllMocks, restoreAllMocks, spyOn } from '@vitest/spy' import { assertTypes, createSimpleStackTrace } from '@vitest/utils' import { getWorkerState, isChildProcess, resetModules, waitForImportsToResolve } from '../runtime/utils' import { parseSingleStack } from '../utils/source-map' From f6577005c1f1cbedacdbd144ec70438d87a7725e Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 30 Jul 2025 19:11:54 +0200 Subject: [PATCH 03/29] fix: copy static props, restore some previous behaviours --- packages/mocker/src/automocker.ts | 32 ++++---- packages/mocker/src/browser/mocker.ts | 7 +- packages/spy/src/index.ts | 80 +++++++++++++++++-- packages/spy/src/types.ts | 2 +- .../__snapshots__/jest-expect.test.ts.snap | 4 +- .../test/__snapshots__/snapshot.test.ts.snap | 4 +- test/core/test/jest-expect.test.ts | 34 ++++---- test/core/test/jest-mock.test.ts | 59 ++------------ .../test/mocked-class-restore-all.test.ts | 12 +++ test/core/test/mocking/vi-fn.test.ts | 16 ++-- test/core/test/snapshot.test.ts | 4 +- test/core/test/spy.test.ts | 10 +++ 12 files changed, 152 insertions(+), 112 deletions(-) diff --git a/packages/mocker/src/automocker.ts b/packages/mocker/src/automocker.ts index e40fda594008..064fa421e68f 100644 --- a/packages/mocker/src/automocker.ts +++ b/packages/mocker/src/automocker.ts @@ -1,16 +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 - createMockInstance: (options?: { - prototypeMembers?: (string | symbol)[] - name?: string | symbol - originalImplementation?: (...args: any[]) => any - keepMembersImplementation?: boolean - }) => any + createMockInstance: CreateMockInstanceProcedure } export function mockObject( @@ -54,7 +54,7 @@ export function mockObject( } // Skip special read-only props, we don't want to mess with those. - if (isReadonlyProp(property, containerType)) { + if (isReadonlyProp(container[property], property)) { continue } @@ -105,7 +105,7 @@ export function mockObject( ? collectFunctionProperties(newContainer[property].prototype) : [] const mock = createMockInstance({ - name: property, + name: newContainer[property].name, prototypeMembers, originalImplementation: options.type === 'autospy' ? newContainer[property] : undefined, keepMembersImplementation: options.type === 'autospy', @@ -153,7 +153,7 @@ function getType(value: unknown): string { return Object.prototype.toString.apply(value).slice(8, -1) } -function isReadonlyProp(object: unknown, prop: string) { +function isReadonlyProp(object: unknown, prop: string | symbol) { if ( prop === 'arguments' || prop === 'caller' @@ -244,10 +244,10 @@ function collectOwnProperties( function collectFunctionProperties(prototype: any) { const properties = new Set() - collectOwnProperties(prototype, (key) => { - const type = getType(prototype[key]) - if (type.includes('Function') && !isReadonlyProp(prototype[key], type)) { - properties.add(key) + collectOwnProperties(prototype, (prop) => { + const type = getType(prototype[prop]) + if (type.includes('Function') && !isReadonlyProp(prototype[prop], prop)) { + properties.add(prop) } }) return Array.from(properties) diff --git a/packages/mocker/src/browser/mocker.ts b/packages/mocker/src/browser/mocker.ts index 61ad5d505646..c8d4d81b6793 100644 --- a/packages/mocker/src/browser/mocker.ts +++ b/packages/mocker/src/browser/mocker.ts @@ -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' @@ -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, ) {} @@ -117,7 +118,7 @@ export class ModuleMocker { public mockObject( object: Record, - moduleType: MockedModuleType = 'automock', + moduleType: 'automock' | 'autospy' = 'automock', ): Record { return mockObject({ globalConstructors: { @@ -127,7 +128,7 @@ export class ModuleMocker { Map, RegExp, }, - spyOn: this.spyOn, + createMockInstance: this.createMockInstance, type: moduleType, }, object) } diff --git a/packages/spy/src/index.ts b/packages/spy/src/index.ts index f908051de7ea..7ed85c49426a 100644 --- a/packages/spy/src/index.ts +++ b/packages/spy/src/index.ts @@ -36,9 +36,11 @@ export function createMockInstance( prototypeConfig, keepMembersImplementation, name, + resetToMockImplementation, }: { originalImplementation?: Procedure | Constructable mockImplementation?: Procedure | Constructable + resetToMockImplementation?: boolean restore?: () => void prototypeMembers?: (string | symbol)[] keepMembersImplementation?: boolean @@ -56,9 +58,10 @@ export function createMockInstance( const mock = createMock( { config, state, name, prototypeState, prototypeConfig, keepMembersImplementation }, - originalImplementation, prototypeMembers, ) + // inherit the default name so it appears in snapshots and logs + // config.mockName = mock.name || 'vi.fn()' MOCK_CONFIGS.set(mock, config) REGISTERED_MOCKS.add(mock) @@ -163,7 +166,9 @@ export function createMockInstance( mock.mockReset = function mockReset() { mock.mockClear() - config.mockImplementation = undefined + config.mockImplementation = resetToMockImplementation + ? mockImplementation + : undefined config.mockName = 'vi.fn()' config.onceMockImplementations = [] return mock @@ -186,7 +191,7 @@ export function createMockInstance( } if (Symbol.dispose) { - mock[Symbol.dispose] = mock.mockRestore + mock[Symbol.dispose] = () => mock.mockRestore() } if (mockImplementation) { @@ -197,9 +202,15 @@ export function createMockInstance( } export function fn( - mockImplementation?: T, + originalImplementation?: T, ): Mock { - return createMockInstance({ mockImplementation }) as Mock + return createMockInstance({ + // so getMockImplementation() returns the value + mockImplementation: originalImplementation, + // special case so that .mockReset() resets the value to + // the the originalImplementation instead of () => undefined + resetToMockImplementation: true, + }) as Mock } export function spyOn>>( @@ -298,7 +309,7 @@ export function spyOn( const mock = createMockInstance({ restore, - originalImplementation: original, + originalImplementation: ssr && original ? original() : original, }) try { @@ -400,9 +411,9 @@ function createMock( name?: string | symbol keepMembersImplementation?: boolean }, - original?: Procedure | Constructable, prototypeMethods: (string | symbol)[] = [], ) { + const original = config.mockOriginal const name = (mockName || original?.name || 'Mock') as string const namedObject: Record = { // to keep the name of the function intact @@ -457,7 +468,7 @@ function createMock( } } else { - returnValue = (implementation as Procedure).call(this, args) + returnValue = (implementation as Procedure).apply(this, args) } } catch (error: any) { @@ -511,9 +522,62 @@ function createMock( return returnValue }) as Mock, } + if (original) { + copyOriginalStaticProperties(namedObject[name], original) + } return namedObject[name] } +function copyOriginalStaticProperties(mock: Mock, original: Procedure | Constructable) { + const { properties, descriptors } = getAllProperties(original) + + for (const key of properties) { + const descriptor = descriptors[key]! + const mockDescriptor = getDescriptor(mock, key) + if (mockDescriptor) { + continue + } + + Object.defineProperty(mock, key, descriptor) + } + return mock +} + +const ignoreProperties = new Set([ + 'length', + 'name', + 'prototype', + Symbol.for('nodejs.util.promisify.custom'), +]) + +function getAllProperties(original: Procedure | Constructable) { + const properties = new Set() + const descriptors: Record + = {} + while ( + original + && original !== Object.prototype + && original !== Function.prototype + ) { + const ownProperties = [ + ...Object.getOwnPropertyNames(original), + ...Object.getOwnPropertySymbols(original), + ] + for (const prop of ownProperties) { + if (descriptors[prop] || ignoreProperties.has(prop)) { + continue + } + properties.add(prop) + descriptors[prop] = Object.getOwnPropertyDescriptor(original, prop) + } + original = Object.getPrototypeOf(original) + } + return { + properties, + descriptors, + } +} + function getDefaultConfig(original?: Procedure | Constructable): MockConfig { return { mockImplementation: undefined, diff --git a/packages/spy/src/types.ts b/packages/spy/src/types.ts index b4ec1c91671b..f7ca0fa5ed89 100644 --- a/packages/spy/src/types.ts +++ b/packages/spy/src/types.ts @@ -442,7 +442,7 @@ export type Mocked = { } & T export interface MockConfig { - mockImplementation: Procedure | undefined + mockImplementation: Procedure | Constructable | undefined mockOriginal: Procedure | Constructable | undefined mockName: string onceMockImplementations: Array diff --git a/test/core/test/__snapshots__/jest-expect.test.ts.snap b/test/core/test/__snapshots__/jest-expect.test.ts.snap index e3a8785eb466..3a49b010891f 100644 --- a/test/core/test/__snapshots__/jest-expect.test.ts.snap +++ b/test/core/test/__snapshots__/jest-expect.test.ts.snap @@ -813,7 +813,7 @@ exports[`toHaveBeenNthCalledWith error 1`] = ` "expected": "Array [ "hey", ]", - "message": "expected 2nd "spy" call to have been called with [ 'hey' ]", + "message": "expected 2nd "vi.fn()" call to have been called with [ 'hey' ]", } `; @@ -824,7 +824,7 @@ exports[`toHaveBeenNthCalledWith error 2`] = ` "expected": "Array [ "hey", ]", - "message": "expected 3rd "spy" call to have been called with [ 'hey' ], but called only 2 times", + "message": "expected 3rd "vi.fn()" call to have been called with [ 'hey' ], but called only 2 times", } `; diff --git a/test/core/test/__snapshots__/snapshot.test.ts.snap b/test/core/test/__snapshots__/snapshot.test.ts.snap index e61bf4f9e133..d47f39d40c90 100644 --- a/test/core/test/__snapshots__/snapshot.test.ts.snap +++ b/test/core/test/__snapshots__/snapshot.test.ts.snap @@ -37,10 +37,10 @@ exports[`properties snapshot 1`] = ` } `; -exports[`renders mock snapshot 1`] = `[MockFunction spy]`; +exports[`renders mock snapshot 1`] = `[MockFunction]`; exports[`renders mock snapshot 2`] = ` -[MockFunction spy] { +[MockFunction] { "calls": [ [ "hello", diff --git a/test/core/test/jest-expect.test.ts b/test/core/test/jest-expect.test.ts index 0d6b039c4141..be8ec92598e5 100644 --- a/test/core/test/jest-expect.test.ts +++ b/test/core/test/jest-expect.test.ts @@ -714,7 +714,7 @@ describe('toSatisfy()', () => { describe('toHaveBeenCalled', () => { describe('negated', () => { it('fails if called', () => { - const mock = vi.fn() + const mock = vi.fn().mockName('spy') mock() expect(() => { @@ -753,7 +753,7 @@ describe('toHaveBeenCalled', () => { describe('toHaveBeenCalledWith', () => { describe('negated', () => { it('fails if called', () => { - const mock = vi.fn() + const mock = vi.fn().mockName('spy') mock(3) expect(() => { @@ -766,7 +766,7 @@ describe('toHaveBeenCalledWith', () => { describe('toHaveBeenCalledExactlyOnceWith', () => { describe('negated', () => { it('fails if called', () => { - const mock = vi.fn() + const mock = vi.fn().mockName('spy') mock(3) expect(() => { @@ -796,7 +796,7 @@ describe('toHaveBeenCalledExactlyOnceWith', () => { }) it('fails if not called or called too many times', () => { - const mock = vi.fn() + const mock = vi.fn().mockName('spy') expect(() => { expect(mock).toHaveBeenCalledExactlyOnceWith(3) @@ -816,7 +816,7 @@ describe('toHaveBeenCalledExactlyOnceWith', () => { expect(() => { expect(mock).toHaveBeenCalledExactlyOnceWith(3) - }).toThrow(/^expected "spy" to be called once with arguments: \[ 3 \][^e]/) + }).toThrow(/^expected "vi\.fn\(\)" to be called once with arguments: \[ 3 \][^e]/) }) it('passes if called exactly once with args', () => { @@ -829,8 +829,8 @@ describe('toHaveBeenCalledExactlyOnceWith', () => { describe('toHaveBeenCalledBefore', () => { it('success if expect mock is called before result mock', () => { - const expectMock = vi.fn() - const resultMock = vi.fn() + const expectMock = vi.fn().mockName('expectMock') + const resultMock = vi.fn().mockName('resultMock') expectMock() resultMock() @@ -859,7 +859,7 @@ describe('toHaveBeenCalledBefore', () => { expect(() => { expect(expectMock).toHaveBeenCalledBefore(resultMock) - }).toThrow(/^expected "spy" to have been called before "spy"/) + }).toThrow(/^expected "vi\.fn\(\)" to have been called before "vi\.fn\(\)"/) }) it('throws with correct mock name if failed', () => { @@ -875,13 +875,13 @@ describe('toHaveBeenCalledBefore', () => { }) it('fails if expect mock is not called', () => { - const resultMock = vi.fn() + const resultMock = vi.fn().mockName('resultMock') resultMock() expect(() => { expect(vi.fn()).toHaveBeenCalledBefore(resultMock) - }).toThrow(/^expected "spy" to have been called before "spy"/) + }).toThrow(/^expected "vi\.fn\(\)" to have been called before "resultMock"/) }) it('not fails if expect mock is not called with option `failIfNoFirstInvocation` set to false', () => { @@ -893,13 +893,13 @@ describe('toHaveBeenCalledBefore', () => { }) it('fails if result mock is not called', () => { - const expectMock = vi.fn() + const expectMock = vi.fn().mockName('expectMock') expectMock() expect(() => { expect(expectMock).toHaveBeenCalledBefore(vi.fn()) - }).toThrow(/^expected "spy" to have been called before "spy"/) + }).toThrow(/^expected "expectMock" to have been called before "vi\.fn\(\)"/) }) }) @@ -935,7 +935,7 @@ describe('toHaveBeenCalledAfter', () => { expect(() => { expect(expectMock).toHaveBeenCalledAfter(resultMock) - }).toThrow(/^expected "spy" to have been called after "spy"/) + }).toThrow(/^expected "vi\.fn\(\)" to have been called after "vi\.fn\(\)"/) }) it('throws with correct mock name if failed', () => { @@ -951,13 +951,13 @@ describe('toHaveBeenCalledAfter', () => { }) it('fails if result mock is not called', () => { - const expectMock = vi.fn() + const expectMock = vi.fn().mockName('expectMock') expectMock() expect(() => { expect(expectMock).toHaveBeenCalledAfter(vi.fn()) - }).toThrow(/^expected "spy" to have been called after "spy"/) + }).toThrow(/^expected "expectMock" to have been called after "vi\.fn\(\)"/) }) it('not fails if result mock is not called with option `failIfNoFirstInvocation` set to false', () => { @@ -969,13 +969,13 @@ describe('toHaveBeenCalledAfter', () => { }) it('fails if expect mock is not called', () => { - const resultMock = vi.fn() + const resultMock = vi.fn().mockName('resultMock') resultMock() expect(() => { expect(vi.fn()).toHaveBeenCalledAfter(resultMock) - }).toThrow(/^expected "spy" to have been called after "spy"/) + }).toThrow(/^expected "vi\.fn\(\)" to have been called after "resultMock"/) }) }) diff --git a/test/core/test/jest-mock.test.ts b/test/core/test/jest-mock.test.ts index 3df0f4f114a3..23042cef21ab 100644 --- a/test/core/test/jest-mock.test.ts +++ b/test/core/test/jest-mock.test.ts @@ -1,5 +1,4 @@ import { describe, expect, expectTypeOf, it, vi } from 'vitest' -import { rolldownVersion } from 'vitest/node' describe('jest mock compat layer', () => { const returnFactory = (type: string) => (value: any) => ({ type, value }) @@ -88,54 +87,6 @@ describe('jest mock compat layer', () => { expect(spy.mock.contexts).toEqual([instance]) }) - it('throws an error when constructing a class with an arrow function', () => { - function getTypeError() { - // esbuild transforms it into () => {\n}, but rolldown keeps it - return new TypeError(rolldownVersion - ? '() => {} is not a constructor' - : `() => { - } is not a constructor`) - } - - const arrow = () => {} - const Fn = vi.fn(arrow) - expect(() => new Fn()).toThrow(new TypeError( - `The spy implementation did not use 'function' or 'class', see https://vitest.dev/api/vi#vi-spyon for examples.`, - { - cause: getTypeError(), - }, - )) - - const obj = { - Spy: arrow, - } - - vi.spyOn(obj, 'Spy') - - expect( - // @ts-expect-error typescript knows you can't do that - () => new obj.Spy(), - ).toThrow(new TypeError( - `The spy implementation did not use 'function' or 'class', see https://vitest.dev/api/vi#vi-spyon for examples.`, - { - cause: getTypeError(), - }, - )) - - const properClass = { - Spy: class {}, - } - - vi.spyOn(properClass, 'Spy').mockImplementation(() => {}) - - expect(() => new properClass.Spy()).toThrow(new TypeError( - `The spy implementation did not use 'function' or 'class', see https://vitest.dev/api/vi#vi-spyon for examples.`, - { - cause: getTypeError(), - }, - )) - }) - it('implementation is set correctly on init', () => { const impl = () => 1 const mock1 = vi.fn(impl) @@ -224,7 +175,9 @@ describe('jest mock compat layer', () => { spy.mockRestore() - expect(spy.getMockImplementation()).toBe(undefined) + // Sicne Vitest 4 this is a special case where + // vi.fn(impl) will always return impl, unless overriden + expect(spy.getMockImplementation()).toBe(originalFn) expect(spy.mock.results).toEqual([]) }) @@ -422,7 +375,7 @@ describe('jest mock compat layer', () => { expect(obj.method).toHaveBeenCalledTimes(1) vi.spyOn(obj, 'method') obj.method() - expect(obj.method).toHaveBeenCalledTimes(1) + expect(obj.method).toHaveBeenCalledTimes(2) }) it('spyOn on the getter multiple times', () => { @@ -450,7 +403,7 @@ describe('jest mock compat layer', () => { expect(vi.isMockFunction(obj.method)).toBe(true) expect(obj.method()).toBe('mocked') - expect(spy1).not.toBe(spy2) + expect(spy1).toBe(spy2) spy2.mockImplementation(() => 'mocked2') @@ -710,7 +663,7 @@ describe('jest mock compat layer', () => { "f": { "configurable": true, "enumerable": false, - "value": [MockFunction f] { + "value": [MockFunction] { "calls": [ [], ], diff --git a/test/core/test/mocked-class-restore-all.test.ts b/test/core/test/mocked-class-restore-all.test.ts index 09bb1a83eb01..efd2ea377a9c 100644 --- a/test/core/test/mocked-class-restore-all.test.ts +++ b/test/core/test/mocked-class-restore-all.test.ts @@ -28,6 +28,9 @@ test(`mocked class are not affected by restoreAllMocks`, () => { expect(instance0.testFn('b')).toMatchInlineSnapshot(`undefined`) expect(vi.mocked(instance0.testFn).mock.calls).toMatchInlineSnapshot(` [ + [ + "a", + ], [ "b", ], @@ -35,6 +38,9 @@ test(`mocked class are not affected by restoreAllMocks`, () => { `) expect(vi.mocked(MockedE.prototype.testFn).mock.calls).toMatchInlineSnapshot(` [ + [ + "a", + ], [ "b", ], @@ -52,6 +58,9 @@ test(`mocked class are not affected by restoreAllMocks`, () => { expect(instance1.testFn('c')).toMatchInlineSnapshot(`undefined`) expect(vi.mocked(instance0.testFn).mock.calls).toMatchInlineSnapshot(` [ + [ + "a", + ], [ "b", ], @@ -67,6 +76,9 @@ test(`mocked class are not affected by restoreAllMocks`, () => { expect(vi.mocked(instance2.testFn).mock.calls).toMatchInlineSnapshot(`[]`) expect(vi.mocked(MockedE.prototype.testFn).mock.calls).toMatchInlineSnapshot(` [ + [ + "a", + ], [ "b", ], diff --git a/test/core/test/mocking/vi-fn.test.ts b/test/core/test/mocking/vi-fn.test.ts index cbae8fbea0b1..1c68f8513c45 100644 --- a/test/core/test/mocking/vi-fn.test.ts +++ b/test/core/test/mocking/vi-fn.test.ts @@ -234,7 +234,7 @@ describe('vi.fn() configuration', () => { const mock = vi.fn(() => 42) expect(mock()).toBe(42) mock.mockReset() - expect(mock()).toBe(undefined) + expect(mock()).toBe(42) }) test('vi.fn() resets the mock implementation', () => { @@ -306,7 +306,7 @@ describe('vi.fn() restoration', () => { const mock = vi.fn(() => 'hello') expect(mock()).toBe('hello') mock.mockRestore() - expect(mock()).toBe(undefined) + expect(mock()).toBe('hello') }) test('vi.fn() doesn\'t resets the added implementation in mock.mockRestore()', () => { @@ -387,7 +387,7 @@ describe('vi.fn() implementations', () => { expect(mock()).toBe(100) expect(mock()).toBe(100) mock.mockReset() - expect(mock()).toBe(undefined) + expect(mock()).toBe(42) }) test('vi.fn() with mockReturnValue overriding another mock', () => { @@ -418,7 +418,7 @@ describe('vi.fn() implementations', () => { expect(mock()).toBe(42) expect(mock()).toBe(42) mock.mockReset() - expect(mock()).toBe(undefined) + expect(mock()).toBe(42) }) test('vi.fn() with mockReturnValueOnce overriding another mock', () => { @@ -458,7 +458,7 @@ describe('vi.fn() implementations', () => { { type: 'fulfilled', value: 100 }, ]) mock.mockReset() - expect(mock()).toBe(undefined) + await expect(mock()).resolves.toBe(42) }) test('vi.fn() with mockResolvedValue overriding another mock', async () => { @@ -494,7 +494,7 @@ describe('vi.fn() implementations', () => { await expect(mock()).resolves.toBe(42) await expect(mock()).resolves.toBe(42) mock.mockReset() - expect(mock()).toBe(undefined) + await expect(mock()).resolves.toBe(42) }) test('vi.fn() with mockResolvedValueOnce overriding another mock', async () => { @@ -534,7 +534,7 @@ describe('vi.fn() implementations', () => { { type: 'rejected', value: 100 }, ]) mock.mockReset() - expect(mock()).toBe(undefined) + await expect(mock()).resolves.toBe(42) }) test('vi.fn() with mockRejectedValue overriding another mock', async () => { @@ -570,7 +570,7 @@ describe('vi.fn() implementations', () => { await expect(mock()).resolves.toBe(42) await expect(mock()).resolves.toBe(42) mock.mockReset() - expect(mock()).toBe(undefined) + await expect(mock()).resolves.toBe(42) }) test('vi.fn() with mockRejectedValueOnce overriding another mock', async () => { diff --git a/test/core/test/snapshot.test.ts b/test/core/test/snapshot.test.ts index 5f763dbe5319..dbcb05c5948a 100644 --- a/test/core/test/snapshot.test.ts +++ b/test/core/test/snapshot.test.ts @@ -114,10 +114,10 @@ test('renders mock snapshot', () => { test('renders inline mock snapshot', () => { const fn = vi.fn() - expect(fn).toMatchInlineSnapshot('[MockFunction spy]') + expect(fn).toMatchInlineSnapshot('[MockFunction]') fn('hello', 'world', 2) expect(fn).toMatchInlineSnapshot(` - [MockFunction spy] { + [MockFunction] { "calls": [ [ "hello", diff --git a/test/core/test/spy.test.ts b/test/core/test/spy.test.ts index 094f5461a547..452e163f6374 100644 --- a/test/core/test/spy.test.ts +++ b/test/core/test/spy.test.ts @@ -53,4 +53,14 @@ describe('spyOn', () => { expect(obj.A.HELLO_WORLD).toBe(true) expect((spy as any).HELLO_WORLD).toBe(true) }) + + test('ignores node.js.promisify symbol', () => { + const promisifySymbol = Symbol.for('nodejs.util.promisify.custom') + class Example { + static [promisifySymbol] = () => Promise.resolve(42) + } + const obj = { Example } + const spy = vi.spyOn(obj, 'Example') + expect((spy as any)[promisifySymbol]).toBe(undefined) + }) }) From d3f87e4eb78c3cfeaa7630c9007c5a70aff0a537 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 30 Jul 2025 19:29:05 +0200 Subject: [PATCH 04/29] fix: do not copy getters --- packages/mocker/src/automocker.ts | 26 ++++++++++++++----- .../test/__snapshots__/mocked.test.ts.snap | 22 ++++++++-------- test/core/test/mocked.test.ts | 4 +-- test/core/test/mocking/autospying.test.ts | 3 ++- 4 files changed, 34 insertions(+), 21 deletions(-) diff --git a/packages/mocker/src/automocker.ts b/packages/mocker/src/automocker.ts index 064fa421e68f..eaf8b4dae0a9 100644 --- a/packages/mocker/src/automocker.ts +++ b/packages/mocker/src/automocker.ts @@ -45,7 +45,14 @@ export function mockObject( // Modules define their exports as getters. We want to process those. if (!isModule && descriptor.get) { try { - Object.defineProperty(newContainer, property, descriptor) + 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. @@ -101,13 +108,14 @@ export function mockObject( ) } const createMockInstance = options.createMockInstance - const prototypeMembers = newContainer[property].prototype - ? collectFunctionProperties(newContainer[property].prototype) + const currentValue = newContainer[property] + const prototypeMembers = currentValue.prototype + ? collectFunctionProperties(currentValue.prototype) : [] const mock = createMockInstance({ - name: newContainer[property].name, + name: currentValue.name, prototypeMembers, - originalImplementation: options.type === 'autospy' ? newContainer[property] : undefined, + originalImplementation: options.type === 'autospy' ? currentValue : undefined, keepMembersImplementation: options.type === 'autospy', }) newContainer[property] = mock @@ -245,8 +253,12 @@ function collectOwnProperties( function collectFunctionProperties(prototype: any) { const properties = new Set() collectOwnProperties(prototype, (prop) => { - const type = getType(prototype[prop]) - if (type.includes('Function') && !isReadonlyProp(prototype[prop], 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) } }) diff --git a/test/core/test/__snapshots__/mocked.test.ts.snap b/test/core/test/__snapshots__/mocked.test.ts.snap index a2eb4faa5f69..808a273cd11a 100644 --- a/test/core/test/__snapshots__/mocked.test.ts.snap +++ b/test/core/test/__snapshots__/mocked.test.ts.snap @@ -1,11 +1,11 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`mocked function which fails on toReturnWith > just one call 1`] = ` -"expected "spy" to return with: 2 at least once +"expected "vi.fn()" to return with: 2 at least once Received: - 1st spy call return: + 1st vi.fn() call return: - 2 + 1 @@ -16,21 +16,21 @@ Number of calls: 1 `; exports[`mocked function which fails on toReturnWith > multi calls 1`] = ` -"expected "spy" to return with: 2 at least once +"expected "vi.fn()" to return with: 2 at least once Received: - 1st spy call return: + 1st vi.fn() call return: - 2 + 1 - 2nd spy call return: + 2nd vi.fn() call return: - 2 + 1 - 3rd spy call return: + 3rd vi.fn() call return: - 2 + 1 @@ -41,25 +41,25 @@ Number of calls: 3 `; exports[`mocked function which fails on toReturnWith > oject type 1`] = ` -"expected "spy" to return with: { a: '4' } at least once +"expected "vi.fn()" to return with: { a: '4' } at least once Received: - 1st spy call return: + 1st vi.fn() call return: { - "a": "4", + "a": "1", } - 2nd spy call return: + 2nd vi.fn() call return: { - "a": "4", + "a": "1", } - 3rd spy call return: + 3rd vi.fn() call return: { - "a": "4", @@ -72,7 +72,7 @@ Number of calls: 3 `; exports[`mocked function which fails on toReturnWith > zero call 1`] = ` -"expected "spy" to return with: 2 at least once +"expected "vi.fn()" to return with: 2 at least once Number of calls: 0 " diff --git a/test/core/test/mocked.test.ts b/test/core/test/mocked.test.ts index 85b60b95720f..19f1b20a12ba 100644 --- a/test/core/test/mocked.test.ts +++ b/test/core/test/mocked.test.ts @@ -91,7 +91,7 @@ describe('mocked classes', () => { expect(descriptor?.get).toBeDefined() expect(descriptor?.set).not.toBeDefined() - expect(instance.getOnlyProp).toBe(42) + expect(instance.getOnlyProp).toBe(undefined) // @ts-expect-error Assign to the read-only prop to ensure it errors. expect(() => instance.getOnlyProp = 4).toThrow() @@ -108,7 +108,7 @@ describe('mocked classes', () => { expect(descriptor?.get).toBeDefined() expect(descriptor?.set).toBeDefined() - expect(instance.getSetProp).toBe(123) + expect(instance.getSetProp).toBe(undefined) expect(() => instance.getSetProp = 4).not.toThrow() const getterSpy = vi.spyOn(instance, 'getSetProp', 'get').mockReturnValue(789) diff --git a/test/core/test/mocking/autospying.test.ts b/test/core/test/mocking/autospying.test.ts index 590ecf53b1ed..8760fdbacfd7 100644 --- a/test/core/test/mocking/autospying.test.ts +++ b/test/core/test/mocking/autospying.test.ts @@ -12,7 +12,8 @@ test('getAuthToken is spied', async () => { expect(token).toBe('123') expect(getAuthToken).toHaveBeenCalledTimes(1) vi.mocked(getAuthToken).mockRestore() - expect(vi.isMockFunction(getAuthToken)).toBe(false) + // module mocks cannot be restored + expect(vi.isMockFunction(getAuthToken)).toBe(true) }) test('package in __mocks__ has lower priority', async () => { From bfda42395eaeffdda975671cc1dd9dad3495dda4 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 30 Jul 2025 19:32:48 +0200 Subject: [PATCH 05/29] fix: keep getters and setters in spy mode --- packages/mocker/src/automocker.ts | 21 +++++++++------- packages/utils/src/source-map.ts | 40 +++++++++++++++---------------- 2 files changed, 33 insertions(+), 28 deletions(-) diff --git a/packages/mocker/src/automocker.ts b/packages/mocker/src/automocker.ts index eaf8b4dae0a9..86903f5d93a7 100644 --- a/packages/mocker/src/automocker.ts +++ b/packages/mocker/src/automocker.ts @@ -45,14 +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, { - configurable: descriptor.configurable, - enumerable: descriptor.enumerable, - // automatically mock getters and setters - // https://github.com/vitest-dev/vitest/issues/8345 - get: () => {}, - set: descriptor.set ? () => {} : undefined, - }) + 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. diff --git a/packages/utils/src/source-map.ts b/packages/utils/src/source-map.ts index 0b66d04321a6..ef32ef24bdb2 100644 --- a/packages/utils/src/source-map.ts +++ b/packages/utils/src/source-map.ts @@ -25,26 +25,26 @@ const SAFARI_NATIVE_CODE_REGEXP = /^(?:eval@)?(?:\[native code\])?$/ const stackIgnorePatterns = [ 'node:internal', - /\/packages\/\w+\/dist\//, - /\/@vitest\/\w+\/dist\//, - '/vitest/dist/', - '/vitest/src/', - '/vite-node/dist/', - '/vite-node/src/', - '/node_modules/chai/', - '/node_modules/tinypool/', - '/node_modules/tinyspy/', - '/vite/dist/node/module-runner', - '/rolldown-vite/dist/node/module-runner', - // browser related deps - '/deps/chunk-', - '/deps/@vitest', - '/deps/loupe', - '/deps/chai', - /node:\w+/, - /__vitest_test__/, - /__vitest_browser__/, - /\/deps\/vitest_/, + // /\/packages\/\w+\/dist\//, + // /\/@vitest\/\w+\/dist\//, + // '/vitest/dist/', + // '/vitest/src/', + // '/vite-node/dist/', + // '/vite-node/src/', + // '/node_modules/chai/', + // '/node_modules/tinypool/', + // '/node_modules/tinyspy/', + // '/vite/dist/node/module-runner', + // '/rolldown-vite/dist/node/module-runner', + // // browser related deps + // '/deps/chunk-', + // '/deps/@vitest', + // '/deps/loupe', + // '/deps/chai', + // /node:\w+/, + // /__vitest_test__/, + // /__vitest_browser__/, + // /\/deps\/vitest_/, ] function extractLocation(urlLike: string) { From 86eb9b10ce6925912bafb89e8c763c0c24c78e80 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 30 Jul 2025 19:38:56 +0200 Subject: [PATCH 06/29] chore: lint --- packages/browser/src/client/tester/tester.ts | 2 +- packages/mocker/src/browser/register.ts | 4 ++-- packages/vitest/src/runtime/moduleRunner/moduleMocker.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/browser/src/client/tester/tester.ts b/packages/browser/src/client/tester/tester.ts index 811a7d29f1a7..547cecc37095 100644 --- a/packages/browser/src/client/tester/tester.ts +++ b/packages/browser/src/client/tester/tester.ts @@ -109,7 +109,7 @@ async function prepareTestEnvironment(options: PrepareOptions) { const mocker = new VitestBrowserClientMocker( interceptor, rpc, - SpyModule.spyOn, + SpyModule.createMockInstance, { root: getBrowserState().viteConfig.root, }, diff --git a/packages/mocker/src/browser/register.ts b/packages/mocker/src/browser/register.ts index 785ae34795ff..a3cf43d00ebb 100644 --- a/packages/mocker/src/browser/register.ts +++ b/packages/mocker/src/browser/register.ts @@ -1,6 +1,6 @@ import type { ModuleMockerCompilerHints } from './hints' import type { ModuleMockerInterceptor } from './index' -import { spyOn } from '@vitest/spy' +import { createMockInstance } from '@vitest/spy' import { createCompilerHints } from './hints' import { ModuleMocker } from './index' import { hot, rpc } from './utils' @@ -24,7 +24,7 @@ export function registerModuleMocker( return rpc('vitest:mocks:invalidate', { ids }) }, }, - spyOn, + createMockInstance, { root: __VITEST_MOCKER_ROOT__, }, diff --git a/packages/vitest/src/runtime/moduleRunner/moduleMocker.ts b/packages/vitest/src/runtime/moduleRunner/moduleMocker.ts index 40e02b77b96a..1acf618a27bc 100644 --- a/packages/vitest/src/runtime/moduleRunner/moduleMocker.ts +++ b/packages/vitest/src/runtime/moduleRunner/moduleMocker.ts @@ -276,7 +276,7 @@ export class VitestMocker { public mockObject( object: Record, mockExports: Record = {}, - behavior: MockedModuleType = 'automock', + behavior: 'automock' | 'autospy' = 'automock', ): Record { const createMockInstance = this.spyModule?.createMockInstance if (!createMockInstance) { From 6a1ba968f7c049d95f8733f76f990114b4ed402c Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 30 Jul 2025 20:09:40 +0200 Subject: [PATCH 07/29] fix: keep using the original name in vi.spyOn --- packages/spy/src/index.ts | 9 +++++++-- test/core/test/jest-mock.test.ts | 2 +- test/core/test/mocking/vi-spyOn.test.ts | 6 +++--- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/packages/spy/src/index.ts b/packages/spy/src/index.ts index 7ed85c49426a..79997d19989e 100644 --- a/packages/spy/src/index.ts +++ b/packages/spy/src/index.ts @@ -37,6 +37,7 @@ export function createMockInstance( keepMembersImplementation, name, resetToMockImplementation, + resetToMockName, }: { originalImplementation?: Procedure | Constructable mockImplementation?: Procedure | Constructable @@ -46,6 +47,7 @@ export function createMockInstance( keepMembersImplementation?: boolean prototypeState?: MockContext prototypeConfig?: MockConfig + resetToMockName?: boolean name?: string | symbol } = {}, ): Mock { @@ -61,7 +63,9 @@ export function createMockInstance( prototypeMembers, ) // inherit the default name so it appears in snapshots and logs - // config.mockName = mock.name || 'vi.fn()' + if (resetToMockName) { + config.mockName = mock.name || 'vi.fn()' + } MOCK_CONFIGS.set(mock, config) REGISTERED_MOCKS.add(mock) @@ -169,7 +173,7 @@ export function createMockInstance( config.mockImplementation = resetToMockImplementation ? mockImplementation : undefined - config.mockName = 'vi.fn()' + config.mockName = resetToMockName ? (mock.name || 'vi.fn()') : 'vi.fn()' config.onceMockImplementations = [] return mock } @@ -310,6 +314,7 @@ export function spyOn( const mock = createMockInstance({ restore, originalImplementation: ssr && original ? original() : original, + resetToMockName: true, }) try { diff --git a/test/core/test/jest-mock.test.ts b/test/core/test/jest-mock.test.ts index 23042cef21ab..9f2d8bcec688 100644 --- a/test/core/test/jest-mock.test.ts +++ b/test/core/test/jest-mock.test.ts @@ -663,7 +663,7 @@ describe('jest mock compat layer', () => { "f": { "configurable": true, "enumerable": false, - "value": [MockFunction] { + "value": [MockFunction f] { "calls": [ [], ], diff --git a/test/core/test/mocking/vi-spyOn.test.ts b/test/core/test/mocking/vi-spyOn.test.ts index c9fdf6ebf775..5edee79fbf1a 100644 --- a/test/core/test/mocking/vi-spyOn.test.ts +++ b/test/core/test/mocking/vi-spyOn.test.ts @@ -418,15 +418,15 @@ describe('vi.spyOn() settings', () => { test('vi.spyOn() has a name', () => { const object = createObject() const spy = vi.spyOn(object, 'method') - expect(spy.getMockName()).toBe('vi.fn()') + expect(spy.getMockName()).toBe('method') spy.mockName('test') expect(spy.getMockName()).toBe('test') spy.mockReset() - expect(spy.getMockName()).toBe('vi.fn()') + expect(spy.getMockName()).toBe('method') spy.mockName('test') expect(spy.getMockName()).toBe('test') vi.resetAllMocks() - expect(spy.getMockName()).toBe('vi.fn()') + expect(spy.getMockName()).toBe('method') }) }) From b3f3e2206917a303f2749f0fb06305fdb5e74ae9 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 30 Jul 2025 20:16:58 +0200 Subject: [PATCH 08/29] docs: add migration guide --- docs/guide/migration.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/docs/guide/migration.md b/docs/guide/migration.md index a4d29e7da233..dc3140053485 100644 --- a/docs/guide/migration.md +++ b/docs/guide/migration.md @@ -116,6 +116,38 @@ const mock = new Spy() Note that now if you provide an arrow function, you will get [` is not a constructor` error](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Not_a_constructor) when the mock is called. +### Changes to Mocking + +Alongside new features like supporting constructors, Vitest 4 creates mocks differently to address several module mocking issues that we received over the years. This release attemts to make module spies less confusing, especially when working with classes. + +- `vi.fn().getMockName()` now returns `vi.fn()` by default instead of `spy`. This can affect snapshots with mocks - the name will be changed from `[MockFunction spy]` to `[MockFunction]`. Spies created with `vi.spyOn` will keep using the original name by default for better debugging experience +- `vi.restoreAllMocks` no longer resets the state of spies and only restores spies created manually with `vi.spyOn`, automocks are no longer affected by this function (this also affects the config option [`restoreMocks`](/config/#restoremocks)). Note that `.mockRestore` will still reset the mock implementation and clear the state +- Calling `vi.spyOn` on a mock now returns the same mock +- Automocked instance methods are now properly isolated, but share a state with the prototype. Overriding the prototype implementation will always affect instance methods unless the methods have a custom mock implementation of their own. Calling `.mockReset` on the mock also no longer breaks that inheritance. +```ts +import { AutoMockedClass } from './example.js' +const instance1 = new AutoMockedClass() +const instance2 = new AutoMockedClass() + +instance1.method.mockReturnValue(42) + +expect(instance1.method()).toBe(42) +expect(instance2.method()).toBe(undefined) + +expect(AutoMockedClass.prototype.method).toHaveBeenCalledTimes(2) + +instance1.method.mockReset() +AutoMockedClass.prototype.method.mockReturnValue(100) + +expect(instance1.method()).toBe(100) +expect(instance2.method()).toBe(100) + +expect(AutoMockedClass.prototype.method).toHaveBeenCalledTimes(4) +``` +- Automocked methods can no longer be restored, even with a manual `.mockRestore`. Automocked modules with `spy: true` will keep working as before +- Automocked getters no longer call the original getter. By default, automocked getters now return `undefined`. You can keep using `vi.spyOn(object, name, 'get')` to spy on a getter and change its implementation +- `vi.fn(implementation).mockReset()` now always resets the implementation to the original one (instead of setting it to `() => undefined`) and returns it properly in `.getMockImplementation()` + ### Standalone mode with filename filter To improve user experience, Vitest will now start running the matched files when [`--standalone`](/guide/cli#standalone) is used with filename filter. From 3cc251bb4171aa8203c707e30439704bb4226f66 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 30 Jul 2025 20:17:40 +0200 Subject: [PATCH 09/29] chore: remove debugging comment --- packages/utils/src/source-map.ts | 40 ++++++++++++++++---------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/packages/utils/src/source-map.ts b/packages/utils/src/source-map.ts index ef32ef24bdb2..0b66d04321a6 100644 --- a/packages/utils/src/source-map.ts +++ b/packages/utils/src/source-map.ts @@ -25,26 +25,26 @@ const SAFARI_NATIVE_CODE_REGEXP = /^(?:eval@)?(?:\[native code\])?$/ const stackIgnorePatterns = [ 'node:internal', - // /\/packages\/\w+\/dist\//, - // /\/@vitest\/\w+\/dist\//, - // '/vitest/dist/', - // '/vitest/src/', - // '/vite-node/dist/', - // '/vite-node/src/', - // '/node_modules/chai/', - // '/node_modules/tinypool/', - // '/node_modules/tinyspy/', - // '/vite/dist/node/module-runner', - // '/rolldown-vite/dist/node/module-runner', - // // browser related deps - // '/deps/chunk-', - // '/deps/@vitest', - // '/deps/loupe', - // '/deps/chai', - // /node:\w+/, - // /__vitest_test__/, - // /__vitest_browser__/, - // /\/deps\/vitest_/, + /\/packages\/\w+\/dist\//, + /\/@vitest\/\w+\/dist\//, + '/vitest/dist/', + '/vitest/src/', + '/vite-node/dist/', + '/vite-node/src/', + '/node_modules/chai/', + '/node_modules/tinypool/', + '/node_modules/tinyspy/', + '/vite/dist/node/module-runner', + '/rolldown-vite/dist/node/module-runner', + // browser related deps + '/deps/chunk-', + '/deps/@vitest', + '/deps/loupe', + '/deps/chai', + /node:\w+/, + /__vitest_test__/, + /__vitest_browser__/, + /\/deps\/vitest_/, ] function extractLocation(urlLike: string) { From fde9888e99cb94c4458f3ccc6dcc90683c359132 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 30 Jul 2025 20:25:23 +0200 Subject: [PATCH 10/29] test: fix browser snapshot test --- test/browser/specs/__snapshots__/update-snapshot.test.ts.snap | 4 ++-- test/browser/specs/update-snapshot.test.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/browser/specs/__snapshots__/update-snapshot.test.ts.snap b/test/browser/specs/__snapshots__/update-snapshot.test.ts.snap index b8595560765a..dfc9e3912e9f 100644 --- a/test/browser/specs/__snapshots__/update-snapshot.test.ts.snap +++ b/test/browser/specs/__snapshots__/update-snapshot.test.ts.snap @@ -18,10 +18,10 @@ test('basic', () => { test('renders inline mock snapshot', () => { const fn = vi.fn() - expect(fn).toMatchInlineSnapshot(\`[MockFunction spy]\`) + expect(fn).toMatchInlineSnapshot(\`[MockFunction]\`) fn('hello', 'world', 2) expect(fn).toMatchInlineSnapshot(\` - [MockFunction spy] { + [MockFunction] { "calls": [ [ "hello", diff --git a/test/browser/specs/update-snapshot.test.ts b/test/browser/specs/update-snapshot.test.ts index cd2cb4fcaacd..83b48cdc0ee0 100644 --- a/test/browser/specs/update-snapshot.test.ts +++ b/test/browser/specs/update-snapshot.test.ts @@ -46,7 +46,7 @@ test('update snapshot', async () => { expect(snapshotData).toContain('`1`') const testFile = readFileSync(testPath, 'utf-8') - expect(testFile).toContain('expect(fn).toMatchInlineSnapshot(`[MockFunction spy]`)') + expect(testFile).toContain('expect(fn).toMatchInlineSnapshot(`[MockFunction]`)') expect(testFile).toMatchSnapshot() // test passes From 9bae5543260184bbdb488477cf8ca5b7b5b5693a Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 30 Jul 2025 20:56:32 +0200 Subject: [PATCH 11/29] fix: don't allow overriding mock.mock --- packages/spy/src/index.ts | 34 +++++++++++----------------- test/core/test/mocking/vi-fn.test.ts | 9 ++++++++ 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/packages/spy/src/index.ts b/packages/spy/src/index.ts index 79997d19989e..758e3cc1c73a 100644 --- a/packages/spy/src/index.ts +++ b/packages/spy/src/index.ts @@ -75,21 +75,10 @@ export function createMockInstance( } Object.defineProperty(mock, 'mock', { - configurable: true, + configurable: false, enumerable: true, - get: () => state, - set: (newState: MockContext) => { - if (!newState || typeof newState !== 'object') { - return - } - - state.calls = newState.calls - state.contexts = newState.contexts - state.instances = newState.instances - state.invocationCallOrder = newState.invocationCallOrder - state.results = newState.results - state.settledResults = newState.settledResults - }, + writable: false, + value: state, }) mock.mockImplementation = function mockImplementation(implementation) { @@ -218,18 +207,18 @@ export function fn( } export function spyOn>>( - obj: T, - methodName: S, + object: T, + key: S, accessor: 'get' ): Mock<() => T[S]> export function spyOn>>( - obj: T, - methodName: G, + object: T, + key: G, accessor: 'set' ): Mock<(arg: T[G]) => void> export function spyOn> | Methods>>( - obj: T, - methodName: M + object: T, + key: M ): Required[M] extends { new (...args: infer A): infer R } ? Mock<{ new (...args: A): R }> : T[M] extends Procedure @@ -281,7 +270,8 @@ export function spyOn( original = object[key] as unknown as Procedure } - if (typeof original === 'function' && '_isMockFunction' in original && original._isMockFunction) { + // TODO: does it work with getters/setters? + if (isMockFunction(original)) { return original as any as Mock } @@ -497,6 +487,7 @@ function createMock( state.contexts[contextIndex - 1] = returnValue state.instances[instanceIndex - 1] = returnValue + // TODO: test this is correct if (contextPrototypeIndex != null && prototypeState) { prototypeState.contexts[contextPrototypeIndex - 1] = returnValue } @@ -594,6 +585,7 @@ function getDefaultConfig(original?: Procedure | Constructable): MockConfig { function getDefaultState(): MockContext { const state = { + // TODO: tests with arguments calls: [], contexts: [], instances: [], diff --git a/test/core/test/mocking/vi-fn.test.ts b/test/core/test/mocking/vi-fn.test.ts index 1c68f8513c45..98a7ccb77db7 100644 --- a/test/core/test/mocking/vi-fn.test.ts +++ b/test/core/test/mocking/vi-fn.test.ts @@ -11,6 +11,15 @@ test('vi.fn() calls implementation if it was passed down', () => { expect(mock()).toBe(3) }) +test('vi.fn().mock cannot be overriden', () => { + const mock = vi.fn() + expect(() => mock.mock = {} as any).toThrowError() + expect(() => { + // @ts-expect-error mock is not optional + delete mock.mock + }).toThrowError() +}) + describe('vi.fn() state', () => { // TODO: test when calls is not empty test('vi.fn() clears calls without a custom implementation', () => { From f7d02270c942978bd11fa79564b76b7d72aad23b Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 30 Jul 2025 20:57:15 +0200 Subject: [PATCH 12/29] docs: add example of mock.mock to jest differences --- docs/guide/migration.md | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/docs/guide/migration.md b/docs/guide/migration.md index dc3140053485..30c5c346b736 100644 --- a/docs/guide/migration.md +++ b/docs/guide/migration.md @@ -146,7 +146,7 @@ expect(AutoMockedClass.prototype.method).toHaveBeenCalledTimes(4) ``` - Automocked methods can no longer be restored, even with a manual `.mockRestore`. Automocked modules with `spy: true` will keep working as before - Automocked getters no longer call the original getter. By default, automocked getters now return `undefined`. You can keep using `vi.spyOn(object, name, 'get')` to spy on a getter and change its implementation -- `vi.fn(implementation).mockReset()` now always resets the implementation to the original one (instead of setting it to `() => undefined`) and returns it properly in `.getMockImplementation()` +- The mock `vi.fn(implementation).mockReset()` now correctly returns the mock implementation in `.getMockImplementation()` ### Standalone mode with filename filter @@ -213,7 +213,7 @@ Jest has their [globals API](https://jestjs.io/docs/api) enabled by default. Vit If you decide to keep globals disabled, be aware that common libraries like [`testing-library`](https://testing-library.com/) will not run auto DOM [cleanup](https://testing-library.com/docs/svelte-testing-library/api/#cleanup). -### `spy.mockReset` +### `mock.mockReset` Jest's [`mockReset`](https://jestjs.io/docs/mock-function-api#mockfnmockreset) replaces the mock implementation with an empty function that returns `undefined`. @@ -221,6 +221,18 @@ empty function that returns `undefined`. Vitest's [`mockReset`](/api/mock#mockreset) resets the mock implementation to its original. That is, resetting a mock created by `vi.fn(impl)` will reset the mock implementation to `impl`. +### `mock.mock` is Persistent + +Jest will recreate the mock state when `.mockClear` is called, meaning you always need to access it as a getter. Vitest, on the other hand, holds a persistent reference to the state, meaning you can reuse it: + +```ts +const mock = vi.fn() +const state = mock.mock +mock.mockClear() + +expect(state).toBe(mock.mock) // fails in Jest +``` + ### Module Mocks When mocking a module in Jest, the factory argument's return value is the default export. In Vitest, the factory argument has to return an object with each export explicitly defined. For example, the following `jest.mock` would have to be updated as follows: From 97c01d884d3a6383ecd220a8ed402859cfaa3941 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 30 Jul 2025 20:58:18 +0200 Subject: [PATCH 13/29] chore: remove tinyspy from dependencies --- packages/spy/package.json | 3 --- pnpm-lock.yaml | 19 +++---------------- 2 files changed, 3 insertions(+), 19 deletions(-) diff --git a/packages/spy/package.json b/packages/spy/package.json index 464bd1f5a5a6..9a724f698abb 100644 --- a/packages/spy/package.json +++ b/packages/spy/package.json @@ -31,8 +31,5 @@ "scripts": { "build": "rimraf dist && rollup -c", "dev": "rollup -c --watch" - }, - "dependencies": { - "tinyspy": "^4.0.3" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6bbfba0bf024..bcd56653141f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -749,11 +749,7 @@ importers: specifier: ^1.4.0 version: 1.4.0 - packages/spy: - dependencies: - tinyspy: - specifier: ^4.0.3 - version: 4.0.3 + packages/spy: {} packages/ui: dependencies: @@ -5542,10 +5538,6 @@ packages: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} - es-set-tostringtag@2.0.3: - resolution: {integrity: sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==} - engines: {node: '>= 0.4'} - es-set-tostringtag@2.1.0: resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} engines: {node: '>= 0.4'} @@ -8212,6 +8204,7 @@ packages: source-map@0.8.0-beta.0: resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} engines: {node: '>= 8'} + deprecated: The work that was done in this beta branch won't be included in future versions sourcemap-codec@1.4.8: resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} @@ -13400,7 +13393,7 @@ snapshots: es-define-property: 1.0.1 es-errors: 1.3.0 es-object-atoms: 1.1.1 - es-set-tostringtag: 2.0.3 + es-set-tostringtag: 2.1.0 es-to-primitive: 1.2.1 function.prototype.name: 1.1.6 get-intrinsic: 1.3.0 @@ -13459,12 +13452,6 @@ snapshots: dependencies: es-errors: 1.3.0 - es-set-tostringtag@2.0.3: - dependencies: - get-intrinsic: 1.3.0 - has-tostringtag: 1.0.2 - hasown: 2.0.2 - es-set-tostringtag@2.1.0: dependencies: es-errors: 1.3.0 From bec02bfc38820ab142ec6782d9f360a98b8b2ed3 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 31 Jul 2025 14:13:57 +0200 Subject: [PATCH 14/29] refactor: cleanup --- packages/spy/src/index.ts | 168 ++++++++++++------------ packages/spy/src/types.ts | 40 ++++-- test/core/test/mocking/vi-spyOn.test.ts | 12 +- 3 files changed, 120 insertions(+), 100 deletions(-) diff --git a/packages/spy/src/index.ts b/packages/spy/src/index.ts index 758e3cc1c73a..3a5aa5b3e02d 100644 --- a/packages/spy/src/index.ts +++ b/packages/spy/src/index.ts @@ -5,7 +5,8 @@ import type { Mock, MockConfig, MockContext, - MockFnContext, + MockInstanceOption, + MockProcedureContext, MockResult, MockReturnType, MockSettledResult, @@ -26,31 +27,15 @@ const MOCK_RESTORE = new Set<() => void>() const REGISTERED_MOCKS = new Set() const MOCK_CONFIGS = new WeakMap() -export function createMockInstance( - { +export function createMockInstance(options: MockInstanceOption = {}): Mock { + const { originalImplementation, restore, mockImplementation, - prototypeMembers, - prototypeState, - prototypeConfig, - keepMembersImplementation, - name, resetToMockImplementation, resetToMockName, - }: { - originalImplementation?: Procedure | Constructable - mockImplementation?: Procedure | Constructable - resetToMockImplementation?: boolean - restore?: () => void - prototypeMembers?: (string | symbol)[] - keepMembersImplementation?: boolean - prototypeState?: MockContext - prototypeConfig?: MockConfig - resetToMockName?: boolean - name?: string | symbol - } = {}, -): Mock { + } = options + if (restore) { MOCK_RESTORE.add(restore) } @@ -58,11 +43,14 @@ export function createMockInstance( const config = getDefaultConfig(originalImplementation) const state = getDefaultState() - const mock = createMock( - { config, state, name, prototypeState, prototypeConfig, keepMembersImplementation }, - prototypeMembers, - ) + const mock = createMock({ + config, + state, + ...options, + }) // inherit the default name so it appears in snapshots and logs + // this is used by `vi.spyOn()` for better debugging. + // when `vi.fn()` is called, we just use the default string if (resetToMockName) { config.mockName = mock.name || 'vi.fn()' } @@ -71,6 +59,8 @@ export function createMockInstance( mock._isMockFunction = true mock.getMockImplementation = () => { + // Jest only returns `config.mockImplementation` here, + // but we think it makes sense to return what the next function will be called return config.onceMockImplementations[0] || config.mockImplementation } @@ -188,7 +178,7 @@ export function createMockInstance( } if (mockImplementation) { - mock.mockImplementation(mockImplementation as any) // TODO: typess + mock.mockImplementation(mockImplementation) } return mock @@ -198,7 +188,7 @@ export function fn( originalImplementation?: T, ): Mock { return createMockInstance({ - // so getMockImplementation() returns the value + // we pass this down so getMockImplementation() always returns the value mockImplementation: originalImplementation, // special case so that .mockReset() resets the value to // the the originalImplementation instead of () => undefined @@ -270,9 +260,8 @@ export function spyOn( original = object[key] as unknown as Procedure } - // TODO: does it work with getters/setters? if (isMockFunction(original)) { - return original as any as Mock + return original } const reassign = (cb: any) => { @@ -358,38 +347,6 @@ function assert(condition: any, message: string): asserts condition { let invocationCallCounter = 1 -function addCalls(args: unknown[], state: MockContext, prototypeState?: MockContext) { - state.calls.push(args) - prototypeState?.calls.push(args) -} - -function increaseInvocationOrder(order: number, state: MockContext, prototypeState?: MockContext) { - state.invocationCallOrder.push(order) - prototypeState?.invocationCallOrder.push(order) -} - -function addResult(result: MockResult, state: MockContext, prototypeState?: MockContext) { - state.results.push(result) - prototypeState?.results.push(result) -} - -function addSettledResult(result: MockSettledResult, state: MockContext, prototypeState?: MockContext) { - state.settledResults.push(result) - prototypeState?.settledResults.push(result) -} - -function addInstance(instance: MockReturnType, state: MockContext, prototypeState?: MockContext) { - const instanceIndex = state.instances.push(instance) - const instancePrototypeIndex = prototypeState?.instances.push(instance) - return [instanceIndex, instancePrototypeIndex] as const -} - -function addContext(context: MockFnContext, state: MockContext, prototypeState?: MockContext) { - const contextIndex = state.contexts.push(context) - const contextPrototypeIndex = prototypeState?.contexts.push(context) - return [contextIndex, contextPrototypeIndex] as const -} - function createMock( { state, @@ -398,23 +355,19 @@ function createMock( prototypeState, prototypeConfig, keepMembersImplementation, - }: { - prototypeState?: MockContext - prototypeConfig?: MockConfig - state: MockContext + prototypeMembers = [], + }: MockInstanceOption & { + state: MockContext config: MockConfig - name?: string | symbol - keepMembersImplementation?: boolean }, - prototypeMethods: (string | symbol)[] = [], ) { const original = config.mockOriginal const name = (mockName || original?.name || 'Mock') as string - const namedObject: Record = { + const namedObject: Record> = { // to keep the name of the function intact [name]: (function (this: any, ...args: any[]) { - addCalls(args, state, prototypeState) - increaseInvocationOrder(invocationCallCounter++, state, prototypeState) + registerCalls(args, state, prototypeState) + registerInvocationOrder(invocationCallCounter++, state, prototypeState) const result = { type: 'incomplete', @@ -426,18 +379,20 @@ function createMock( value: undefined, } as MockSettledResult - addResult(result, state, prototypeState) - addSettledResult(settledResult, state, prototypeState) + registerResult(result, state, prototypeState) + registerSettledResult(settledResult, state, prototypeState) - const [instanceIndex, instancePrototypeIndex] = addInstance(new.target ? undefined : this, state, prototypeState) - const [contextIndex, contextPrototypeIndex] = addContext(new.target ? undefined : this, state, prototypeState) + const context = new.target ? undefined : this + const [instanceIndex, instancePrototypeIndex] = registerInstance(context, state, prototypeState) + const [contextIndex, contextPrototypeIndex] = registerContext(context, state, prototypeState) - const implementation: Procedure | Constructable = config.onceMockImplementations.shift() - || config.mockImplementation - || prototypeConfig?.onceMockImplementations.shift() - || prototypeConfig?.mockImplementation - || original - || function () {} + const implementation: Procedure | Constructable + = config.onceMockImplementations.shift() + || config.mockImplementation + || prototypeConfig?.onceMockImplementations.shift() + || prototypeConfig?.mockImplementation + || original + || function () {} let returnValue let thrownValue @@ -447,7 +402,11 @@ function createMock( if (new.target) { returnValue = Reflect.construct(implementation, args, new.target) - for (const prop of prototypeMethods) { + // 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. + // fortunetly, the constructor is always an empty functon because `prototypeMethods` + // are only used by the automocker, so this doesn't matter + for (const prop of prototypeMembers) { const prototypeMock = returnValue[prop] const isMock = isMockFunction(prototypeMock) const prototypeState = isMock ? prototypeMock.mock : undefined @@ -516,7 +475,7 @@ function createMock( } return returnValue - }) as Mock, + }) as Mock, } if (original) { copyOriginalStaticProperties(namedObject[name], original) @@ -524,6 +483,38 @@ function createMock( return namedObject[name] } +function registerCalls(args: unknown[], state: MockContext, prototypeState?: MockContext) { + state.calls.push(args) + prototypeState?.calls.push(args) +} + +function registerInvocationOrder(order: number, state: MockContext, prototypeState?: MockContext) { + state.invocationCallOrder.push(order) + prototypeState?.invocationCallOrder.push(order) +} + +function registerResult(result: MockResult, state: MockContext, prototypeState?: MockContext) { + state.results.push(result) + prototypeState?.results.push(result) +} + +function registerSettledResult(result: MockSettledResult, state: MockContext, prototypeState?: MockContext) { + state.settledResults.push(result) + prototypeState?.settledResults.push(result) +} + +function registerInstance(instance: MockReturnType, state: MockContext, prototypeState?: MockContext) { + const instanceIndex = state.instances.push(instance) + const instancePrototypeIndex = prototypeState?.instances.push(instance) + return [instanceIndex, instancePrototypeIndex] as const +} + +function registerContext(context: MockProcedureContext, state: MockContext, prototypeState?: MockContext) { + const contextIndex = state.contexts.push(context) + const contextPrototypeIndex = prototypeState?.contexts.push(context) + return [contextIndex, contextPrototypeIndex] as const +} + function copyOriginalStaticProperties(mock: Mock, original: Procedure | Constructable) { const { properties, descriptors } = getAllProperties(original) @@ -585,7 +576,6 @@ function getDefaultConfig(original?: Procedure | Constructable): MockConfig { function getDefaultState(): MockContext { const state = { - // TODO: tests with arguments calls: [], contexts: [], instances: [], @@ -629,8 +619,18 @@ export type { MockedObject, MockedObjectDeep, MockInstance, + MockInstanceOption, + MockParameters, + MockProcedureContext, MockResult, + MockResultIncomplete, + MockResultReturn, + MockResultThrow, + MockReturnType, MockSettledResult, + MockSettledResultFulfilled, + MockSettledResultIncomplete, + MockSettledResultRejected, PartiallyMockedFunction, PartiallyMockedFunctionDeep, PartialMock, diff --git a/packages/spy/src/types.ts b/packages/spy/src/types.ts index f7ca0fa5ed89..eda27a357f95 100644 --- a/packages/spy/src/types.ts +++ b/packages/spy/src/types.ts @@ -1,15 +1,15 @@ -interface MockResultReturn { +export interface MockResultReturn { type: 'return' /** * The value that was returned from the function. If function returned a Promise, then this will be a resolved value. */ value: T } -interface MockResultIncomplete { +export interface MockResultIncomplete { type: 'incomplete' value: undefined } -interface MockResultThrow { +export interface MockResultThrow { type: 'throw' /** * An error that was thrown during function execution. @@ -17,17 +17,17 @@ interface MockResultThrow { value: any } -interface MockSettledResultIncomplete { +export interface MockSettledResultIncomplete { type: 'incomplete' value: undefined } -interface MockSettledResultFulfilled { +export interface MockSettledResultFulfilled { type: 'fulfilled' value: T } -interface MockSettledResultRejected { +export interface MockSettledResultRejected { type: 'rejected' value: any } @@ -51,7 +51,7 @@ export type MockReturnType = T extends Cons : T extends Procedure ? ReturnType : never -export type MockFnContext = T extends Constructable +export type MockProcedureContext = T extends Constructable ? InstanceType : ThisParameterType @@ -81,7 +81,7 @@ export interface MockContext { * An array of `this` values that were used during each call to the mock function. * @see https://vitest.dev/api/mock#mock-contexts */ - contexts: MockFnContext[] + contexts: MockProcedureContext[] /** * The order of mock's execution. This returns an array of numbers which are shared between all defined mocks. * @@ -139,7 +139,12 @@ export interface MockContext { * * const result = fn() * - * fn.mock.settledResults === [] + * fn.mock.settledResults === [ + * { + * type: 'incomplete', + * value: undefined, + * } + * ] * fn.mock.results === [ * { * type: 'return', @@ -361,8 +366,6 @@ export interface Mock extends M (...args: MockParameters): MockReturnType /** @internal */ _isMockFunction: true - /** @internal */ - _protoImplementation?: Procedure | Constructable } type PartialMaybePromise = T extends Promise> @@ -445,5 +448,18 @@ export interface MockConfig { mockImplementation: Procedure | Constructable | undefined mockOriginal: Procedure | Constructable | undefined mockName: string - onceMockImplementations: Array + onceMockImplementations: Array +} + +export interface MockInstanceOption { + originalImplementation?: Procedure | Constructable + mockImplementation?: Procedure | Constructable + resetToMockImplementation?: boolean + restore?: () => void + prototypeMembers?: (string | symbol)[] + keepMembersImplementation?: boolean + prototypeState?: MockContext + prototypeConfig?: MockConfig + resetToMockName?: boolean + name?: string | symbol } diff --git a/test/core/test/mocking/vi-spyOn.test.ts b/test/core/test/mocking/vi-spyOn.test.ts index 5edee79fbf1a..5b0573617bbc 100644 --- a/test/core/test/mocking/vi-spyOn.test.ts +++ b/test/core/test/mocking/vi-spyOn.test.ts @@ -264,8 +264,10 @@ describe('vi.spyOn() settings', () => { const spy2 = vi.spyOn(object, 'getter', 'get') expect(spy1).toBe(spy2) - object.method() - expect(spy2.mock.calls).toEqual(spy2.mock.calls) + const _example = object.getter + expect(spy2).toHaveBeenCalledTimes(1) + expect(spy1).toHaveBeenCalledTimes(1) + expect(spy2.mock.calls).toEqual(spy1.mock.calls) }) test('vi.spyOn() when spying on a setter spy returns the same spy', () => { @@ -274,8 +276,10 @@ describe('vi.spyOn() settings', () => { const spy2 = vi.spyOn(object, 'getter', 'set') expect(spy1).toBe(spy2) - object.method() - expect(spy2.mock.calls).toEqual(spy2.mock.calls) + object.getter = 33 + expect(spy2).toHaveBeenCalledTimes(1) + expect(spy1).toHaveBeenCalledTimes(1) + expect(spy2.mock.calls).toEqual(spy1.mock.calls) }) test('vi.spyOn() can spy on multiple class instances without intervention', () => { From 5f12b6a384496d9ebe5c4426fd9875817c3210b6 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 31 Jul 2025 14:14:18 +0200 Subject: [PATCH 15/29] docs: cleanup --- docs/api/mock.md | 43 ++++++++++++++++++++++++++--------------- docs/guide/migration.md | 1 + 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/docs/api/mock.md b/docs/api/mock.md index 782c6ca8769c..b8f74fe4a97c 100644 --- a/docs/api/mock.md +++ b/docs/api/mock.md @@ -47,7 +47,7 @@ Use it to return the name assigned to the mock with the `.mockName(name)` method ## mockClear ```ts -function mockClear(): MockInstance +function mockClear(): Mock ``` Clears all information about every call. After calling it, all properties on `.mock` will return to their initial state. This method does not reset implementations. It is useful for cleaning up mocks between different assertions. @@ -72,7 +72,7 @@ To automatically call this method before each test, enable the [`clearMocks`](/c ## mockName ```ts -function mockName(name: string): MockInstance +function mockName(name: string): Mock ``` Sets the internal mock name. This is useful for identifying the mock when an assertion fails. @@ -80,7 +80,7 @@ Sets the internal mock name. This is useful for identifying the mock when an ass ## mockImplementation ```ts -function mockImplementation(fn: T): MockInstance +function mockImplementation(fn: T): Mock ``` Accepts a function to be used as the mock implementation. TypeScript expects the arguments and return type to match those of the original function. @@ -102,7 +102,7 @@ mockFn.mock.calls[1][0] === 1 // true ## mockImplementationOnce ```ts -function mockImplementationOnce(fn: T): MockInstance +function mockImplementationOnce(fn: T): Mock ``` Accepts a function to be used as the mock implementation. TypeScript expects the arguments and return type to match those of the original function. This method can be chained to produce different results for multiple function calls. @@ -135,11 +135,11 @@ console.log(myMockFn(), myMockFn(), myMockFn(), myMockFn()) function withImplementation( fn: T, cb: () => void -): MockInstance +): Mock function withImplementation( fn: T, cb: () => Promise -): Promise> +): Promise> ``` Overrides the original mock implementation temporarily while the callback is being executed. @@ -177,7 +177,7 @@ Note that this method takes precedence over the [`mockImplementationOnce`](#mock ## mockRejectedValue ```ts -function mockRejectedValue(value: unknown): MockInstance +function mockRejectedValue(value: unknown): Mock ``` Accepts an error that will be rejected when async function is called. @@ -191,7 +191,7 @@ await asyncMock() // throws Error<'Async error'> ## mockRejectedValueOnce ```ts -function mockRejectedValueOnce(value: unknown): MockInstance +function mockRejectedValueOnce(value: unknown): Mock ``` Accepts a value that will be rejected during the next function call. If chained, each consecutive call will reject the specified value. @@ -209,7 +209,7 @@ await asyncMock() // throws Error<'Async error'> ## mockReset ```ts -function mockReset(): MockInstance +function mockReset(): Mock ``` Does what [`mockClear`](#mockClear) does and resets inner implementation to the original function. @@ -241,7 +241,7 @@ To automatically call this method before each test, enable the [`mockReset`](/co ## mockRestore ```ts -function mockRestore(): MockInstance +function mockRestore(): Mock ``` Does what [`mockReset`](#mockReset) does and restores original descriptors of spied-on objects. @@ -270,7 +270,7 @@ To automatically call this method before each test, enable the [`restoreMocks`]( ## mockResolvedValue ```ts -function mockResolvedValue(value: Awaited>): MockInstance +function mockResolvedValue(value: Awaited>): Mock ``` Accepts a value that will be resolved when the async function is called. TypeScript will only accept values that match the return type of the original function. @@ -284,7 +284,7 @@ await asyncMock() // 42 ## mockResolvedValueOnce ```ts -function mockResolvedValueOnce(value: Awaited>): MockInstance +function mockResolvedValueOnce(value: Awaited>): Mock ``` Accepts a value that will be resolved during the next function call. TypeScript will only accept values that match the return type of the original function. If chained, each consecutive call will resolve the specified value. @@ -305,7 +305,7 @@ await asyncMock() // default ## mockReturnThis ```ts -function mockReturnThis(): MockInstance +function mockReturnThis(): Mock ``` Use this if you need to return the `this` context from the method without invoking the actual implementation. This is a shorthand for: @@ -319,7 +319,7 @@ spy.mockImplementation(function () { ## mockReturnValue ```ts -function mockReturnValue(value: ReturnType): MockInstance +function mockReturnValue(value: ReturnType): Mock ``` Accepts a value that will be returned whenever the mock function is called. TypeScript will only accept values that match the return type of the original function. @@ -335,7 +335,7 @@ mock() // 43 ## mockReturnValueOnce ```ts -function mockReturnValueOnce(value: ReturnType): MockInstance +function mockReturnValueOnce(value: ReturnType): Mock ``` Accepts a value that will be returned whenever the mock function is called. TypeScript will only accept values that match the return type of the original function. @@ -450,6 +450,11 @@ fn.mock.results === [ ## mock.settledResults ```ts +interface MockSettledResultIncomplete { + type: 'incomplete' + value: undefined +} + interface MockSettledResultFulfilled { type: 'fulfilled' value: T @@ -463,6 +468,7 @@ interface MockSettledResultRejected { export type MockSettledResult = | MockSettledResultFulfilled | MockSettledResultRejected + | MockSettledResultIncomplete const settledResults: MockSettledResult>>[] ``` @@ -476,7 +482,12 @@ const fn = vi.fn().mockResolvedValueOnce('result') const result = fn() -fn.mock.settledResults === [] +fn.mock.settledResults === [ + { + type: 'incomplete', + value: undefined, + }, +] await result diff --git a/docs/guide/migration.md b/docs/guide/migration.md index 30c5c346b736..a7dfea30437f 100644 --- a/docs/guide/migration.md +++ b/docs/guide/migration.md @@ -147,6 +147,7 @@ expect(AutoMockedClass.prototype.method).toHaveBeenCalledTimes(4) - Automocked methods can no longer be restored, even with a manual `.mockRestore`. Automocked modules with `spy: true` will keep working as before - Automocked getters no longer call the original getter. By default, automocked getters now return `undefined`. You can keep using `vi.spyOn(object, name, 'get')` to spy on a getter and change its implementation - The mock `vi.fn(implementation).mockReset()` now correctly returns the mock implementation in `.getMockImplementation()` +- `vi.fn().mock.invocationCallOrder` now starts with `1`, like Jest does, instead of `0` ### Standalone mode with filename filter From 0e14a62ac0b9f37e9bf7631fe22e4d4d6799e403 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 31 Jul 2025 16:24:26 +0200 Subject: [PATCH 16/29] test: more test --- test/core/test/mocking/vi-mockObject.test.ts | 47 +++++++++++++++++++- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/test/core/test/mocking/vi-mockObject.test.ts b/test/core/test/mocking/vi-mockObject.test.ts index 540fc2b50631..b81dc0439023 100644 --- a/test/core/test/mocking/vi-mockObject.test.ts +++ b/test/core/test/mocking/vi-mockObject.test.ts @@ -18,6 +18,14 @@ test('when properties are spied, they keep the implementation', () => { expect(instance.method()).toBe(42) expect(instance.method).toHaveBeenCalled() expect(module.Class.prototype.method).toHaveBeenCalledTimes(1) + + vi.mocked(instance.method).mockReturnValue(100) + expect(instance.method()).toBe(100) + expect(module.Class.prototype.method).toHaveBeenCalledTimes(2) + + vi.mocked(instance.method).mockReset() + expect(instance.method()).toBe(42) + expect(module.Class.prototype.method).toHaveBeenCalledTimes(3) }) test('vi.restoreAllMocks() does not affect mocks', () => { @@ -77,6 +85,41 @@ test('instance mocks are independently tracked, but prototype shares the state', expect(Class.prototype.method).toHaveBeenCalledTimes(3) }) +test('instance methods and prototype method share the state', () => { + const { Class } = mockModule() + const t1 = vi.mocked(new Class()) + + expect(t1.method.mock).toEqual(Class.prototype.method.mock) + + t1.method('hello world', Symbol.for('example')) + + expect(t1.method.mock.calls[0][0]).toBe('hello world') + expect(t1.method.mock.calls[0][1]).toBe(Symbol.for('example')) + + expect(t1.method.mock.instances[0]).toBe(t1) + expect(t1.method.mock.contexts[0]).toBe(t1) + expect(t1.method.mock.results[0]).toEqual({ + type: 'return', + value: undefined, + }) + + expect(Class.prototype.method.mock.calls[0][0]).toBe('hello world') + expect(Class.prototype.method.mock.calls[0][1]).toBe(Symbol.for('example')) + + expect(t1.method.mock).toEqual(Class.prototype.method.mock) + + const t2 = vi.mocked(new Class()) + + t2.method('bye world') + + expect(t1.method).toHaveBeenCalledTimes(1) + expect(t2.method).toHaveBeenCalledTimes(1) + + expect(t2.method.mock.calls[0][0]).toBe('bye world') + // note that Class.prototype.method keeps accumulating state + expect(Class.prototype.method.mock.calls[1][0]).toBe('bye world') +}) + test('instance methods inherit the implementation, but can override the local ones', () => { const { Class } = mockModule() const t1 = vi.mocked(new Class()) @@ -144,11 +187,11 @@ function mockModule(type: 'automock' | 'autospy' = 'automock') { return vi.mockObject({ [Symbol.toStringTag]: 'Module', __esModule: true, - method() { + method(..._args: any[]) { return 42 }, Class: class { - method() { + method(..._args: any[]) { return 42 } }, From 2a032661c4eb468c6efde5c97b422e9352298180 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 31 Jul 2025 18:49:39 +0200 Subject: [PATCH 17/29] docs: add mocking modules guide --- docs/.vitepress/config.ts | 35 ++++ docs/guide/mocking.md | 247 +++--------------------- docs/guide/module-mocking.md | 355 +++++++++++++++++++++++++++++++++++ 3 files changed, 418 insertions(+), 219 deletions(-) create mode 100644 docs/guide/module-mocking.md diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 690b348c48ef..bc83bc0fb72f 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -502,6 +502,41 @@ function guide(): DefaultTheme.SidebarItem[] { { text: 'Mocking', link: '/guide/mocking', + collapsed: true, + items: [ + { + text: 'Mocking Dates', + link: '/guide/mocking#dates', + }, + { + text: 'Mocking Functions', + link: '/guide/mocking#functions', + }, + { + text: 'Mocking Globals', + link: '/guide/mocking#globals', + }, + { + text: 'Mocking Modules', + link: '/guide/module-mocking', + }, + { + text: 'Mocking File System', + link: '/guide/mocking#file-system', + }, + { + text: 'Mocking Requests', + link: '/guide/mocking#requests', + }, + { + text: 'Mocking Timers', + link: '/guide/mocking#timers', + }, + { + text: 'Mocking Classes', + link: '/guide/mocking#classes', + }, + ], }, { text: 'Parallelism', diff --git a/docs/guide/mocking.md b/docs/guide/mocking.md index e4e761843142..84540085f0dd 100644 --- a/docs/guide/mocking.md +++ b/docs/guide/mocking.md @@ -155,203 +155,7 @@ vi.stubGlobal('IntersectionObserver', IntersectionObserverMock) ## Modules -Mock modules observe third-party-libraries, that are invoked in some other code, allowing you to test arguments, output or even redeclare its implementation. - -See the [`vi.mock()` API section](/api/vi#vi-mock) for a more in-depth detailed API description. - -### Automocking Algorithm - -If your code is importing a mocked module, without any associated `__mocks__` file or `factory` for this module, Vitest will mock the module itself by invoking it and mocking every export. - -The following principles apply -* All arrays will be emptied -* All primitives and collections will stay the same -* All objects will be deeply cloned -* All instances of classes and their prototypes will be deeply cloned - -### Virtual Modules - -Vitest supports mocking Vite [virtual modules](https://vitejs.dev/guide/api-plugin.html#virtual-modules-convention). It works differently from how virtual modules are treated in Jest. Instead of passing down `virtual: true` to a `vi.mock` function, you need to tell Vite that module exists otherwise it will fail during parsing. You can do that in several ways: - -1. Provide an alias - -```ts [vitest.config.js] -import { defineConfig } from 'vitest/config' -import { resolve } from 'node:path' -export default defineConfig({ - test: { - alias: { - '$app/forms': resolve('./mocks/forms.js'), - }, - }, -}) -``` - -2. Provide a plugin that resolves a virtual module - -```ts [vitest.config.js] -import { defineConfig } from 'vitest/config' -export default defineConfig({ - plugins: [ - { - name: 'virtual-modules', - resolveId(id) { - if (id === '$app/forms') { - return 'virtual:$app/forms' - } - }, - }, - ], -}) -``` - -The benefit of the second approach is that you can dynamically create different virtual entrypoints. If you redirect several virtual modules into a single file, then all of them will be affected by `vi.mock`, so make sure to use unique identifiers. - -### Mocking Pitfalls - -Beware that it is not possible to mock calls to methods that are called inside other methods of the same file. For example, in this code: - -```ts [foobar.js] -export function foo() { - return 'foo' -} - -export function foobar() { - return `${foo()}bar` -} -``` - -It is not possible to mock the `foo` method from the outside because it is referenced directly. So this code will have no effect on the `foo` call inside `foobar` (but it will affect the `foo` call in other modules): - -```ts [foobar.test.ts] -import { vi } from 'vitest' -import * as mod from './foobar.js' - -// this will only affect "foo" outside of the original module -vi.spyOn(mod, 'foo') -vi.mock('./foobar.js', async (importOriginal) => { - return { - ...await importOriginal(), - // this will only affect "foo" outside of the original module - foo: () => 'mocked' - } -}) -``` - -You can confirm this behaviour by providing the implementation to the `foobar` method directly: - -```ts [foobar.test.js] -import * as mod from './foobar.js' - -vi.spyOn(mod, 'foo') - -// exported foo references mocked method -mod.foobar(mod.foo) -``` - -```ts [foobar.js] -export function foo() { - return 'foo' -} - -export function foobar(injectedFoo) { - return injectedFoo === foo // false -} -``` - -This is the intended behaviour. It is usually a sign of bad code when mocking is involved in such a manner. Consider refactoring your code into multiple files or improving your application architecture by using techniques such as [dependency injection](https://en.wikipedia.org/wiki/Dependency_injection). - -### Example - -```js -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { Client } from 'pg' -import { failure, success } from './handlers.js' - -// get todos -export async function getTodos(event, context) { - const client = new Client({ - // ...clientOptions - }) - - await client.connect() - - try { - const result = await client.query('SELECT * FROM todos;') - - client.end() - - return success({ - message: `${result.rowCount} item(s) returned`, - data: result.rows, - status: true, - }) - } - catch (e) { - console.error(e.stack) - - client.end() - - return failure({ message: e, status: false }) - } -} - -vi.mock('pg', () => { - const Client = vi.fn() - Client.prototype.connect = vi.fn() - Client.prototype.query = vi.fn() - Client.prototype.end = vi.fn() - - return { Client } -}) - -vi.mock('./handlers.js', () => { - return { - success: vi.fn(), - failure: vi.fn(), - } -}) - -describe('get a list of todo items', () => { - let client - - beforeEach(() => { - client = new Client() - }) - - afterEach(() => { - vi.clearAllMocks() - }) - - it('should return items successfully', async () => { - client.query.mockResolvedValueOnce({ rows: [], rowCount: 0 }) - - await getTodos() - - expect(client.connect).toBeCalledTimes(1) - expect(client.query).toBeCalledWith('SELECT * FROM todos;') - expect(client.end).toBeCalledTimes(1) - - expect(success).toBeCalledWith({ - message: '0 item(s) returned', - data: [], - status: true, - }) - }) - - it('should throw an error', async () => { - const mError = new Error('Unable to retrieve rows') - client.query.mockRejectedValueOnce(mError) - - await getTodos() - - expect(client.connect).toBeCalledTimes(1) - expect(client.query).toBeCalledWith('SELECT * FROM todos;') - expect(client.end).toBeCalledTimes(1) - expect(failure).toBeCalledWith({ message: mError, status: false }) - }) -}) -``` +See ["Mocking Modules" guide](/guide/mocking-modules). ## File System @@ -594,7 +398,7 @@ describe('delayed execution', () => { ## Classes -You can mock an entire class with a single `vi.fn` call - since all classes are also functions, this works out of the box. Beware that currently Vitest doesn't respect the `new` keyword so the `new.target` is always `undefined` in the body of a function. +You can mock an entire class with a single `vi.fn` call. ```ts class Dog { @@ -621,23 +425,20 @@ class Dog { } ``` -We can re-create this class with ES5 functions: +We can re-create this class with `vi.fn` (or `vi.spyOn().mockImplementation()`): ```ts -const Dog = vi.fn(function (name) { - this.name = name - // mock instance methods in the constructor, each instance will have its own spy - this.greet = vi.fn(() => `Hi! My name is ${this.name}!`) -}) +const Dog = vi.fn(class { + static getType = vi.fn(() => 'mocked animal') -// notice that static methods are mocked directly on the function, -// not on the instance of the class -Dog.getType = vi.fn(() => 'mocked animal') + constructor(name) { + this.name = name + } -// mock the "speak" and "feed" methods on every instance of a class -// all `new Dog()` instances will inherit and share these spies -Dog.prototype.speak = vi.fn(() => 'loud bark!') -Dog.prototype.feed = vi.fn() + greet = vi.fn(() => `Hi! My name is ${this.name}!`) + speak = vi.fn(() => 'loud bark!') + feed = vi.fn() +}) ``` ::: warning @@ -658,6 +459,8 @@ const Newt = new IncorrectDogClass('Newt') Marti instanceof CorrectDogClass // ✅ true Newt instanceof IncorrectDogClass // ❌ false! ``` + +If you are mocking classes, prefer the class syntax over the function. ::: ::: tip WHEN TO USE? @@ -667,9 +470,10 @@ Generally speaking, you would re-create a class like this inside the module fact import { Dog } from './dog.js' vi.mock(import('./dog.js'), () => { - const Dog = vi.fn() - Dog.prototype.feed = vi.fn() - // ... other mocks + const Dog = vi.fn(class { + feed = vi.fn() + // ... other mocks + }) return { Dog } }) ``` @@ -685,8 +489,9 @@ function feed(dog: Dog) { import { expect, test, vi } from 'vitest' import { feed } from '../src/feed.js' -const Dog = vi.fn() -Dog.prototype.feed = vi.fn() +const Dog = vi.fn(class { + feed = vi.fn() +}) test('can feed dogs', () => { const dogMax = new Dog('Max') @@ -712,8 +517,8 @@ expect(Cooper.greet).toHaveBeenCalled() const Max = new Dog('Max') -// methods assigned to the prototype are shared between instances -expect(Max.speak).toHaveBeenCalled() +// methods are not shared between instances if you assigned them directly +expect(Max.speak).not.toHaveBeenCalled() expect(Max.greet).not.toHaveBeenCalled() ``` @@ -724,7 +529,7 @@ const dog = new Dog('Cooper') // "vi.mocked" is a type helper, since // TypeScript doesn't know that Dog is a mocked class, -// it wraps any function in a MockInstance type +// it wraps any function in a Mock type // without validating if the function is a mock vi.mocked(dog.speak).mockReturnValue('woof woof') @@ -746,6 +551,10 @@ expect(nameSpy).toHaveBeenCalledTimes(1) You can also spy on getters and setters using the same method. ::: +::: danger +Using classes with `vi.fn()` was introduced in Vitest 4. Previously, you had to use `function` and `prototype` inheritence directly. See [v3 guide](https://v3.vitest.dev/guide/mocking.html#classes). +::: + ## Cheat Sheet :::info diff --git a/docs/guide/module-mocking.md b/docs/guide/module-mocking.md new file mode 100644 index 000000000000..d306c771a4f7 --- /dev/null +++ b/docs/guide/module-mocking.md @@ -0,0 +1,355 @@ +# Mocking Modules + +## Defining a Module + +Before mocking a "module", we should define what it is. In Vitest context the "module" is a file that exports something. Using [plugins](https://vite.dev/guide/api-plugin.html), any file can be turned into a JavaScript module. The "module object" is a namespace object that holds dynamic references to exported identifiers. Simply put, it's an object with exported methods and properties. In this example, `example.js` is a module that exports `method` and `variable`: + +```js [example.js] +export function answer() { + // ... + return 42 +} + +export const variable = 'example' +``` + +The `exampleObject` here is a module object: + +```js [example.test.js] +import * as exampleObject from './example.js' +``` + +The `exampleObject` will always exist even if you imported the example using named imports: + +```js [example.test.js] +import { answer, variable } from './example.js' +``` + +You can only reference `exampleObject` outside the example module itself. For example, in a test. + +## Mocking a Module + +For the purpose of this guide, let's introduce some definitions. + +- **Mocked module** is a module that was completely replaced with another one. +- **Spied module** is mocked module, but its exported methods keep the original implementation. They can also be tracked. +- **Mocked export** is a module export, which invocations can be tracked. +- **Spied export** is a mocked export. + +To mock a module completely, you can use the [`vi.mock` API](/api/vi#vi-mock). You can define a new module dynamicaly by providing a factory that returns a new module as a second argument: + +```ts +import { vi } from 'vitest' + +// The ./example.js module will be replaced with +// the result of a factory function, and the +// original ./example.js module will never be called +vi.mock(import('./example.js'), () => { + return { + answer() { + // ... + return 42 + }, + variable: 'mock', + } +}) +``` + +::: tip +Remember that you can call `vi.mock` in a [setup file](/config/#setupfiles) to apply the module mock in every test file automatically. +::: + +::: tip +Note the usage of dynamic import: `import('./example.ts')`. Vitest will strip it before the code is executed, but it allows TypeScript to properly validate the string and type the `importOriginal` method in your IDE or CLI. +::: + +If your code is trying to access a method that was not returned from this factory, Vitest will throw an error with a helpful message. Note that `answer` is not mocked, i.e. it cannot be tracked. To make it trackable, use `vi.fn()` instead: + +```ts +import { vi } from 'vitest' + +vi.mock(import('./example.js'), () => { + return { + answer: vi.fn(), + variable: 'mock', + } +}) +``` + +The factory method accepts an `importOriginal` function that will execute the original module and return its module object: + +```ts +import { expect, vi } from 'vitest' +import { answer } from './example.js' + +vi.mock(import('./example.js'), async (importOriginal) => { + const originalModule = await importOriginal() + return { + answer: vi.fn(originalModule.answer), + variable: 'mock', + } +}) + +expect(answer()).toBe(42) + +expect(answer).toHaveBeenCalled() +expect(answer).toHaveReturned(42) +``` + +::: warning +Note that `importOriginal` is asynchronous and needs to be awaited. +::: + +In the above example we provided the original `answer` to the `vi.fn()` call so it can keep calling it while being tracked at the same time. + +If you require the use of `importOriginal`, consider spying on the export directly via another API: `vi.spyOn`. Instead of replacing the whole module, you can spy only on a single exported method. To do that, you need to import the module as a namespace object: + +```ts +import { expect, vi } from 'vitest' +import * as exampleObject from './example.js' + +const spy = vi.spyOn(exampleObject, 'answer').mockReturnValue(0) + +expect(example.answer()).toBe(0) +expect(example.answer).toHaveBeenCalled() +``` + +::: danger Browser Mode Support +This will not work in the [Browser Mode](/guide/browser) because it uses the browser's native ESM support to serve modules. The module namespace object is sealed and can't be reconfigured. To bypass this limitation, Vitest supports `{ spy: true }` option in `vi.mock('./example.js')`. This will automatically spy on every export in the module without replacing them with fake ones. + +```ts +import { vi } from 'vitest' +import * as exampleObject from './example.js' + +vi.mock('./example.js', { spy: true }) + +vi.mocked(exampleObject.answer).mockReturnValue(0) +``` +::: + +::: warning +You only need to import the module as a namespace object in the file where you are using the `vi.spyOn` utility. If the `answer` is called in another file and is imported there as a named export, Vitest will be able to properly track it as long as the function that called it is called after `vi.spyOn`: + +```ts [source.js] +import { answer } from './example.js' + +export function question() { + if (answer() === 42) { + return 'Ultimate Question of Life, the Universe, and Everything' + } + + return 'Unknown Question' +} +``` +::: + +Note that `vi.spyOn` will only spy on calls that were done after it spied on the method. So, if the function is executed at the top level during an import or it was called before the spying, `vi.spyOn` will not be able to report on it. + +To automatically mock any module before it is imported, you can call `vi.mock` with a path: + +```ts +import { vi } from 'vitest' + +vi.mock(import('./example.js')) +``` + +If the file `./__mocks__/example.js` exists, then Vitest will load it instead. Otherwise, Vitest will load the original module and replace everything recursively: + +- All arrays will be empty +- All primitives will stay untouched +- All getters will return `undefined` +- All methods will return `undefined` +- All objects will be deeply cloned +- All instances of classes and their prototypes will be cloned + +To disable this behaviour, you can pass down `spy: true` as the second argument: + +```ts +import { vi } from 'vitest' + +vi.mock(import('./example.js'), { spy: true }) +``` + +Instead of returning `undefined`, all methods will call the original implementation, but you can still keep track of these calls: + +```ts +import { expect, vi } from 'vitest' +import { answer } from './example.js' + +vi.mock(import('./example.js'), { spy: true }) + +// calls the original implementation +expect(answer()).toBe(42) +// vitest can still track the invocations +expect(answer).toHaveBeenCalled() +``` + +One nice thing that mocked modules support is sharing the state between the instance and its prototype. Consider this module: + +```ts [answer.js] +export class Answer { + constructor(value) { + this._value = value + } + + value() { + return this._value + } +} +``` + +By mocking it, we can keep track of every invokation of `.value()` even without having the access to the instance itself: + +```ts [answer.test.js] +import { expect, test, vi } from 'vitest' +import { Answer } from './answer.js' + +vi.mock(import('./answer.js'), { spy: true }) + +test('instance inherits the state', () => { + // these invocations could be private inside another function + // that you don't have access to in your test + const answer1 = new Answer(42) + const answer2 = new Answer(0) + + expect(answer1.value()).toBe(42) + expect(answer1.value).toHaveBeenCalled() + // note that different instances have their own states + expect(answer2.value).not.toHaveBeenCalled() + + expect(answer2.value()).toBe(0) + + // but the prototype state accumulates all calls + expect(Answer.prototype.value).toHaveBeenCalledTimes(2) + expect(Answer.prototype.value).toHaveReturned(42) + expect(Answer.prototype.value).toHaveReturned(0) +}) +``` + +This can be very useful to track calls to instances that are never exposed. + +## How it Works + +Vitest implements different module mocking mechanism depending on the environment. The only feature they share is the plugin transformer. When Vitest sees that a file has `vi.mock` inside, it will transform every static import into a dynamic one and move the `vi.mock` call to the top of the file. This allows Vitest to register the mock before the import happens without breaking the ESM rule of hoisted imports. + +::: code-group +```ts [example.js] +import { answer } from './answer.js' + +vi.mock(import('./answer.js')) + +console.log(answer) +``` +```ts [example.transformed.js] +vi.mock('./answer.js') + +const __vitest_module_0__ = await __handle_mock__( + () => import('./answer.js') +) +// to keep the live binding, we have to access +// the export on the module namespace +console.log(__vitest_module_0__.answer()) +``` +::: + +The `__handle_mock__` wrapper just makes sure the mock is resolved before the import is initiated, it doesn't modify the module in any way. + +The module mocking plugins are available in the [`@vitest/mocker` package](https://github.com/vitest-dev/vitest/tree/main/packages/mocker). + +### JSDOM, happy-dom, Node + +When you run your tests in an emulated environment, Vitest creates a [module runner](https://vite.dev/guide/api-environment-runtimes.html#modulerunner) that can consume Vite code. The module runner is designed in such a way that Vitest can hook into the module evaluation and replace it with the mock, if it was registered. This means that Vitest runs your code in a ESM-like environment, but it doesn't use native ESM mechanism directly. This allows the test runner to bend the rules around ES Modules mutability, allowing users to call `vi.spyOn` on a seemingly ES Module. + +### Browser Mode + +Vitest uses native ESM in the Browser Mode. This means that we cannot replace the module so easily. Instead, Vitest intercepts the fetch request (via playwright's `page.route` or a Vite plugin API if using `preview` or `webdriverio`) and serves transformed code, if the module was mocked. + +For example, if the module is automocked, Vitest can parse static exports and create a placeholder module: + +::: code-group +```ts [answer.js] +export function answer() { + return 42 +} +``` +```ts [answer.transformed.js] +function answer() { + return 42 +} + +const __private_module__ = { + [Symbol.toStringTag]: 'Module', + answer: vi.fn(answer), +} + +export const answer = __private_module__.answer +``` +::: + +The example is simplified for brevity, but the concept is unchanged. We can inject a `__private_module__` variable into the module to hold the mocked values. If the user called `vi.mock` with `spy: true`, we pass down the original value; otherwise, we create a simple `vi.fn()` mock. + +If user defined a custom factory, this makes it harder to inject the code, but not impossible. When the mocked file is served, we first resolve the factory in the browser, then pass down the keys back to the server, and use them to create a placeholder module: + +```ts +const resolvedFactoryKeys = await resolveBrowserFactory(url) +const mockedModule = ` +const __private_module__ = getFactoryReturnValue(${url}) +${resolvedFactoryKeys.map(key => `export const ${key} = __private_module__["${key}""]`).join('\n')} +` +``` + +This module can now be served back to the browser. You can inspect the code in the devtools, when you run the tests. + +## Mocking Modules Pitfalls + +Beware that it is not possible to mock calls to methods that are called inside other methods of the same file. For example, in this code: + +```ts [foobar.js] +export function foo() { + return 'foo' +} + +export function foobar() { + return `${foo()}bar` +} +``` + +It is not possible to mock the `foo` method from the outside because it is referenced directly. So this code will have no effect on the `foo` call inside `foobar` (but it will affect the `foo` call in other modules): + +```ts [foobar.test.ts] +import { vi } from 'vitest' +import * as mod from './foobar.js' + +// this will only affect "foo" outside of the original module +vi.spyOn(mod, 'foo') +vi.mock(import('./foobar.js'), async (importOriginal) => { + return { + ...await importOriginal(), + // this will only affect "foo" outside of the original module + foo: () => 'mocked' + } +}) +``` + +You can confirm this behaviour by providing the implementation to the `foobar` method directly: + +```ts [foobar.test.js] +import * as mod from './foobar.js' + +vi.spyOn(mod, 'foo') + +// exported foo references mocked method +mod.foobar(mod.foo) +``` + +```ts [foobar.js] +export function foo() { + return 'foo' +} + +export function foobar(injectedFoo) { + return injectedFoo === foo // false +} +``` + +This is the intended behaviour, and we do not plan to implement a workaround. Consider refactoring your code into multiple files or use techniques such as [dependency injection](https://en.wikipedia.org/wiki/Dependency_injection). We believe that making the application testable is not the responsibility of the test runner, but of the application architecture. From b6932dfac9fbd959d70ea8327966edf93cacc588 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 31 Jul 2025 18:59:51 +0200 Subject: [PATCH 18/29] fix: don't empty array if `spy` is set to `true` --- packages/mocker/src/automocker.ts | 55 +++++++++++++------- test/core/test/mocking/vi-mockObject.test.ts | 37 +++++++++++++ 2 files changed, 74 insertions(+), 18 deletions(-) diff --git a/packages/mocker/src/automocker.ts b/packages/mocker/src/automocker.ts index 86903f5d93a7..c90f3b274859 100644 --- a/packages/mocker/src/automocker.ts +++ b/packages/mocker/src/automocker.ts @@ -31,6 +31,24 @@ export function mockObject( } } + const createMock = (currentValue: (...args: any[]) => any) => { + if (!options.createMockInstance) { + throw new Error( + '[@vitest/mocker] `createMockInstance` is not defined. This is a Vitest error. Please open a new issue with reproduction.', + ) + } + const createMockInstance = options.createMockInstance + const prototypeMembers = currentValue.prototype + ? collectFunctionProperties(currentValue.prototype) + : [] + return createMockInstance({ + name: currentValue.name, + prototypeMembers, + originalImplementation: options.type === 'autospy' ? currentValue : undefined, + keepMembersImplementation: options.type === 'autospy', + }) + } + const mockPropertiesOf = ( container: Record, newContainer: Record, @@ -85,7 +103,23 @@ export function mockObject( const type = getType(value) if (Array.isArray(value)) { - define(newContainer, property, []) + if (options.type === 'automock') { + define(newContainer, property, []) + } + else { + const array = value.map((value) => { + if (value && typeof value === 'object') { + const newObject = {} + mockPropertiesOf(value, newObject) + return newObject + } + if (typeof value === 'function') { + return createMock(value) + } + return value + }) + define(newContainer, property, array) + } continue } @@ -102,27 +136,12 @@ export function mockObject( // Sometimes this assignment fails for some unknown reason. If it does, // just move along. - if (!define(newContainer, property, isFunction ? value : {})) { + if (!define(newContainer, property, isFunction || options.type === 'autospy' ? value : {})) { continue } if (isFunction) { - if (!options.createMockInstance) { - throw new Error( - '[@vitest/mocker] `createMockInstance` is not defined. This is a Vitest error. Please open a new issue with reproduction.', - ) - } - 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', - }) + const mock = createMock(newContainer[property]) newContainer[property] = mock } diff --git a/test/core/test/mocking/vi-mockObject.test.ts b/test/core/test/mocking/vi-mockObject.test.ts index b81dc0439023..daa4780d8de6 100644 --- a/test/core/test/mocking/vi-mockObject.test.ts +++ b/test/core/test/mocking/vi-mockObject.test.ts @@ -183,6 +183,43 @@ test('vi.mockReset() does not break inherited properties', () => { expect(instance3.method).toHaveBeenCalledTimes(0) }) +test('the array is empty by default', () => { + const { array } = vi.mockObject({ + array: [1, 2, 3], + }) + expect(array).toEqual([]) +}) + +test('the array is not empty when spying', () => { + const { array } = vi.mockObject( + { + array: [ + 1, + 'text', + () => 42, + { + property: 'static', + answer() { + return 42 + }, + array: [1], + }, + ] as const, + }, + { spy: true }, + ) + + expect(array).toHaveLength(4) + expect(array[0]).toBe(1) + expect(array[1]).toBe('text') + expect(vi.isMockFunction(array[2])).toBe(true) + expect(array[2]()).toBe(42) + expect(array[3].property).toBe('static') + expect(array[3].answer()).toBe(42) + expect(array[3].answer).toHaveBeenCalled() + expect(array[3].array).toEqual([1]) +}) + function mockModule(type: 'automock' | 'autospy' = 'automock') { return vi.mockObject({ [Symbol.toStringTag]: 'Module', From 46b484655c0f63f8ca4c429a6492ed8d9bd66264 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 31 Jul 2025 19:36:12 +0200 Subject: [PATCH 19/29] chore: fix links --- docs/guide/mocking.md | 2 +- docs/guide/module-mocking.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/guide/mocking.md b/docs/guide/mocking.md index 84540085f0dd..0f0584d2f461 100644 --- a/docs/guide/mocking.md +++ b/docs/guide/mocking.md @@ -155,7 +155,7 @@ vi.stubGlobal('IntersectionObserver', IntersectionObserverMock) ## Modules -See ["Mocking Modules" guide](/guide/mocking-modules). +See ["Mocking Modules" guide](/guide/module-mocking). ## File System diff --git a/docs/guide/module-mocking.md b/docs/guide/module-mocking.md index d306c771a4f7..51cf64e87cc1 100644 --- a/docs/guide/module-mocking.md +++ b/docs/guide/module-mocking.md @@ -115,7 +115,7 @@ expect(example.answer).toHaveBeenCalled() ``` ::: danger Browser Mode Support -This will not work in the [Browser Mode](/guide/browser) because it uses the browser's native ESM support to serve modules. The module namespace object is sealed and can't be reconfigured. To bypass this limitation, Vitest supports `{ spy: true }` option in `vi.mock('./example.js')`. This will automatically spy on every export in the module without replacing them with fake ones. +This will not work in the [Browser Mode](/guide/browser/) because it uses the browser's native ESM support to serve modules. The module namespace object is sealed and can't be reconfigured. To bypass this limitation, Vitest supports `{ spy: true }` option in `vi.mock('./example.js')`. This will automatically spy on every export in the module without replacing them with fake ones. ```ts import { vi } from 'vitest' From 93cb84c31c281d765125260ec0af69edd6e3604b Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 31 Jul 2025 19:37:50 +0200 Subject: [PATCH 20/29] docs: cleanup --- docs/.vitepress/config.ts | 2 +- docs/guide/{module-mocking.md => mocking-modules.md} | 0 docs/guide/mocking.md | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename docs/guide/{module-mocking.md => mocking-modules.md} (100%) diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index bc83bc0fb72f..4badec74446d 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -518,7 +518,7 @@ function guide(): DefaultTheme.SidebarItem[] { }, { text: 'Mocking Modules', - link: '/guide/module-mocking', + link: '/guide/mocking-modules', }, { text: 'Mocking File System', diff --git a/docs/guide/module-mocking.md b/docs/guide/mocking-modules.md similarity index 100% rename from docs/guide/module-mocking.md rename to docs/guide/mocking-modules.md diff --git a/docs/guide/mocking.md b/docs/guide/mocking.md index 0f0584d2f461..84540085f0dd 100644 --- a/docs/guide/mocking.md +++ b/docs/guide/mocking.md @@ -155,7 +155,7 @@ vi.stubGlobal('IntersectionObserver', IntersectionObserverMock) ## Modules -See ["Mocking Modules" guide](/guide/module-mocking). +See ["Mocking Modules" guide](/guide/mocking-modules). ## File System From c1e22528c9b7200082456419d11efc9c0cd15685 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 31 Jul 2025 19:41:01 +0200 Subject: [PATCH 21/29] docs: cleanup --- docs/guide/mocking-modules.md | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/guide/mocking-modules.md b/docs/guide/mocking-modules.md index 51cf64e87cc1..523cdd91d8e6 100644 --- a/docs/guide/mocking-modules.md +++ b/docs/guide/mocking-modules.md @@ -2,7 +2,7 @@ ## Defining a Module -Before mocking a "module", we should define what it is. In Vitest context the "module" is a file that exports something. Using [plugins](https://vite.dev/guide/api-plugin.html), any file can be turned into a JavaScript module. The "module object" is a namespace object that holds dynamic references to exported identifiers. Simply put, it's an object with exported methods and properties. In this example, `example.js` is a module that exports `method` and `variable`: +Before mocking a "module", we should define what it is. In Vitest context, the "module" is a file that exports something. Using [plugins](https://vite.dev/guide/api-plugin.html), any file can be turned into a JavaScript module. The "module object" is a namespace object that holds dynamic references to exported identifiers. Simply put, it's an object with exported methods and properties. In this example, `example.js` is a module that exports `method` and `variable`: ```js [example.js] export function answer() { @@ -32,11 +32,11 @@ You can only reference `exampleObject` outside the example module itself. For ex For the purpose of this guide, let's introduce some definitions. - **Mocked module** is a module that was completely replaced with another one. -- **Spied module** is mocked module, but its exported methods keep the original implementation. They can also be tracked. +- **Spied module** is a mocked module, but its exported methods keep the original implementation. They can also be tracked. - **Mocked export** is a module export, which invocations can be tracked. - **Spied export** is a mocked export. -To mock a module completely, you can use the [`vi.mock` API](/api/vi#vi-mock). You can define a new module dynamicaly by providing a factory that returns a new module as a second argument: +To mock a module completely, you can use the [`vi.mock` API](/api/vi#vi-mock). You can define a new module dynamically by providing a factory that returns a new module as a second argument: ```ts import { vi } from 'vitest' @@ -100,7 +100,7 @@ expect(answer).toHaveReturned(42) Note that `importOriginal` is asynchronous and needs to be awaited. ::: -In the above example we provided the original `answer` to the `vi.fn()` call so it can keep calling it while being tracked at the same time. +In the above example, we provided the original `answer` to the `vi.fn()` call so it can keep calling it while being tracked at the same time. If you require the use of `importOriginal`, consider spying on the export directly via another API: `vi.spyOn`. Instead of replacing the whole module, you can spy only on a single exported method. To do that, you need to import the module as a namespace object: @@ -110,8 +110,8 @@ import * as exampleObject from './example.js' const spy = vi.spyOn(exampleObject, 'answer').mockReturnValue(0) -expect(example.answer()).toBe(0) -expect(example.answer).toHaveBeenCalled() +expect(exampleObject.answer()).toBe(0) +expect(exampleObject.answer).toHaveBeenCalled() ``` ::: danger Browser Mode Support @@ -162,7 +162,7 @@ If the file `./__mocks__/example.js` exists, then Vitest will load it instead. O - All objects will be deeply cloned - All instances of classes and their prototypes will be cloned -To disable this behaviour, you can pass down `spy: true` as the second argument: +To disable this behavior, you can pass down `spy: true` as the second argument: ```ts import { vi } from 'vitest' @@ -198,7 +198,7 @@ export class Answer { } ``` -By mocking it, we can keep track of every invokation of `.value()` even without having the access to the instance itself: +By mocking it, we can keep track of every invocation of `.value()` even without having access to the instance itself: ```ts [answer.test.js] import { expect, test, vi } from 'vitest' @@ -230,7 +230,7 @@ This can be very useful to track calls to instances that are never exposed. ## How it Works -Vitest implements different module mocking mechanism depending on the environment. The only feature they share is the plugin transformer. When Vitest sees that a file has `vi.mock` inside, it will transform every static import into a dynamic one and move the `vi.mock` call to the top of the file. This allows Vitest to register the mock before the import happens without breaking the ESM rule of hoisted imports. +Vitest implements different module mocking mechanisms depending on the environment. The only feature they share is the plugin transformer. When Vitest sees that a file has `vi.mock` inside, it will transform every static import into a dynamic one and move the `vi.mock` call to the top of the file. This allows Vitest to register the mock before the import happens without breaking the ESM rule of hoisted imports. ::: code-group ```ts [example.js] @@ -258,7 +258,7 @@ The module mocking plugins are available in the [`@vitest/mocker` package](https ### JSDOM, happy-dom, Node -When you run your tests in an emulated environment, Vitest creates a [module runner](https://vite.dev/guide/api-environment-runtimes.html#modulerunner) that can consume Vite code. The module runner is designed in such a way that Vitest can hook into the module evaluation and replace it with the mock, if it was registered. This means that Vitest runs your code in a ESM-like environment, but it doesn't use native ESM mechanism directly. This allows the test runner to bend the rules around ES Modules mutability, allowing users to call `vi.spyOn` on a seemingly ES Module. +When you run your tests in an emulated environment, Vitest creates a [module runner](https://vite.dev/guide/api-environment-runtimes.html#modulerunner) that can consume Vite code. The module runner is designed in such a way that Vitest can hook into the module evaluation and replace it with the mock, if it was registered. This means that Vitest runs your code in an ESM-like environment, but it doesn't use native ESM mechanism directly. This allows the test runner to bend the rules around ES Modules mutability, allowing users to call `vi.spyOn` on a seemingly ES Module. ### Browser Mode @@ -294,11 +294,11 @@ If user defined a custom factory, this makes it harder to inject the code, but n const resolvedFactoryKeys = await resolveBrowserFactory(url) const mockedModule = ` const __private_module__ = getFactoryReturnValue(${url}) -${resolvedFactoryKeys.map(key => `export const ${key} = __private_module__["${key}""]`).join('\n')} +${resolvedFactoryKeys.map(key => `export const ${key} = __private_module__["${key}"]`).join('\n')} ` ``` -This module can now be served back to the browser. You can inspect the code in the devtools, when you run the tests. +This module can now be served back to the browser. You can inspect the code in the devtools when you run the tests. ## Mocking Modules Pitfalls @@ -331,7 +331,7 @@ vi.mock(import('./foobar.js'), async (importOriginal) => { }) ``` -You can confirm this behaviour by providing the implementation to the `foobar` method directly: +You can confirm this behavior by providing the implementation to the `foobar` method directly: ```ts [foobar.test.js] import * as mod from './foobar.js' @@ -352,4 +352,4 @@ export function foobar(injectedFoo) { } ``` -This is the intended behaviour, and we do not plan to implement a workaround. Consider refactoring your code into multiple files or use techniques such as [dependency injection](https://en.wikipedia.org/wiki/Dependency_injection). We believe that making the application testable is not the responsibility of the test runner, but of the application architecture. +This is the intended behavior, and we do not plan to implement a workaround. Consider refactoring your code into multiple files or use techniques such as [dependency injection](https://en.wikipedia.org/wiki/Dependency_injection). We believe that making the application testable is not the responsibility of the test runner, but of the application architecture. From 1cb2d0d5efde7d5f891e82c6bef844434fcebaa2 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 1 Aug 2025 14:17:33 +0200 Subject: [PATCH 22/29] chore: add a log for console.warn --- packages/spy/src/index.ts | 3 +++ test/core/test/mocking/vi-fn.test.ts | 4 ++++ test/core/vite.config.ts | 3 +++ 3 files changed, 10 insertions(+) diff --git a/packages/spy/src/index.ts b/packages/spy/src/index.ts index 3a5aa5b3e02d..a17ad7355cbb 100644 --- a/packages/spy/src/index.ts +++ b/packages/spy/src/index.ts @@ -428,6 +428,9 @@ function createMock( catch (error: any) { thrownValue = error didThrow = true + if (error instanceof TypeError && error.message.includes('is not a constructor')) { + console.warn(`[vitest] The ${namedObject[name].getMockName()} mock did not use 'function' or 'class' in its implementation, see https://vitest.dev/api/vi#vi-spyon for examples.`) + } throw error } finally { diff --git a/test/core/test/mocking/vi-fn.test.ts b/test/core/test/mocking/vi-fn.test.ts index 98a7ccb77db7..002d7d8a22cc 100644 --- a/test/core/test/mocking/vi-fn.test.ts +++ b/test/core/test/mocking/vi-fn.test.ts @@ -593,8 +593,12 @@ describe('vi.fn() implementations', () => { }) test('vi.fn() throws an error if new is called on arrow function', () => { + using log = vi.spyOn(console, 'warn') const Mock = vi.fn(() => {}) expect(() => new Mock()).toThrowError() + expect(log).toHaveBeenCalledWith( + `[vitest] The vi.fn() mock did not use 'function' or 'class' in its implementation, see https://vitest.dev/api/vi#vi-spyon for examples.`, + ) }) test('vi.fn() throws an error if new is not called on a class', () => { diff --git a/test/core/vite.config.ts b/test/core/vite.config.ts index a8c5cdc4f110..d1bc207d216d 100644 --- a/test/core/vite.config.ts +++ b/test/core/vite.config.ts @@ -153,6 +153,9 @@ export default defineConfig({ if (log.includes('run [...filters]')) { return false } + if (log.startsWith(`[vitest]`) && log.includes(`did not use 'function' or 'class' in its implementation`)) { + return false + } }, projects: [ project('threads', 'red'), From 4bb8b5e0cf1fe74834db233d84160f788ddb02c3 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 1 Aug 2025 14:31:50 +0200 Subject: [PATCH 23/29] docs: add virtual modules --- docs/guide/mocking-modules.md | 57 ++++++++++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/docs/guide/mocking-modules.md b/docs/guide/mocking-modules.md index 523cdd91d8e6..83b7d54b63a6 100644 --- a/docs/guide/mocking-modules.md +++ b/docs/guide/mocking-modules.md @@ -228,6 +228,61 @@ test('instance inherits the state', () => { This can be very useful to track calls to instances that are never exposed. +## Mocking Non-existing Module + +Vitest supports mocking virtual modules. These modules don't exist on the file system, but your code imports them. For example, this can happen when your development environment is different from production. One common example is mocking `vscode` APIs in your unit tests. + +By default, Vitest will fail transforming files if it cannot find the source of the import. To bypass this, you need to specify it in your config. You can either always redirect the import to a file, or just signal Vite to ignore it and use the `vi.mock` factory to define its exports. + +To redirect the import, use [`test.alias`](/config/#alias) config option: + +```ts [vitest.config.ts] +import { defineConfig } from 'vitest/config' +import { resolve } from 'node:path' + +export default defineConfig({ + test: { + alias: { + vscode: resolve(import.meta.dirname, './mock/vscode.js'), + }, + }, +}) +``` + +To mark the module as always resolved, return the same string from `resolveId` hook of a plugin: + +```ts [vitest.config.ts] +import { defineConfig } from 'vitest/config' +import { resolve } from 'node:path' + +export default defineConfig({ + plugins: [ + { + name: 'virtual-vscode', + resolveId(id) { + if (id === 'vscode') { + return 'vscode' + } + } + } + ] +}) +``` + +Now you can use `vi.mock` as usual in your tests: + +```ts +import { vi } from 'vitest' + +vi.mock(import('vscode'), () => { + return { + window: { + createOutputChannel: vi.fn(), + } + } +}) +``` + ## How it Works Vitest implements different module mocking mechanisms depending on the environment. The only feature they share is the plugin transformer. When Vitest sees that a file has `vi.mock` inside, it will transform every static import into a dynamic one and move the `vi.mock` call to the top of the file. This allows Vitest to register the mock before the import happens without breaking the ESM rule of hoisted imports. @@ -258,7 +313,7 @@ The module mocking plugins are available in the [`@vitest/mocker` package](https ### JSDOM, happy-dom, Node -When you run your tests in an emulated environment, Vitest creates a [module runner](https://vite.dev/guide/api-environment-runtimes.html#modulerunner) that can consume Vite code. The module runner is designed in such a way that Vitest can hook into the module evaluation and replace it with the mock, if it was registered. This means that Vitest runs your code in an ESM-like environment, but it doesn't use native ESM mechanism directly. This allows the test runner to bend the rules around ES Modules mutability, allowing users to call `vi.spyOn` on a seemingly ES Module. +When you run your tests in an emulated environment, Vitest creates a [module runner](https://vite.dev/guide/api-environment-runtimes.html#modulerunner) that can consume Vite code. The module runner is designed in such a way that Vitest can hook into the module evaluation and replace it with the mock, if it was registered. This means that Vitest runs your code in an ESM-like environment, but it doesn't use native ESM mechanism directly. This allows the test runner to bend the rules around ES Modules immutability, allowing users to call `vi.spyOn` on a seemingly ES Module. ### Browser Mode From 35c3aefd20ac083eaf4c893d34840082cae51258 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 1 Aug 2025 15:27:52 +0200 Subject: [PATCH 24/29] docs: update mocking docs --- docs/.vitepress/config.ts | 2 +- docs/api/mock.md | 41 ++++--- docs/api/vi.md | 248 +++++++++++++++++++++++++++++++------- docs/config/index.md | 11 +- 4 files changed, 230 insertions(+), 72 deletions(-) diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 4badec74446d..27088f0f6d7a 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -621,7 +621,7 @@ function api(): DefaultTheme.SidebarItem[] { link: '/api/', }, { - text: 'Mock Functions', + text: 'Mocks', link: '/api/mock', }, { diff --git a/docs/api/mock.md b/docs/api/mock.md index b8f74fe4a97c..4f2775b9e011 100644 --- a/docs/api/mock.md +++ b/docs/api/mock.md @@ -1,6 +1,6 @@ -# Mock Functions +# Mocks -You can create a mock function to track its execution with `vi.fn` method. If you want to track a method on an already created object, you can use `vi.spyOn` method: +You can create a mock function or a class to track its execution with the `vi.fn` method. If you want to track a property on an already created object, you can use the `vi.spyOn` method: ```js import { vi } from 'vitest' @@ -18,7 +18,7 @@ market.getApples() getApplesSpy.mock.calls.length === 1 ``` -You should use mock assertions (e.g., [`toHaveBeenCalled`](/api/expect#tohavebeencalled)) on [`expect`](/api/expect) to assert mock result. This API reference describes available properties and methods to manipulate mock behavior. +You should use mock assertions (e.g., [`toHaveBeenCalled`](/api/expect#tohavebeencalled)) on [`expect`](/api/expect) to assert mock results. This API reference describes available properties and methods to manipulate mock behavior. ::: tip The custom function implementation in the types below is marked with a generic ``. @@ -30,7 +30,7 @@ The custom function implementation in the types below is marked with a generic ` function getMockImplementation(): T | undefined ``` -Returns current mock implementation if there is one. +Returns the current mock implementation if there is one. If the mock was created with [`vi.fn`](/api/vi#vi-fn), it will use the provided method as the mock implementation. @@ -42,7 +42,7 @@ If the mock was created with [`vi.spyOn`](/api/vi#vi-spyon), it will return `und function getMockName(): string ``` -Use it to return the name assigned to the mock with the `.mockName(name)` method. By default, it will return `vi.fn()`. +Use it to return the name assigned to the mock with the `.mockName(name)` method. By default, `vi.fn()` mocks will return `'vi.fn()'`, while spies created with `vi.spyOn` will keep the original name. ## mockClear @@ -180,7 +180,7 @@ Note that this method takes precedence over the [`mockImplementationOnce`](#mock function mockRejectedValue(value: unknown): Mock ``` -Accepts an error that will be rejected when async function is called. +Accepts an error that will be rejected when an async function is called. ```ts const asyncMock = vi.fn().mockRejectedValue(new Error('Async error')) @@ -212,11 +212,10 @@ await asyncMock() // throws Error<'Async error'> function mockReset(): Mock ``` -Does what [`mockClear`](#mockClear) does and resets inner implementation to the original function. -This also resets all "once" implementations. +Does what [`mockClear`](#mockClear) does and resets the mock implementation. This also resets all "once" implementations. -Note that resetting a mock from `vi.fn()` will set implementation to an empty function that returns `undefined`. -resetting a mock from `vi.fn(impl)` will restore implementation to `impl`. +Note that resetting a mock from `vi.fn()` will set the implementation to an empty function that returns `undefined`. +Resetting a mock from `vi.fn(impl)` will reset the implementation to `impl`. This is useful when you want to reset a mock to its original state. @@ -244,10 +243,9 @@ To automatically call this method before each test, enable the [`mockReset`](/co function mockRestore(): Mock ``` -Does what [`mockReset`](#mockReset) does and restores original descriptors of spied-on objects. +Does what [`mockReset`](#mockreset) does and restores the original descriptors of spied-on objects, if the mock was created with [`vi.spyOn`](/api/vi#vi-spyon). -Note that restoring a mock from `vi.fn()` will set implementation to an empty function that returns `undefined`. -Restoring a mock from `vi.fn(impl)` will restore implementation to `impl`. +`mockRestore` on a `vi.fn()` mock is identical to [`mockReset`](#mockreset). ```ts const person = { @@ -379,7 +377,7 @@ fn.mock.calls === [ const lastCall: Parameters | undefined ``` -This contains the arguments of the last call. If mock wasn't called, it will return `undefined`. +This contains the arguments of the last call. If the mock wasn't called, it will return `undefined`. ## mock.results @@ -388,7 +386,7 @@ interface MockResultReturn { type: 'return' /** * The value that was returned from the function. - * If function returned a Promise, then this will be a resolved value. + * If the function returned a Promise, then this will be a resolved value. */ value: T } @@ -418,6 +416,7 @@ This is an array containing all values that were `returned` from the function. O - `'return'` - function returned without throwing. - `'throw'` - function threw a value. +- `'incomplete'` - the function did not finish running yet. The `value` property contains the returned value or thrown error. If the function returned a `Promise`, then `result` will always be `'return'` even if the promise was rejected. @@ -473,9 +472,11 @@ export type MockSettledResult const settledResults: MockSettledResult>>[] ``` -An array containing all values that were `resolved` or `rejected` from the function. +An array containing all values that were resolved or rejected by the function. -This array will be empty if the function was never resolved or rejected. +If the function returned non-promise values, the `value` will be kept as is, but the `type` will still says `fulfilled` or `rejected`. + +Until the value is resolved or rejected, the `settledResult` type will be `incomplete`. ```js const fn = vi.fn().mockResolvedValueOnce('result') @@ -544,10 +545,10 @@ fn.mock.contexts[1] === context const instances: ReturnType[] ``` -This property is an array containing all instances that were created when the mock was called with the `new` keyword. Note that this is an actual context (`this`) of the function, not a return value. +This property is an array containing all instances that were created when the mock was called with the `new` keyword. Note that this is the actual context (`this`) of the function, not a return value. ::: warning -If mock was instantiated with `new MyClass()`, then `mock.instances` will be an array with one value: +If the mock was instantiated with `new MyClass()`, then `mock.instances` will be an array with one value: ```js const MyClass = vi.fn() @@ -556,7 +557,7 @@ const a = new MyClass() MyClass.mock.instances[0] === a ``` -If you return a value from constructor, it will not be in `instances` array, but instead inside `results`: +If you return a value from the constructor, it will not be in the `instances` array, but instead inside `results`: ```js const Spy = vi.fn(() => ({ method: vi.fn() })) diff --git a/docs/api/vi.md b/docs/api/vi.md index 76806d2ef87b..caa111e57b71 100644 --- a/docs/api/vi.md +++ b/docs/api/vi.md @@ -16,8 +16,24 @@ This section describes the API that you can use when [mocking a module](/guide/m ### vi.mock -- **Type**: `(path: string, factory?: MockOptions | ((importOriginal: () => unknown) => unknown)) => void` -- **Type**: `(path: Promise, factory?: MockOptions | ((importOriginal: () => T) => T | Promise)) => void` +```ts +interface MockOptions { + spy?: boolean +} + +interface MockFactory { + (importOriginal: () => T): unknown +} + +function mock( + path: string, + factory?: MockOptions | MockFactory +): void +function mock( + path: Promise, + factory?: MockOptions | MockFactory +): void +``` Substitutes all imported modules from provided `path` with another module. You can use configured Vite aliases inside a path. The call to `vi.mock` is hoisted, so it doesn't matter where you call it. It will always be executed before all imports. If you need to reference some variables outside of its scope, you can define them inside [`vi.hoisted`](#vi-hoisted) and reference them inside `vi.mock`. @@ -159,8 +175,16 @@ If there is no `__mocks__` folder or a factory provided, Vitest will import the ### vi.doMock -- **Type**: `(path: string, factory?: MockOptions | ((importOriginal: () => unknown) => unknown)) => void` -- **Type**: `(path: Promise, factory?: MockOptions | ((importOriginal: () => T) => T | Promise)) => void` +```ts +function doMock( + path: string, + factory?: MockOptions | MockFactory +): void +function doMock( + path: Promise, + factory?: MockOptions | MockFactory +): void +``` The same as [`vi.mock`](#vi-mock), but it's not hoisted to the top of the file, so you can reference variables in the global file scope. The next [dynamic import](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import) of the module will be mocked. @@ -207,8 +231,16 @@ test('importing the next module imports mocked one', async () => { ### vi.mocked -- **Type**: `(obj: T, deep?: boolean) => MaybeMockedDeep` -- **Type**: `(obj: T, options?: { partial?: boolean; deep?: boolean }) => MaybePartiallyMockedDeep` +```ts +function mocked( + object: T, + deep?: boolean +): MaybeMockedDeep +function mocked( + object: T, + options?: { partial?: boolean; deep?: boolean } +): MaybePartiallyMockedDeep +``` Type helper for TypeScript. Just returns the object that was passed. @@ -243,7 +275,9 @@ test('mock return value with only partially correct typing', async () => { ### vi.importActual -- **Type**: `(path: string) => Promise` +```ts +function importActual(path: string): Promise +``` Imports module, bypassing all checks if it should be mocked. Can be useful if you want to mock module partially. @@ -257,19 +291,25 @@ vi.mock('./example.js', async () => { ### vi.importMock -- **Type**: `(path: string) => Promise>` +```ts +function importMock(path: string): Promise> +``` Imports a module with all of its properties (including nested properties) mocked. Follows the same rules that [`vi.mock`](#vi-mock) does. For the rules applied, see [algorithm](/guide/mocking#automocking-algorithm). ### vi.unmock -- **Type**: `(path: string | Promise) => void` +```ts +function unmock(path: string | Promise): void +``` Removes module from the mocked registry. All calls to import will return the original module even if it was mocked before. This call is hoisted to the top of the file, so it will only unmock modules that were defined in `setupFiles`, for example. ### vi.doUnmock -- **Type**: `(path: string | Promise) => void` +```ts +function doUnmock(path: string | Promise): void +``` The same as [`vi.unmock`](#vi-unmock), but is not hoisted to the top of the file. The next import of the module will import the original module instead of the mock. This will not unmock previously imported modules. @@ -308,7 +348,9 @@ unmockedIncrement(30) === 31 ### vi.resetModules -- **Type**: `() => Vitest` +```ts +function resetModules(): Vitest +``` Resets modules registry by clearing the cache of all modules. This allows modules to be reevaluated when reimported. Top-level imports cannot be re-evaluated. Might be useful to isolate modules where local state conflicts between tests. @@ -339,6 +381,10 @@ Does not reset mocks registry. To clear mocks registry, use [`vi.unmock`](#vi-un ### vi.dynamicImportSettled +```ts +function dynamicImportSettled(): Promise +``` + Wait for all imports to load. Useful, if you have a synchronous call that starts importing a module that you cannot wait otherwise. ```ts @@ -370,7 +416,9 @@ This section describes how to work with [method mocks](/api/mock) and replace en ### vi.fn -- **Type:** `(fn?: Function) => Mock` +```ts +function fn(fn?: Procedure | Constructable): Mock +``` Creates a spy on a function, though can be initiated without one. Every time a function is invoked, it stores its call arguments, returns, and instances. Also, you can manipulate its behavior with [methods](/api/mock). If no function is given, mock will return `undefined`, when invoked. @@ -390,9 +438,22 @@ expect(res).toBe(5) expect(getApples).toHaveNthReturnedWith(2, 5) ``` +You can also pass down a class to `vi.fn`: + +```ts +const Cart = vi.fn(class { + get = () => 0 +}) + +const cart = new Cart() +expect(Cart).toHaveBeenCalled() +``` + ### vi.mockObject 3.2.0 -- **Type:** `(value: T) => MaybeMockedDeep` +```ts +function mockObject(value: T): MaybeMockedDeep +``` Deeply mocks properties and methods of a given object in the same way as `vi.mock()` mocks module exports. See [automocking](/guide/mocking.html#automocking-algorithm) for the detail. @@ -428,28 +489,55 @@ expect(spied.simple.mock.results[0]).toEqual({ type: 'return', value: 'value' }) ### vi.isMockFunction -- **Type:** `(fn: Function) => boolean` +```ts +function isMockFunction(fn: unknown): asserts fn is Mock +``` Checks that a given parameter is a mock function. If you are using TypeScript, it will also narrow down its type. ### vi.clearAllMocks +```ts +function clearAllMocks(): Vitest +``` + Calls [`.mockClear()`](/api/mock#mockclear) on all spies. This will clear mock history without affecting mock implementations. ### vi.resetAllMocks +```ts +function resetAllMocks(): Vitest +``` + Calls [`.mockReset()`](/api/mock#mockreset) on all spies. -This will clear mock history and reset each mock's implementation to its original. +This will clear mock history and reset each mock's implementation. ### vi.restoreAllMocks -Calls [`.mockRestore()`](/api/mock#mockrestore) on all spies. -This will clear mock history, restore all original mock implementations, and restore original descriptors of spied-on objects. +```ts +function restoreAllMocks(): Vitest +``` + +This restores all original implementations on spies created with [`vi.spyOn`](#vi-spyon). + +After the mock was restored, you can spy on it again. + +::: warning +This method also does not affect mocks created during [automocking](/guide/mocking-modules#mocking-a-module). + +Note that unlike [`mock.mockRestore`](/api/mock/#mockrestore), `vi.restoreAllMocks` will not clear mock history or reset the mock implementation +::: ### vi.spyOn -- **Type:** `(object: T, method: K, accessType?: 'get' | 'set') => MockInstance` +```ts +function spyOn( + object: T, + key: K, + accessor?: 'get' | 'set' +): Mock +``` Creates a spy on a method or getter/setter of an object similar to [`vi.fn()`](#vi-fn). It returns a [mock function](/api/mock). @@ -509,7 +597,7 @@ it('calls console.log', () => { ::: ::: tip -You can call [`vi.restoreAllMocks`](#vi-restoreallmocks) inside [`afterEach`](/api/#aftereach) (or enable [`test.restoreMocks`](/config/#restoreMocks)) to restore all methods to their original implementations. This will restore the original [object descriptor](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty), so you won't be able to change method's implementation: +You can call [`vi.restoreAllMocks`](#vi-restoreallmocks) inside [`afterEach`](/api/#aftereach) (or enable [`test.restoreMocks`](/config/#restoreMocks)) to restore all methods to their original implementations after every test. This will restore the original [object descriptor](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty), so you won't be able to change method's implementation anymore, unless you spy again: ```ts const cart = { @@ -545,7 +633,12 @@ And while it is possible to spy on exports in `jsdom` or other Node.js environme ### vi.stubEnv {#vi-stubenv} -- **Type:** `(name: T, value: T extends "PROD" | "DEV" | "SSR" ? boolean : string | undefined) => Vitest` +```ts +function stubEnv( + name: T, + value: T extends 'PROD' | 'DEV' | 'SSR' ? boolean : string | undefined +): Vitest +``` Changes the value of environmental variable on `process.env` and `import.meta.env`. You can restore its value by calling `vi.unstubAllEnvs`. @@ -579,7 +672,9 @@ import.meta.env.MODE = 'test' ### vi.unstubAllEnvs {#vi-unstuballenvs} -- **Type:** `() => Vitest` +```ts +function unstubAllEnvs(): Vitest +``` Restores all `import.meta.env` and `process.env` values that were changed with `vi.stubEnv`. When it's called for the first time, Vitest remembers the original value and will store it, until `unstubAllEnvs` is called again. @@ -608,7 +703,12 @@ import.meta.env.NODE_ENV === 'development' ### vi.stubGlobal -- **Type:** `(name: string | number | symbol, value: unknown) => Vitest` +```ts +function stubGlobal( + name: string | number | symbol, + value: unknown +): Vitest +``` Changes the value of global variable. You can restore its original value by calling `vi.unstubAllGlobals`. @@ -637,7 +737,9 @@ window.innerWidth = 100 ### vi.unstubAllGlobals {#vi-unstuballglobals} -- **Type:** `() => Vitest` +```ts +function unstubAllGlobals(): Vitest +``` Restores all global values on `globalThis`/`global` (and `window`/`top`/`self`/`parent`, if you are using `jsdom` or `happy-dom` environment) that were changed with `vi.stubGlobal`. When it's called for the first time, Vitest remembers the original value and will store it, until `unstubAllGlobals` is called again. @@ -670,7 +772,9 @@ This sections describes how to work with [fake timers](/guide/mocking#timers). ### vi.advanceTimersByTime -- **Type:** `(ms: number) => Vitest` +```ts +function advanceTimersByTime(ms: number): Vitest +``` This method will invoke every initiated timer until the specified number of milliseconds is passed or the queue is empty - whatever comes first. @@ -687,7 +791,9 @@ vi.advanceTimersByTime(150) ### vi.advanceTimersByTimeAsync -- **Type:** `(ms: number) => Promise` +```ts +function advanceTimersByTimeAsync(ms: number): Promise +``` This method will invoke every initiated timer until the specified number of milliseconds is passed or the queue is empty - whatever comes first. This will include asynchronously set timers. @@ -704,7 +810,9 @@ await vi.advanceTimersByTimeAsync(150) ### vi.advanceTimersToNextTimer -- **Type:** `() => Vitest` +```ts +function advanceTimersToNextTimer(): Vitest +``` Will call next available timer. Useful to make assertions between each timer call. You can chain call it to manage timers by yourself. @@ -719,7 +827,9 @@ vi.advanceTimersToNextTimer() // log: 1 ### vi.advanceTimersToNextTimerAsync -- **Type:** `() => Promise` +```ts +function advanceTimersToNextTimerAsync(): Promise +``` Will call next available timer and wait until it's resolved if it was set asynchronously. Useful to make assertions between each timer call. @@ -734,9 +844,11 @@ await vi.advanceTimersToNextTimerAsync() // log: 2 await vi.advanceTimersToNextTimerAsync() // log: 3 ``` -### vi.advanceTimersToNextFrame 2.1.0 {#vi-advancetimerstonextframe} +### vi.advanceTimersToNextFrame {#vi-advancetimerstonextframe} -- **Type:** `() => Vitest` +```ts +function advanceTimersToNextFrame(): Vitest +``` Similar to [`vi.advanceTimersByTime`](https://vitest.dev/api/vi#vi-advancetimersbytime), but will advance timers by the milliseconds needed to execute callbacks currently scheduled with `requestAnimationFrame`. @@ -754,35 +866,49 @@ expect(frameRendered).toBe(true) ### vi.getTimerCount -- **Type:** `() => number` +```ts +function getTimerCount(): number +``` Get the number of waiting timers. ### vi.clearAllTimers +```ts +function clearAllTimers(): void +``` + Removes all timers that are scheduled to run. These timers will never run in the future. ### vi.getMockedSystemTime -- **Type**: `() => Date | null` +```ts +function getMockedSystemTime(): Date | null +``` Returns mocked current date. If date is not mocked the method will return `null`. ### vi.getRealSystemTime -- **Type**: `() => number` +```ts +function getRealSystemTime(): number +``` When using `vi.useFakeTimers`, `Date.now` calls are mocked. If you need to get real time in milliseconds, you can call this function. ### vi.runAllTicks -- **Type:** `() => Vitest` +```ts +function runAllTicks(): Vitest +``` Calls every microtask that was queued by `process.nextTick`. This will also run all microtasks scheduled by themselves. ### vi.runAllTimers -- **Type:** `() => Vitest` +```ts +function runAllTimers(): Vitest +``` This method will invoke every initiated timer until the timer queue is empty. It means that every timer called during `runAllTimers` will be fired. If you have an infinite interval, it will throw after 10 000 tries (can be configured with [`fakeTimers.loopLimit`](/config/#faketimers-looplimit)). @@ -805,7 +931,9 @@ vi.runAllTimers() ### vi.runAllTimersAsync -- **Type:** `() => Promise` +```ts +function runAllTimersAsync(): Promise +``` This method will asynchronously invoke every initiated timer until the timer queue is empty. It means that every timer called during `runAllTimersAsync` will be fired even asynchronous timers. If you have an infinite interval, it will throw after 10 000 tries (can be configured with [`fakeTimers.loopLimit`](/config/#faketimers-looplimit)). @@ -822,7 +950,9 @@ await vi.runAllTimersAsync() ### vi.runOnlyPendingTimers -- **Type:** `() => Vitest` +```ts +function runOnlyPendingTimers(): Vitest +``` This method will call every timer that was initiated after [`vi.useFakeTimers`](#vi-usefaketimers) call. It will not fire any timer that was initiated during its call. @@ -837,7 +967,9 @@ vi.runOnlyPendingTimers() ### vi.runOnlyPendingTimersAsync -- **Type:** `() => Promise` +```ts +function runOnlyPendingTimersAsync(): Promise +``` This method will asynchronously call every timer that was initiated after [`vi.useFakeTimers`](#vi-usefaketimers) call, even asynchronous ones. It will not fire any timer that was initiated during its call. @@ -864,7 +996,9 @@ await vi.runOnlyPendingTimersAsync() ### vi.setSystemTime -- **Type**: `(date: string | number | Date) => void` +```ts +function setSystemTime(date: string | number | Date): Vitest +``` If fake timers are enabled, this method simulates a user changing the system clock (will affect date related API like `hrtime`, `performance.now` or `new Date()`) - however, it will not fire any timers. If fake timers are not enabled, this method will only mock `Date.*` calls. @@ -885,7 +1019,9 @@ vi.useRealTimers() ### vi.useFakeTimers -- **Type:** `(config?: FakeTimerInstallOpts) => Vitest` +```ts +function useFakeTimers(config?: FakeTimerInstallOpts): Vitest +``` To enable mocking timers, you need to call this method. It will wrap all further calls to timers (such as `setTimeout`, `setInterval`, `clearTimeout`, `clearInterval`, `setImmediate`, `clearImmediate`, and `Date`) until [`vi.useRealTimers()`](#vi-userealtimers) is called. @@ -900,13 +1036,17 @@ But you can enable it by specifying the option in `toFake` argument: `vi.useFake ### vi.isFakeTimers {#vi-isfaketimers} -- **Type:** `() => boolean` +```ts +function isFakeTimers(): boolean +``` Returns `true` if fake timers are enabled. ### vi.useRealTimers -- **Type:** `() => Vitest` +```ts +function useRealTimers(): Vitest +``` When timers have run out, you may call this method to return mocked timers to its original implementations. All timers that were scheduled before will be discarded. @@ -916,7 +1056,12 @@ A set of useful helper functions that Vitest provides. ### vi.waitFor {#vi-waitfor} -- **Type:** `(callback: WaitForCallback, options?: number | WaitForOptions) => Promise` +```ts +function waitFor( + callback: WaitForCallback, + options?: number | WaitForOptions +): Promise +``` Wait for the callback to execute successfully. If the callback throws an error or returns a rejected promise it will continue to wait until it succeeds or times out. @@ -978,7 +1123,12 @@ If `vi.useFakeTimers` is used, `vi.waitFor` automatically calls `vi.advanceTimer ### vi.waitUntil {#vi-waituntil} -- **Type:** `(callback: WaitUntilCallback, options?: number | WaitUntilOptions) => Promise` +```ts +function waitUntil( + callback: WaitUntilCallback, + options?: number | WaitUntilOptions +): Promise +``` This is similar to `vi.waitFor`, but if the callback throws any errors, execution is immediately interrupted and an error message is received. If the callback returns falsy value, the next check will continue until truthy value is returned. This is useful when you need to wait for something to exist before taking the next step. @@ -1003,7 +1153,9 @@ test('Element render correctly', async () => { ### vi.hoisted {#vi-hoisted} -- **Type**: `(factory: () => T) => T` +```ts +function hoisted(factory: () => T): T +``` All static `import` statements in ES modules are hoisted to the top of the file, so any code that is defined before the imports will actually be executed after imports are evaluated. @@ -1080,7 +1232,9 @@ const json = await vi.hoisted(async () => { ### vi.setConfig -- **Type**: `RuntimeConfig` +```ts +function setConfig(config: RuntimeOptions): void +``` Updates config for the current test file. This method supports only config options that will affect the current test file: @@ -1105,6 +1259,8 @@ vi.setConfig({ ### vi.resetConfig -- **Type**: `RuntimeConfig` +```ts +function resetConfig(): void +``` If [`vi.setConfig`](#vi-setconfig) was called before, this will reset config to the original state. diff --git a/docs/config/index.md b/docs/config/index.md index 796fa980121f..2e3362463904 100644 --- a/docs/config/index.md +++ b/docs/config/index.md @@ -1643,7 +1643,7 @@ This is an experimental feature. Breaking changes might not follow SemVer, pleas - **Type:** `boolean` - **Default:** `false` -Will call [`.mockClear()`](/api/mock#mockclear) on all spies before each test. +Will call [`vi.clearAllMocks()`](/api/vi#vi-clearallmocks) before each test. This will clear mock history without affecting mock implementations. ### mockReset @@ -1651,16 +1651,17 @@ This will clear mock history without affecting mock implementations. - **Type:** `boolean` - **Default:** `false` -Will call [`.mockReset()`](/api/mock#mockreset) on all spies before each test. -This will clear mock history and reset each implementation to its original. +Will call [`vi.resetAllMocks()`](/api/vi#vi-resetallmocks) before each test. +This will clear mock history and reset each implementation. ### restoreMocks - **Type:** `boolean` - **Default:** `false` -Will call [`.mockRestore()`](/api/mock#mockrestore) on all spies before each test. -This will clear mock history, restore each implementation to its original, and restore original descriptors of spied-on objects.. +Will call [`vi.restoreAllMocks()`](/api/vi#vi-restoreallmocks) before each test. + +This restores all original implementations on spies created with [`vi.spyOn`](#vi-spyon). ### unstubEnvs {#unstubenvs} From 6c2a4d5ae1aaa193bd739c1d41a92680c13d0545 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 1 Aug 2025 15:28:54 +0200 Subject: [PATCH 25/29] test: don't use using --- test/core/test/mocking/vi-fn.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/core/test/mocking/vi-fn.test.ts b/test/core/test/mocking/vi-fn.test.ts index 002d7d8a22cc..43e33c65e938 100644 --- a/test/core/test/mocking/vi-fn.test.ts +++ b/test/core/test/mocking/vi-fn.test.ts @@ -592,8 +592,9 @@ describe('vi.fn() implementations', () => { expect(mock()).toBe(undefined) }) - test('vi.fn() throws an error if new is called on arrow function', () => { - using log = vi.spyOn(console, 'warn') + test('vi.fn() throws an error if new is called on arrow function', ({ onTestFinished }) => { + const log = vi.spyOn(console, 'warn') + onTestFinished(() => log.mockRestore()) const Mock = vi.fn(() => {}) expect(() => new Mock()).toThrowError() expect(log).toHaveBeenCalledWith( From 952755d9c7a0f476d18ffaa0ad7fbe66a7371bb1 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 1 Aug 2025 15:35:08 +0200 Subject: [PATCH 26/29] docs: remove irrelevant comment --- packages/spy/src/types.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/spy/src/types.ts b/packages/spy/src/types.ts index eda27a357f95..e035fe3d446d 100644 --- a/packages/spy/src/types.ts +++ b/packages/spy/src/types.ts @@ -240,8 +240,6 @@ export interface MockInstance e mockReset(): this /** * Does what `mockReset` does and restores original descriptors of spied-on objects. - * - * Note that restoring mock from `vi.fn()` will set implementation to an empty function that returns `undefined`. Restoring a `vi.fn(impl)` will restore implementation to `impl`. * @see https://vitest.dev/api/mock#mockrestore */ mockRestore(): void From 72a0634afa0f6529c74c2b48e3b4a26e27589500 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 1 Aug 2025 15:36:17 +0200 Subject: [PATCH 27/29] docs: fix link --- docs/api/vi.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api/vi.md b/docs/api/vi.md index caa111e57b71..dbd4ae0d32b4 100644 --- a/docs/api/vi.md +++ b/docs/api/vi.md @@ -526,7 +526,7 @@ After the mock was restored, you can spy on it again. ::: warning This method also does not affect mocks created during [automocking](/guide/mocking-modules#mocking-a-module). -Note that unlike [`mock.mockRestore`](/api/mock/#mockrestore), `vi.restoreAllMocks` will not clear mock history or reset the mock implementation +Note that unlike [`mock.mockRestore`](/api/mock#mockrestore), `vi.restoreAllMocks` will not clear mock history or reset the mock implementation ::: ### vi.spyOn From 5a32529bb0e0e12784321f56ff9ad8ffef879350 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 1 Aug 2025 15:41:06 +0200 Subject: [PATCH 28/29] docs: cleanup --- docs/api/vi.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api/vi.md b/docs/api/vi.md index dbd4ae0d32b4..2aee8dffe9f2 100644 --- a/docs/api/vi.md +++ b/docs/api/vi.md @@ -30,7 +30,7 @@ function mock( factory?: MockOptions | MockFactory ): void function mock( - path: Promise, + module: Promise, factory?: MockOptions | MockFactory ): void ``` @@ -181,7 +181,7 @@ function doMock( factory?: MockOptions | MockFactory ): void function doMock( - path: Promise, + module: Promise, factory?: MockOptions | MockFactory ): void ``` From 34adde5f175c43086b8dde83eb09b0ae110fb89d Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 1 Aug 2025 15:53:41 +0200 Subject: [PATCH 29/29] chore: cleanup --- packages/spy/src/index.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/spy/src/index.ts b/packages/spy/src/index.ts index a17ad7355cbb..73568c31ff26 100644 --- a/packages/spy/src/index.ts +++ b/packages/spy/src/index.ts @@ -449,7 +449,6 @@ function createMock( state.contexts[contextIndex - 1] = returnValue state.instances[instanceIndex - 1] = returnValue - // TODO: test this is correct if (contextPrototypeIndex != null && prototypeState) { prototypeState.contexts[contextPrototypeIndex - 1] = returnValue } @@ -478,7 +477,7 @@ function createMock( } return returnValue - }) as Mock, + }) as Mock, } if (original) { copyOriginalStaticProperties(namedObject[name], original) @@ -530,7 +529,6 @@ function copyOriginalStaticProperties(mock: Mock, original: Procedure | Construc Object.defineProperty(mock, key, descriptor) } - return mock } const ignoreProperties = new Set([ @@ -577,7 +575,7 @@ function getDefaultConfig(original?: Procedure | Constructable): MockConfig { } } -function getDefaultState(): MockContext { +function getDefaultState(): MockContext { const state = { calls: [], contexts: [],