Skip to content

Commit 328267b

Browse files
authored
feat(types): support mocked constructors (#26)
1 parent 24e1b63 commit 328267b

File tree

6 files changed

+84
-31
lines changed

6 files changed

+84
-31
lines changed

src/behaviors.ts

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,30 @@
11
import { equals } from '@vitest/expect'
22

3-
import type { AnyFunction, WithMatchers } from './types.ts'
3+
import type {
4+
AnyCallable,
5+
AnyFunction,
6+
ExtractParameters,
7+
ExtractReturnType,
8+
WithMatchers,
9+
} from './types.ts'
410

511
export interface WhenOptions {
612
times?: number
713
}
814

9-
export interface BehaviorStack<TFunc extends AnyFunction> {
10-
use: (args: Parameters<TFunc>) => BehaviorEntry<Parameters<TFunc>> | undefined
15+
export interface BehaviorStack<TFunc extends AnyCallable> {
16+
use: (
17+
args: ExtractParameters<TFunc>,
18+
) => BehaviorEntry<ExtractParameters<TFunc>> | undefined
1119

12-
getAll: () => readonly BehaviorEntry<Parameters<TFunc>>[]
20+
getAll: () => readonly BehaviorEntry<ExtractParameters<TFunc>>[]
1321

14-
getUnmatchedCalls: () => readonly Parameters<TFunc>[]
22+
getUnmatchedCalls: () => readonly ExtractParameters<TFunc>[]
1523

1624
bindArgs: (
17-
args: WithMatchers<Parameters<TFunc>>,
25+
args: WithMatchers<ExtractParameters<TFunc>>,
1826
options: WhenOptions,
19-
) => BoundBehaviorStack<ReturnType<TFunc>>
27+
) => BoundBehaviorStack<ExtractReturnType<TFunc>>
2028
}
2129

2230
export interface BoundBehaviorStack<TReturn> {
@@ -55,10 +63,10 @@ export interface BehaviorOptions<TValue> {
5563
}
5664

5765
export const createBehaviorStack = <
58-
TFunc extends AnyFunction,
66+
TFunc extends AnyCallable,
5967
>(): BehaviorStack<TFunc> => {
60-
const behaviors: BehaviorEntry<Parameters<TFunc>>[] = []
61-
const unmatchedCalls: Parameters<TFunc>[] = []
68+
const behaviors: BehaviorEntry<ExtractParameters<TFunc>>[] = []
69+
const unmatchedCalls: ExtractParameters<TFunc>[] = []
6270

6371
return {
6472
getAll: () => behaviors,

src/debug.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55

66
import { type Behavior, BehaviorType } from './behaviors'
77
import { getBehaviorStack, validateSpy } from './stubs'
8-
import type { AnyFunction, MockInstance } from './types'
8+
import type { AnyCallable, MockInstance } from './types'
99

1010
export interface DebugResult {
1111
name: string
@@ -20,7 +20,7 @@ export interface Stubbing {
2020
calls: readonly unknown[][]
2121
}
2222

23-
export const getDebug = <TFunc extends AnyFunction>(
23+
export const getDebug = <TFunc extends AnyCallable>(
2424
spy: TFunc | MockInstance<TFunc>,
2525
): DebugResult => {
2626
const target = validateSpy<TFunc>(spy)

src/stubs.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,21 @@ import {
44
createBehaviorStack,
55
} from './behaviors.ts'
66
import { NotAMockFunctionError } from './errors.ts'
7-
import type { AnyFunction, MockInstance } from './types.ts'
7+
import type {
8+
AnyCallable,
9+
AnyFunction,
10+
ExtractParameters,
11+
MockInstance,
12+
} from './types.ts'
813

914
const BEHAVIORS_KEY = Symbol('behaviors')
1015

11-
interface WhenStubImplementation<TFunc extends AnyFunction> {
12-
(...args: Parameters<TFunc>): unknown
16+
interface WhenStubImplementation<TFunc extends AnyCallable> {
17+
(...args: ExtractParameters<TFunc>): unknown
1318
[BEHAVIORS_KEY]: BehaviorStack<TFunc>
1419
}
1520

16-
export const configureStub = <TFunc extends AnyFunction>(
21+
export const configureStub = <TFunc extends AnyCallable>(
1722
maybeSpy: unknown,
1823
): BehaviorStack<TFunc> => {
1924
const spy = validateSpy<TFunc>(maybeSpy)
@@ -26,10 +31,10 @@ export const configureStub = <TFunc extends AnyFunction>(
2631
const behaviors = createBehaviorStack<TFunc>()
2732
const fallbackImplementation = spy.getMockImplementation()
2833

29-
const implementation = (...args: Parameters<TFunc>) => {
34+
const implementation = (...args: ExtractParameters<TFunc>) => {
3035
const behavior = behaviors.use(args)?.behavior ?? {
3136
type: BehaviorType.DO,
32-
callback: fallbackImplementation,
37+
callback: fallbackImplementation as AnyFunction | undefined,
3338
}
3439

3540
switch (behavior.type) {
@@ -63,7 +68,7 @@ export const configureStub = <TFunc extends AnyFunction>(
6368
return behaviors
6469
}
6570

66-
export const validateSpy = <TFunc extends AnyFunction>(
71+
export const validateSpy = <TFunc extends AnyCallable>(
6772
maybeSpy: unknown,
6873
): MockInstance<TFunc> => {
6974
if (
@@ -81,7 +86,7 @@ export const validateSpy = <TFunc extends AnyFunction>(
8186
throw new NotAMockFunctionError(maybeSpy)
8287
}
8388

84-
export const getBehaviorStack = <TFunc extends AnyFunction>(
89+
export const getBehaviorStack = <TFunc extends AnyCallable>(
8590
spy: MockInstance<TFunc>,
8691
): BehaviorStack<TFunc> | undefined => {
8792
const existingImplementation = spy.getMockImplementation() as

src/types.ts

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,32 @@
11
/** Common type definitions. */
22
import type { AsymmetricMatcher } from '@vitest/expect'
33

4-
/** Any function, for use in `extends` */
4+
/** Any function. */
55
export type AnyFunction = (...args: never[]) => unknown
66

7+
/** Any constructor. */
8+
export type AnyConstructor = new (...args: never[]) => unknown
9+
10+
/** Any callable, for use in `extends` */
11+
export type AnyCallable = AnyFunction | AnyConstructor
12+
13+
/** Extract parameters from either a function or constructor. */
14+
export type ExtractParameters<T> = T extends new (...args: infer P) => unknown
15+
? P
16+
: T extends (...args: infer P) => unknown
17+
? P
18+
: never
19+
20+
/** Extract return type from either a function or constructor */
21+
export type ExtractReturnType<T> = T extends new (...args: never[]) => infer R
22+
? R
23+
: T extends (...args: never[]) => infer R
24+
? R
25+
: never
26+
727
/** Accept a value or an AsymmetricMatcher in an arguments array */
828
export type WithMatchers<T extends unknown[]> = {
9-
[K in keyof T]: T[K] | AsymmetricMatcher<unknown>
29+
[K in keyof T]: AsymmetricMatcher<unknown> | T[K]
1030
}
1131

1232
/**
@@ -15,13 +35,13 @@ export type WithMatchers<T extends unknown[]> = {
1535
* Used to ensure backwards compatibility
1636
* with older versions of Vitest.
1737
*/
18-
export interface MockInstance<TFunc extends AnyFunction = AnyFunction> {
38+
export interface MockInstance<TFunc extends AnyCallable = AnyCallable> {
1939
getMockName(): string
2040
getMockImplementation(): TFunc | undefined
2141
mockImplementation: (impl: TFunc) => this
2242
mock: MockContext<TFunc>
2343
}
2444

25-
export interface MockContext<TFunc extends AnyFunction> {
26-
calls: Parameters<TFunc>[]
45+
export interface MockContext<TFunc extends AnyCallable> {
46+
calls: ExtractParameters<TFunc>[]
2747
}

src/vitest-when.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,22 @@
11
import type { WhenOptions } from './behaviors.ts'
22
import { type DebugResult, getDebug } from './debug.ts'
33
import { configureStub } from './stubs.ts'
4-
import type { AnyFunction, MockInstance, WithMatchers } from './types.ts'
4+
import type {
5+
AnyCallable,
6+
ExtractParameters,
7+
ExtractReturnType,
8+
MockInstance,
9+
WithMatchers,
10+
} from './types.ts'
511

612
export { type Behavior, BehaviorType, type WhenOptions } from './behaviors.ts'
713
export type { DebugResult, Stubbing } from './debug.ts'
814
export * from './errors.ts'
915

10-
export interface StubWrapper<TFunc extends AnyFunction> {
11-
calledWith<TArgs extends Parameters<TFunc>>(
16+
export interface StubWrapper<TFunc extends AnyCallable> {
17+
calledWith<TArgs extends ExtractParameters<TFunc>>(
1218
...args: WithMatchers<TArgs>
13-
): Stub<TArgs, ReturnType<TFunc>>
19+
): Stub<TArgs, ExtractReturnType<TFunc>>
1420
}
1521

1622
export interface Stub<TArgs extends unknown[], TReturn> {
@@ -21,7 +27,7 @@ export interface Stub<TArgs extends unknown[], TReturn> {
2127
thenDo: (...callbacks: ((...args: TArgs) => TReturn)[]) => void
2228
}
2329

24-
export const when = <TFunc extends AnyFunction>(
30+
export const when = <TFunc extends AnyCallable>(
2531
spy: TFunc | MockInstance<TFunc>,
2632
options: WhenOptions = {},
2733
): StubWrapper<TFunc> => {
@@ -46,7 +52,7 @@ export interface DebugOptions {
4652
log?: boolean
4753
}
4854

49-
export const debug = <TFunc extends AnyFunction>(
55+
export const debug = <TFunc extends AnyCallable>(
5056
spy: TFunc | MockInstance<TFunc>,
5157
options: DebugOptions = {},
5258
): DebugResult => {

test/typing.test-d.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,20 @@ describe('vitest-when type signatures', () => {
9191
subject.when(simple).calledWith(expect.any(Number))
9292
subject.when(complex).calledWith(expect.objectContaining({ a: 1 }))
9393
})
94+
95+
it('should accept a class constructor', () => {
96+
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
97+
class TestClass {
98+
constructor(input: number) {
99+
throw new Error(`TestClass(${input})`)
100+
}
101+
}
102+
103+
subject.when(TestClass).calledWith(42)
104+
105+
// @ts-expect-error: args wrong type
106+
subject.when(TestClass).calledWith('42')
107+
})
94108
})
95109

96110
function untyped(...args: any[]): any {

0 commit comments

Comments
 (0)