diff --git a/packages/spy/src/index.ts b/packages/spy/src/index.ts index 6e3315b56e00..a9dfcc70fe04 100644 --- a/packages/spy/src/index.ts +++ b/packages/spy/src/index.ts @@ -22,7 +22,7 @@ interface MockResultThrow { type MockResult = MockResultReturn | MockResultThrow | MockResultIncomplete -export interface MockContext { +export interface MockContext { /** * This is an array containing all arguments for each call. One item of the array is the arguments of that call. * @@ -37,11 +37,11 @@ export interface MockContext { * ['arg3'], // second call * ] */ - calls: TArgs[] + calls: Parameters[] /** * 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. */ - instances: TReturns[] + instances: ReturnType[] /** * The order of mock's execution. This returns an array of numbers which are shared between all defined mocks. * @@ -85,14 +85,15 @@ export interface MockContext { * }, * ] */ - results: MockResult[] + results: MockResult>[] /** * This contains the arguments of the last call. If spy wasn't called, will return `undefined`. */ - lastCall: TArgs | undefined + lastCall: Parameters | undefined } type Procedure = (...args: any[]) => any +type UnknownProcedure = (...args: unknown[]) => unknown type Methods = keyof { [K in keyof T as T[K] extends Procedure ? K : never]: T[K]; @@ -104,42 +105,49 @@ type Classes = { [K in keyof T]: T[K] extends new (...args: any[]) => any ? K : never }[keyof T] & (string | symbol) -/** - * @deprecated Use MockInstance instead - */ -export interface SpyInstance extends MockInstance {} +/* +cf. https://typescript-eslint.io/rules/method-signature-style/ -export interface MockInstance { +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 { /** * Use it to return the name given to mock with method `.mockName(name)`. */ - getMockName: () => string + getMockName(): string /** * Sets internal mock name. Useful to see the name of the mock if an assertion fails. */ - mockName: (n: string) => this + mockName(n: string): this /** * Current context of the mock. It stores information about all invocation calls, instances, and results. */ - mock: MockContext + mock: MockContext /** * Clears all information about every call. After calling it, all properties on `.mock` will return an empty state. This method does not reset implementations. * * It is useful if you need to clean up mock between different assertions. */ - mockClear: () => this + mockClear(): this /** * Does what `mockClear` does and makes inner implementation an empty function (returning `undefined` when invoked). This also resets all "once" implementations. * * This is useful when you want to completely reset a mock to the default state. */ - mockReset: () => this + mockReset(): this /** * Does what `mockReset` does and restores inner implementation to the original function. * * 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`. */ - mockRestore: () => void + mockRestore(): void /** * Returns current mock implementation if there is one. * @@ -147,14 +155,14 @@ export interface MockInstance { * * If mock was created with `vi.spyOn`, it will return `undefined` unless a custom implementation was provided. */ - getMockImplementation: () => ((...args: TArgs) => TReturns) | undefined + getMockImplementation(): T | undefined /** * Accepts a function that will be used as an implementation of the mock. * @example * const increment = vi.fn().mockImplementation(count => count + 1); * expect(increment(3)).toBe(4); */ - mockImplementation: (fn: ((...args: TArgs) => TReturns)) => this + mockImplementation(fn: T): this /** * Accepts a function that will be used as a mock implementation during the next call. Can be chained so that multiple function calls produce different results. * @example @@ -162,7 +170,7 @@ export interface MockInstance { * expect(fn(3)).toBe(4); * expect(fn(3)).toBe(3); */ - mockImplementationOnce: (fn: ((...args: TArgs) => TReturns)) => this + mockImplementationOnce(fn: T): this /** * Overrides the original mock implementation temporarily while the callback is being executed. * @example @@ -174,15 +182,16 @@ export interface MockInstance { * * myMockFn() // 'original' */ - withImplementation: (fn: ((...args: TArgs) => TReturns), cb: () => T) => T extends Promise ? Promise : this + withImplementation(fn: T, cb: () => T2): T2 extends Promise ? Promise : this + /** * Use this if you need to return `this` context from the method without invoking actual implementation. */ - mockReturnThis: () => this + mockReturnThis(): this /** * Accepts a value that will be returned whenever the mock function is called. */ - mockReturnValue: (obj: TReturns) => this + mockReturnValue(obj: ReturnType): this /** * Accepts a value that will be returned during the next function call. If chained, every consecutive call will return the specified value. * @@ -197,14 +206,14 @@ export interface MockInstance { * // 'first call', 'second call', 'default' * console.log(myMockFn(), myMockFn(), myMockFn()) */ - mockReturnValueOnce: (obj: TReturns) => this + mockReturnValueOnce(obj: ReturnType): this /** * Accepts a value that will be resolved when async function is called. * @example * const asyncMock = vi.fn().mockResolvedValue(42) * asyncMock() // Promise<42> */ - mockResolvedValue: (obj: Awaited) => this + mockResolvedValue(obj: Awaited>): this /** * Accepts a value that will be resolved during the next function call. If chained, every consecutive call will resolve specified value. * @example @@ -217,14 +226,14 @@ export interface MockInstance { * // Promise<'first call'>, Promise<'second call'>, Promise<'default'> * console.log(myMockFn(), myMockFn(), myMockFn()) */ - mockResolvedValueOnce: (obj: Awaited) => this + mockResolvedValueOnce(obj: 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 'Async error' */ - mockRejectedValue: (obj: any) => this + mockRejectedValue(obj: any): this /** * Accepts a value that will be rejected during the next function call. If chained, every consecutive call will reject specified value. * @example @@ -236,31 +245,35 @@ export interface MockInstance { * await asyncMock() // first call * await asyncMock() // throws "Async error" */ - mockRejectedValueOnce: (obj: any) => this + mockRejectedValueOnce(obj: any): this } +/* eslint-enable ts/method-signature-style */ -export interface Mock extends MockInstance { - new (...args: TArgs): TReturns - (...args: TArgs): TReturns +export interface Mock extends MockInstance { + new (...args: Parameters): ReturnType + (...args: Parameters): ReturnType } -export interface PartialMock extends MockInstance> ? Promise>> : Partial> { - new (...args: TArgs): TReturns - (...args: TArgs): TReturns + +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, R> + ? Mock<(...args: ConstructorParameters) => R> : T -export type MockedFunction = Mock, ReturnType> & { +export type MockedFunction = Mock & { [K in keyof T]: T[K]; } -export type PartiallyMockedFunction = PartialMock, ReturnType> & { +export type PartiallyMockedFunction = PartialMock & { [K in keyof T]: T[K]; } -export type MockedFunctionDeep = Mock, ReturnType> & MockedObjectDeep -export type PartiallyMockedFunctionDeep = PartialMock, ReturnType> & MockedObjectDeep +export type MockedFunctionDeep = Mock & MockedObjectDeep +export type PartiallyMockedFunctionDeep = PartialMock & MockedObjectDeep export type MockedObject = MaybeMockedConstructor & { [K in Methods]: T[K] extends Procedure ? MockedFunction @@ -300,16 +313,13 @@ interface Constructable { new (...args: any[]): any } -export type MockedClass = MockInstance< - T extends new (...args: infer P) => any ? P : never, - InstanceType -> & { +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 (...args: infer Args) => infer Returns - ? MockInstance + [P in keyof T]: T[P] extends Procedure + ? MockInstance : T[P] extends Constructable ? MockedClass : T[P] @@ -328,16 +338,16 @@ export function spyOn>>( obj: T, methodName: S, accessType: 'get', -): MockInstance<[], T[S]> +): MockInstance<() => T[S]> export function spyOn>>( obj: T, methodName: G, accessType: 'set', -): MockInstance<[T[G]], void> +): MockInstance<(arg: T[G]) => void> export function spyOn> | Methods>)>( obj: T, methodName: M, -): Required[M] extends ({ new (...args: infer A): infer R }) | ((...args: infer A) => infer R) ? MockInstance : never +): Required[M] extends ({ new (...args: infer A): infer R }) | ((...args: infer A) => infer R) ? MockInstance<(...args: A) => R> : never export function spyOn( obj: T, method: K, @@ -356,12 +366,15 @@ export function spyOn( let callOrder = 0 -function enhanceSpy( - spy: SpyInternalImpl, -): MockInstance { - const stub = spy as unknown as MockInstance +function enhanceSpy( + spy: SpyInternalImpl, ReturnType>, +): MockInstance { + type TArgs = Parameters + type TReturns = ReturnType + + const stub = spy as unknown as MockInstance - let implementation: ((...args: TArgs) => TReturns) | undefined + let implementation: T | undefined let instances: any[] = [] let invocations: number[] = [] @@ -416,7 +429,7 @@ function enhanceSpy( stub.mockReset = () => { stub.mockClear() - implementation = () => undefined as unknown as TReturns + implementation = (() => undefined) as T onceImplementations = [] return stub } @@ -429,20 +442,20 @@ function enhanceSpy( } stub.getMockImplementation = () => implementation - stub.mockImplementation = (fn: (...args: TArgs) => TReturns) => { + stub.mockImplementation = (fn: T) => { implementation = fn state.willCall(mockCall) return stub } - stub.mockImplementationOnce = (fn: (...args: TArgs) => TReturns) => { + stub.mockImplementationOnce = (fn: T) => { onceImplementations.push(fn) return stub } - function withImplementation(fn: (...args: TArgs) => TReturns, cb: () => void): MockInstance - function withImplementation(fn: (...args: TArgs) => TReturns, cb: () => Promise): Promise> - function withImplementation(fn: (...args: TArgs) => TReturns, cb: () => void | Promise): MockInstance | Promise> { + function withImplementation(fn: T, cb: () => void): MockInstance + function withImplementation(fn: T, cb: () => Promise): Promise> + function withImplementation(fn: T, cb: () => void | Promise): MockInstance | Promise> { const originalImplementation = implementation implementation = fn @@ -471,24 +484,24 @@ function enhanceSpy( stub.withImplementation = withImplementation stub.mockReturnThis = () => - stub.mockImplementation(function (this: TReturns) { + stub.mockImplementation((function (this: TReturns) { return this - }) + }) as any) - stub.mockReturnValue = (val: TReturns) => stub.mockImplementation(() => val) - stub.mockReturnValueOnce = (val: TReturns) => stub.mockImplementationOnce(() => val) + stub.mockReturnValue = (val: TReturns) => stub.mockImplementation((() => val) as any) + stub.mockReturnValueOnce = (val: TReturns) => stub.mockImplementationOnce((() => val) as any) stub.mockResolvedValue = (val: Awaited) => - stub.mockImplementation(() => Promise.resolve(val as TReturns) as any) + stub.mockImplementation((() => Promise.resolve(val as TReturns)) as any) stub.mockResolvedValueOnce = (val: Awaited) => - stub.mockImplementationOnce(() => Promise.resolve(val as TReturns) as any) + stub.mockImplementationOnce((() => Promise.resolve(val as TReturns)) as any) stub.mockRejectedValue = (val: unknown) => - stub.mockImplementation(() => Promise.reject(val) as any) + stub.mockImplementation((() => Promise.reject(val)) as any) stub.mockRejectedValueOnce = (val: unknown) => - stub.mockImplementationOnce(() => Promise.reject(val) as any) + stub.mockImplementationOnce((() => Promise.reject(val)) as any) Object.defineProperty(stub, 'mock', { get: () => mockContext, @@ -501,16 +514,12 @@ function enhanceSpy( return stub as any } -export function fn(): Mock -export function fn( - implementation: (...args: TArgs) => R -): Mock -export function fn( - implementation?: (...args: TArgs) => R, -): Mock { +export function fn( + implementation?: T, +): Mock { const enhancedSpy = enhanceSpy(tinyspy.internalSpyOn({ spy: implementation || (() => {}) }, 'spy')) if (implementation) enhancedSpy.mockImplementation(implementation) - return enhancedSpy as Mock + return enhancedSpy as any } diff --git a/packages/vitest/src/types/index.ts b/packages/vitest/src/types/index.ts index 3da526a29867..05eaef50742d 100644 --- a/packages/vitest/src/types/index.ts +++ b/packages/vitest/src/types/index.ts @@ -17,7 +17,6 @@ export type { DiffOptions } from '@vitest/utils/diff' export type { MockedFunction, MockedObject, - SpyInstance, MockInstance, Mock, MockContext, diff --git a/test/config/test/resolution.test.ts b/test/config/test/resolution.test.ts index f047d7d44760..5b5c99fb2eaf 100644 --- a/test/config/test/resolution.test.ts +++ b/test/config/test/resolution.test.ts @@ -305,7 +305,7 @@ describe.each([ await expect(async () => { await config(rawConfig.options) }).rejects.toThrowError() - expect(error.mock.lastCall[0]).toEqual( + expect(error.mock.lastCall?.[0]).toEqual( expect.stringContaining(`Inspector host cannot be a URL. Use "host:port" instead of "${url}"`), ) }) diff --git a/test/core/test/mock-internals.test.ts b/test/core/test/mock-internals.test.ts index ea3ac225d36f..246c691093c1 100644 --- a/test/core/test/mock-internals.test.ts +++ b/test/core/test/mock-internals.test.ts @@ -1,6 +1,6 @@ import childProcess, { exec } from 'node:child_process' import timers from 'node:timers' -import { type SpyInstance, afterEach, beforeEach, describe, expect, test, vi } from 'vitest' +import { type MockInstance, afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { execDefault, execHelloWorld, execImportAll } from '../src/exec' import { dynamicImport } from '../src/dynamic-import' @@ -31,7 +31,7 @@ test('mocked dynamically imported packages', async () => { describe('Math.random', () => { describe('mock is restored', () => { - let spy: SpyInstance + let spy: MockInstance beforeEach(() => { spy = vi.spyOn(Math, 'random').mockReturnValue(0.1) diff --git a/test/core/test/spy.test.ts b/test/core/test/spy.test.ts index 35cddb90374a..719160540b01 100644 --- a/test/core/test/spy.test.ts +++ b/test/core/test/spy.test.ts @@ -15,7 +15,7 @@ describe('spyOn', () => { test('infers a class correctly', () => { vi.spyOn(mock, 'HelloWorld').mockImplementationOnce(() => { - const Mock = vi.fn() + const Mock = vi.fn() Mock.prototype.hello = vi.fn(() => 'hello world') return new Mock() }) diff --git a/test/core/test/vi.spec.ts b/test/core/test/vi.spec.ts index 79ea9ad5b910..5de26f05f495 100644 --- a/test/core/test/vi.spec.ts +++ b/test/core/test/vi.spec.ts @@ -2,8 +2,8 @@ * @vitest-environment jsdom */ -import type { MockedFunction, MockedObject } from 'vitest' -import { describe, expect, test, vi } from 'vitest' +import type { Mock, MockedFunction, MockedObject } from 'vitest' +import { describe, expect, expectTypeOf, test, vi } from 'vitest' import { getWorkerState } from '../../../packages/vitest/src/utils' function expectType(obj: T) { @@ -41,6 +41,15 @@ describe('testing vi utils', () => { }) expectType boolean>>(vi.fn(() => true)) expectType boolean>>(vi.fn()) + + expectType boolean>>(vi.fn<() => boolean>(() => true)) + expectType boolean>>(vi.fn<() => boolean>(() => true)) + expectType<() => boolean>(vi.fn(() => true)) + + // @ts-expect-error default unkonwn + expectType<(v: number) => boolean>(vi.fn()) + + expectType<(v: number) => boolean>(vi.fn()) }) test('vi partial mocked', () => { @@ -50,32 +59,28 @@ describe('testing vi utils', () => { baz: string } - type FooBarFactory = () => FooBar - - const mockFactory: FooBarFactory = vi.fn() + const mockFactory = vi.fn<() => FooBar>() vi.mocked(mockFactory, { partial: true }).mockReturnValue({ foo: vi.fn(), }) vi.mocked(mockFactory, { partial: true, deep: false }).mockReturnValue({ - bar: vi.fn(), + bar: vi.fn(), }) vi.mocked(mockFactory, { partial: true, deep: true }).mockReturnValue({ baz: 'baz', }) - type FooBarAsyncFactory = () => Promise - - const mockFactoryAsync: FooBarAsyncFactory = vi.fn() + const mockFactoryAsync = vi.fn<() => Promise>() vi.mocked(mockFactoryAsync, { partial: true }).mockResolvedValue({ foo: vi.fn(), }) vi.mocked(mockFactoryAsync, { partial: true, deep: false }).mockResolvedValue({ - bar: vi.fn(), + bar: vi.fn(), }) vi.mocked(mockFactoryAsync, { partial: true, deep: true }).mockResolvedValue({ @@ -83,6 +88,39 @@ describe('testing vi utils', () => { }) }) + test('vi.fn and Mock type', () => { + // use case from https://github.com/vitest-dev/vitest/issues/4723#issuecomment-1851034249 + + // hypotetical library to be tested + type SomeFn = (v: string) => number + function acceptSomeFn(f: SomeFn) { + f('hi') + } + + // SETUP + // no args are allowed even though it's not type safe + const someFn1: Mock = vi.fn() + + // argument types are infered + const someFn2: Mock = vi.fn((v) => { + expectTypeOf(v).toEqualTypeOf() + return 0 + }) + + // arguments are not necessary + const someFn3: Mock = vi.fn(() => 0) + + // @ts-expect-error wrong return type will be caught + const someFn4: Mock = vi.fn(() => '0') + + // TEST + acceptSomeFn(someFn1) + expect(someFn1).toBeCalledWith('hi') + expect(someFn2).not.toBeCalled() + expect(someFn3).not.toBeCalled() + expect(someFn4).not.toBeCalled() + }) + test('can change config', () => { const state = getWorkerState() expect(state.config.hookTimeout).toBe(10000)