Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix!(spy): align mocking related typings with jest #4784

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
157 changes: 83 additions & 74 deletions packages/spy/src/index.ts
Expand Up @@ -22,7 +22,7 @@ interface MockResultThrow {

type MockResult<T> = MockResultReturn<T> | MockResultThrow | MockResultIncomplete

export interface MockContext<TArgs, TReturns> {
export interface MockContext<T extends Procedure> {
/**
* This is an array containing all arguments for each call. One item of the array is the arguments of that call.
*
Expand All @@ -37,11 +37,11 @@ export interface MockContext<TArgs, TReturns> {
* ['arg3'], // second call
* ]
*/
calls: TArgs[]
calls: Parameters<T>[]
/**
* 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<T>[]
/**
* The order of mock's execution. This returns an array of numbers which are shared between all defined mocks.
*
Expand Down Expand Up @@ -85,14 +85,15 @@ export interface MockContext<TArgs, TReturns> {
* },
* ]
*/
results: MockResult<TReturns>[]
results: MockResult<ReturnType<T>>[]
/**
* This contains the arguments of the last call. If spy wasn't called, will return `undefined`.
*/
lastCall: TArgs | undefined
lastCall: Parameters<T> | undefined
}

type Procedure = (...args: any[]) => any
type UnknownProcedure = (...args: unknown[]) => unknown

type Methods<T> = keyof {
[K in keyof T as T[K] extends Procedure ? K : never]: T[K];
Expand All @@ -104,65 +105,72 @@ type Classes<T> = {
[K in keyof T]: T[K] extends new (...args: any[]) => any ? K : never
}[keyof T] & (string | symbol)

/**
* @deprecated Use MockInstance<A, R> instead
*/
export interface SpyInstance<TArgs extends any[] = any[], TReturns = any> extends MockInstance<TArgs, TReturns> {}
/*
cf. https://typescript-eslint.io/rules/method-signature-style/

export interface MockInstance<TArgs extends any[] = any[], TReturns = any> {
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)
*/
Comment on lines +111 to +118
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I took the latter one to follow Jest for now, but I think going with the former should be totally fine too.
Jest one is technically type unsafe and the assignment example I wrote here can be easily rewritten by:

jest.fn<() => boolean>(() => true)
jest.fn(() => true as boolean)

Or in a more realistic scenario, it would probably look like:

// setup
type MyFn = () => boolean;
let myMockFn: Mock<MyFn>

// test
myMockFn = vi.fn<MyFn>(() => true)

/* eslint-disable ts/method-signature-style */
export interface MockInstance<T extends Procedure = UnknownProcedure> {
/**
* 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<TArgs, TReturns>
mock: MockContext<T>
/**
* 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.
*
* 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: () => ((...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
* const fn = vi.fn(count => count).mockImplementationOnce(count => count + 1);
* 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
Expand All @@ -174,15 +182,16 @@ export interface MockInstance<TArgs extends any[] = any[], TReturns = any> {
*
* myMockFn() // 'original'
*/
withImplementation: <T>(fn: ((...args: TArgs) => TReturns), cb: () => T) => T extends Promise<unknown> ? Promise<this> : this
withImplementation<T2>(fn: T, cb: () => T2): T2 extends Promise<unknown> ? Promise<this> : 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<T>): this
/**
* Accepts a value that will be returned during the next function call. If chained, every consecutive call will return the specified value.
*
Expand All @@ -197,14 +206,14 @@ export interface MockInstance<TArgs extends any[] = any[], TReturns = any> {
* // 'first call', 'second call', 'default'
* console.log(myMockFn(), myMockFn(), myMockFn())
*/
mockReturnValueOnce: (obj: TReturns) => this
mockReturnValueOnce(obj: ReturnType<T>): 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<TReturns>) => this
mockResolvedValue(obj: Awaited<ReturnType<T>>): this
/**
* Accepts a value that will be resolved during the next function call. If chained, every consecutive call will resolve specified value.
* @example
Expand All @@ -217,14 +226,14 @@ export interface MockInstance<TArgs extends any[] = any[], TReturns = any> {
* // Promise<'first call'>, Promise<'second call'>, Promise<'default'>
* console.log(myMockFn(), myMockFn(), myMockFn())
*/
mockResolvedValueOnce: (obj: Awaited<TReturns>) => this
mockResolvedValueOnce(obj: Awaited<ReturnType<T>>): 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
Expand All @@ -236,31 +245,35 @@ export interface MockInstance<TArgs extends any[] = any[], TReturns = any> {
* 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<TArgs extends any[] = any, TReturns = any> extends MockInstance<TArgs, TReturns> {
new (...args: TArgs): TReturns
(...args: TArgs): TReturns
export interface Mock<T extends Procedure = UnknownProcedure> extends MockInstance<T> {
new (...args: Parameters<T>): ReturnType<T>
(...args: Parameters<T>): ReturnType<T>
}
export interface PartialMock<TArgs extends any[] = any, TReturns = any> extends MockInstance<TArgs, TReturns extends Promise<Awaited<TReturns>> ? Promise<Partial<Awaited<TReturns>>> : Partial<TReturns>> {
new (...args: TArgs): TReturns
(...args: TArgs): TReturns

type PartialMaybePromise<T> = T extends Promise<Awaited<T>> ? Promise<Partial<Awaited<T>>> : Partial<T>

export interface PartialMock<T extends Procedure = UnknownProcedure> extends MockInstance<(...args: Parameters<T>) => PartialMaybePromise<ReturnType<T>>> {
new (...args: Parameters<T>): ReturnType<T>
(...args: Parameters<T>): ReturnType<T>
}

export type MaybeMockedConstructor<T> = T extends new (
...args: Array<any>
) => infer R
? Mock<ConstructorParameters<T>, R>
? Mock<(...args: ConstructorParameters<T>) => R>
: T
export type MockedFunction<T extends Procedure> = Mock<Parameters<T>, ReturnType<T>> & {
export type MockedFunction<T extends Procedure> = Mock<T> & {
[K in keyof T]: T[K];
}
export type PartiallyMockedFunction<T extends Procedure> = PartialMock<Parameters<T>, ReturnType<T>> & {
export type PartiallyMockedFunction<T extends Procedure> = PartialMock<T> & {
[K in keyof T]: T[K];
}
export type MockedFunctionDeep<T extends Procedure> = Mock<Parameters<T>, ReturnType<T>> & MockedObjectDeep<T>
export type PartiallyMockedFunctionDeep<T extends Procedure> = PartialMock<Parameters<T>, ReturnType<T>> & MockedObjectDeep<T>
export type MockedFunctionDeep<T extends Procedure> = Mock<T> & MockedObjectDeep<T>
export type PartiallyMockedFunctionDeep<T extends Procedure> = PartialMock<T> & MockedObjectDeep<T>
export type MockedObject<T> = MaybeMockedConstructor<T> & {
[K in Methods<T>]: T[K] extends Procedure
? MockedFunction<T[K]>
Expand Down Expand Up @@ -300,16 +313,13 @@ interface Constructable {
new (...args: any[]): any
}

export type MockedClass<T extends Constructable> = MockInstance<
T extends new (...args: infer P) => any ? P : never,
InstanceType<T>
> & {
export type MockedClass<T extends Constructable> = MockInstance<(...args: ConstructorParameters<T>) => InstanceType<T>> & {
prototype: T extends { prototype: any } ? Mocked<T['prototype']> : never
} & T

export type Mocked<T> = {
[P in keyof T]: T[P] extends (...args: infer Args) => infer Returns
? MockInstance<Args, Returns>
[P in keyof T]: T[P] extends Procedure
? MockInstance<T[P]>
: T[P] extends Constructable
? MockedClass<T[P]>
: T[P]
Expand All @@ -328,16 +338,16 @@ export function spyOn<T, S extends Properties<Required<T>>>(
obj: T,
methodName: S,
accessType: 'get',
): MockInstance<[], T[S]>
): MockInstance<() => T[S]>
export function spyOn<T, G extends Properties<Required<T>>>(
obj: T,
methodName: G,
accessType: 'set',
): MockInstance<[T[G]], void>
): MockInstance<(arg: T[G]) => void>
export function spyOn<T, M extends (Classes<Required<T>> | Methods<Required<T>>)>(
obj: T,
methodName: M,
): Required<T>[M] extends ({ new (...args: infer A): infer R }) | ((...args: infer A) => infer R) ? MockInstance<A, R> : never
): Required<T>[M] extends ({ new (...args: infer A): infer R }) | ((...args: infer A) => infer R) ? MockInstance<(...args: A) => R> : never
export function spyOn<T, K extends keyof T>(
obj: T,
method: K,
Expand All @@ -356,12 +366,15 @@ export function spyOn<T, K extends keyof T>(

let callOrder = 0

function enhanceSpy<TArgs extends any[], TReturns>(
spy: SpyInternalImpl<TArgs, TReturns>,
): MockInstance<TArgs, TReturns> {
const stub = spy as unknown as MockInstance<TArgs, TReturns>
function enhanceSpy<T extends Procedure>(
spy: SpyInternalImpl<Parameters<T>, ReturnType<T>>,
): MockInstance<T> {
type TArgs = Parameters<T>
type TReturns = ReturnType<T>

const stub = spy as unknown as MockInstance<T>

let implementation: ((...args: TArgs) => TReturns) | undefined
let implementation: T | undefined

let instances: any[] = []
let invocations: number[] = []
Expand Down Expand Up @@ -416,7 +429,7 @@ function enhanceSpy<TArgs extends any[], TReturns>(

stub.mockReset = () => {
stub.mockClear()
implementation = () => undefined as unknown as TReturns
implementation = (() => undefined) as T
onceImplementations = []
return stub
}
Expand All @@ -429,20 +442,20 @@ function enhanceSpy<TArgs extends any[], TReturns>(
}

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<TArgs, TReturns>
function withImplementation(fn: (...args: TArgs) => TReturns, cb: () => Promise<void>): Promise<MockInstance<TArgs, TReturns>>
function withImplementation(fn: (...args: TArgs) => TReturns, cb: () => void | Promise<void>): MockInstance<TArgs, TReturns> | Promise<MockInstance<TArgs, TReturns>> {
function withImplementation(fn: T, cb: () => void): MockInstance<T>
function withImplementation(fn: T, cb: () => Promise<void>): Promise<MockInstance<T>>
function withImplementation(fn: T, cb: () => void | Promise<void>): MockInstance<T> | Promise<MockInstance<T>> {
const originalImplementation = implementation

implementation = fn
Expand Down Expand Up @@ -471,24 +484,24 @@ function enhanceSpy<TArgs extends any[], TReturns>(
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<TReturns>) =>
stub.mockImplementation(() => Promise.resolve(val as TReturns) as any)
stub.mockImplementation((() => Promise.resolve(val as TReturns)) as any)

stub.mockResolvedValueOnce = (val: Awaited<TReturns>) =>
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,
Expand All @@ -501,16 +514,12 @@ function enhanceSpy<TArgs extends any[], TReturns>(
return stub as any
}

export function fn<TArgs extends any[] = any, R = any>(): Mock<TArgs, R>
export function fn<TArgs extends any[] = any[], R = any>(
implementation: (...args: TArgs) => R
): Mock<TArgs, R>
export function fn<TArgs extends any[] = any[], R = any>(
implementation?: (...args: TArgs) => R,
): Mock<TArgs, R> {
export function fn<T extends Procedure = UnknownProcedure>(
implementation?: T,
): Mock<T> {
const enhancedSpy = enhanceSpy(tinyspy.internalSpyOn({ spy: implementation || (() => {}) }, 'spy'))
if (implementation)
enhancedSpy.mockImplementation(implementation)

return enhancedSpy as Mock
return enhancedSpy as any
}
1 change: 0 additions & 1 deletion packages/vitest/src/types/index.ts
Expand Up @@ -17,7 +17,6 @@ export type { DiffOptions } from '@vitest/utils/diff'
export type {
MockedFunction,
MockedObject,
SpyInstance,
MockInstance,
Mock,
MockContext,
Expand Down
2 changes: 1 addition & 1 deletion test/config/test/resolution.test.ts
Expand Up @@ -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}"`),
)
})
Expand Down