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): simplify mock function generic types and align with jest #4784

Merged
merged 34 commits into from
Jun 23, 2024
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
e2d6312
fix!(spy): align vi.fn and vi.Mock typings to jest
hi-ogawa Dec 20, 2023
c5781fe
fix: put back `any` for starter
hi-ogawa Dec 20, 2023
8a13a22
chore: lint
hi-ogawa Dec 20, 2023
a87d320
chore: back to "unknown"
hi-ogawa Dec 20, 2023
a0e8876
Merge branch 'main' into feat-breaking-fn-mock-typing
hi-ogawa Jan 3, 2024
f79f1c8
chore: revert lock
hi-ogawa Jan 3, 2024
5efee95
chore: more any type
hi-ogawa Jan 3, 2024
ba54044
Merge branch 'main' into feat-breaking-fn-mock-typing
hi-ogawa Feb 8, 2024
c17bac0
Merge branch 'main' into feat-breaking-fn-mock-typing
hi-ogawa Apr 23, 2024
4f50db5
Merge branch 'main' into feat-breaking-fn-mock-typing
hi-ogawa Apr 23, 2024
a3bdb7a
fix: fix types
hi-ogawa Apr 23, 2024
e034a05
test: fix and add
hi-ogawa Apr 23, 2024
eb1c442
fix: give up "ts/method-signature-style"
hi-ogawa Apr 23, 2024
d0903b6
chore: comment
hi-ogawa Apr 23, 2024
fef7e5f
chore: cleanup
hi-ogawa Apr 23, 2024
cb777f1
chore: cleanup
hi-ogawa Apr 23, 2024
2363d57
Merge branch 'main' into feat-breaking-fn-mock-typing
hi-ogawa Apr 23, 2024
7b59e10
feat: replace `any` with `unknown` for default types
hi-ogawa Apr 23, 2024
74984d7
Merge branch 'main' into feat-breaking-fn-mock-typing
hi-ogawa Jun 20, 2024
67b1887
fix: fix types
hi-ogawa Jun 20, 2024
8ff1526
chore: fix format 1
hi-ogawa Jun 20, 2024
3527342
chore: fix format 2
hi-ogawa Jun 20, 2024
61f6ecc
chore: fix format 3
hi-ogawa Jun 20, 2024
0d47f57
docs: jest migration
hi-ogawa Jun 20, 2024
5e8b508
docs: v2 migration
hi-ogawa Jun 20, 2024
1a7a3c8
docs: tweak
hi-ogawa Jun 20, 2024
140c25d
fix: more any types
hi-ogawa Jun 20, 2024
532684b
docs: also call out `any` to `unknown`
hi-ogawa Jun 20, 2024
bfa8ebf
Merge branch 'main' into feat-breaking-fn-mock-typing
hi-ogawa Jun 20, 2024
e5c85c1
chore: revert default UknownProcedure to AnyProcedure
hi-ogawa Jun 21, 2024
b417c3e
Merge branch 'main' into feat-breaking-fn-mock-typing
hi-ogawa Jun 21, 2024
0fde92a
Merge remote-tracking branch 'origin/feat-breaking-fn-mock-typing' in…
hi-ogawa Jun 21, 2024
72adccf
chore: more any
hi-ogawa Jun 21, 2024
1946007
Merge remote-tracking branch 'origin/feat-breaking-fn-mock-typing' in…
hi-ogawa Jun 21, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 83 additions & 74 deletions packages/spy/src/index.ts
Original file line number Diff line number Diff line change
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)
*/
/* 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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Loading
Loading