Skip to content

Commit

Permalink
feat: Add type inference to parameters of 'have been called with' fun…
Browse files Browse the repository at this point in the history
…ctions (#15034)

WIP will be amended
  • Loading branch information
eyalroth committed Jun 15, 2024
1 parent c54bccd commit 34acf3e
Show file tree
Hide file tree
Showing 2 changed files with 243 additions and 3 deletions.
104 changes: 101 additions & 3 deletions packages/expect/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

import type {EqualsFunction, Tester} from '@jest/expect-utils';
import type * as jestMatcherUtils from 'jest-matcher-utils';
// TODO does this require dependency in package.json?
import type {Mock} from 'jest-mock';
import type {INTERNAL_MATCHER_FLAG} from './jestMatchersObject';

export type SyncExpectationResult = {
Expand Down Expand Up @@ -231,16 +233,16 @@ export interface Matchers<R extends void | Promise<void>, T = unknown> {
/**
* Ensure that a mock function is called with specific arguments.
*/
toHaveBeenCalledWith(...expected: Array<unknown>): R;
toHaveBeenCalledWith(...expected: FunctionParameters<T>): R;
/**
* Ensure that a mock function is called with specific arguments on an Nth call.
*/
toHaveBeenNthCalledWith(nth: number, ...expected: Array<unknown>): R;
toHaveBeenNthCalledWith(nth: number, ...expected: FunctionParameters<T>): R;
/**
* If you have a mock function, you can use `.toHaveBeenLastCalledWith`
* to test what arguments it was last called with.
*/
toHaveBeenLastCalledWith(...expected: Array<unknown>): R;
toHaveBeenLastCalledWith(...expected: FunctionParameters<T>): R;
/**
* Use to test the specific value that a mock function last returned.
* If the last call to the mock function threw an error, then this matcher will fail
Expand Down Expand Up @@ -307,3 +309,99 @@ export interface Matchers<R extends void | Promise<void>, T = unknown> {
*/
toThrow(expected?: unknown): R;
}

// TODO add more overload options (up to 10?)
type FunctionParameters<M> =
M extends Mock<infer F>
? F extends OverloadedFunction4
? FunctionParameters4<F>
: F extends OverloadedFunction3
? FunctionParameters3<F>
: F extends OverloadedFunction2
? FunctionParameters2<F>
: F extends NoOverloadsFunction
? FunctionParameters1<F>
: Array<unknown>
: Array<unknown>;

type NoOverloadsFunction = (...args: any) => any;

type OverloadedFunction2 = {
(...args: any): any;
(...args: any): any;
};
type OverloadedFunction3 = {
(...args: any): any;
(...args: any): any;
(...args: any): any;
};
type OverloadedFunction4 = {
(...args: any): any;
(...args: any): any;
(...args: any): any;
(...args: any): any;
};

type WithAsymmetricMatchers<P extends Array<any>> = {
[K in keyof P]: P[K] | AsymmetricMatcher;
};

type FunctionParameters1<F extends NoOverloadsFunction> = F extends (
...args: infer P
) => any
? WithAsymmetricMatchers<P>
: never;

type FunctionParameters2<F extends OverloadedFunction2> = F extends {
(...args: infer P1): any;
(...args: infer P2): any;
}
? WithAsymmetricMatchers<P1> | WithAsymmetricMatchers<P2>
: never;

type FunctionParameters3<F extends OverloadedFunction2> = F extends {
(...args: infer P1): any;
(...args: infer P2): any;
(...args: infer P3): any;
}
?
| WithAsymmetricMatchers<P1>
| WithAsymmetricMatchers<P2>
| WithAsymmetricMatchers<P3>
: never;

type FunctionParameters4<F extends OverloadedFunction2> = F extends {
(...args: infer P1): any;
(...args: infer P2): any;
(...args: infer P3): any;
(...args: infer P4): any;
}
?
| WithAsymmetricMatchers<P1>
| WithAsymmetricMatchers<P2>
| WithAsymmetricMatchers<P3>
| WithAsymmetricMatchers<P4>
: never;

// TODO delete
// const ama: AsymmetricMatchers = null;
// const x: Mock<(s: string, n: number) => void> = null;
// const ex: Expect = null;
// const y = ex(x);
// y.toHaveBeenCalledWith('s', 1);
// y.toHaveBeenCalledWith(ama.stringContaining('sd'), 1);
// y.toHaveBeenCalledWith();

// function withOverload(): void;
// function withOverload(n: number): void;
// function withOverload(n: number, s: string): void;
// function withOverload(n?: number, s?: string): void {}
//
// const pp: FunctionParameters<Mock<typeof withOverload>> = null;
//
// const wo: typeof withOverload = null;
// const x: Mock<typeof withOverload> = null;
// const ex: Expect = null;
// const y = ex(x);
// y.toHaveBeenCalledWith();
// y.toHaveBeenCalledWith(1, 's');
142 changes: 142 additions & 0 deletions packages/jest-types/__typetests__/expect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,147 @@ expect(
),
).type.toBeVoid();

expect(
jestExpect(jest.fn<() => void>()).toHaveBeenCalledWith(),
).type.toBeVoid();
expect(
jestExpect(jest.fn<() => void>()).toHaveBeenCalledWith(1),
).type.toRaiseError();

expect(
jestExpect(jest.fn<(n?: number) => void>()).toHaveBeenCalledWith(),
).type.toBeVoid();
expect(
jestExpect(jest.fn<(n?: number) => void>()).toHaveBeenCalledWith(123),
).type.toBeVoid();
expect(
jestExpect(jest.fn<(n?: number) => void>()).toHaveBeenCalledWith('value'),
).type.toRaiseError();

expect(
jestExpect(jest.fn<(n: number) => void>()).toHaveBeenCalledWith(123),
).type.toBeVoid();
expect(
jestExpect(jest.fn<(n: number) => void>()).toHaveBeenCalledWith(),
).type.toRaiseError();
expect(
jestExpect(jest.fn<(n: number) => void>()).toHaveBeenCalledWith('value'),
).type.toRaiseError();

expect(
jestExpect(jest.fn<(s: string) => void>()).toHaveBeenCalledWith('value'),
).type.toBeVoid();
expect(
jestExpect(jest.fn<(s: string) => void>()).toHaveBeenCalledWith(),
).type.toRaiseError();
expect(
jestExpect(jest.fn<(s: string) => void>()).toHaveBeenCalledWith(123),
).type.toRaiseError();

expect(
jestExpect(jest.fn<(n: number, s: string) => void>()).toHaveBeenCalledWith(
123,
'value',
),
).type.toBeVoid();
expect(
jestExpect(jest.fn<(n: number, s: string) => void>()).toHaveBeenCalledWith(),
).type.toRaiseError();
expect(
jestExpect(jest.fn<(n: number, s: string) => void>()).toHaveBeenCalledWith(
123,
),
).type.toRaiseError();
expect(
jestExpect(jest.fn<(n: number, s: string) => void>()).toHaveBeenCalledWith(
123,
123,
),
).type.toRaiseError();
expect(
jestExpect(jest.fn<(n: number, s: string) => void>()).toHaveBeenCalledWith(
'value',
'value',
),
).type.toRaiseError();
expect(
jestExpect(jest.fn<(n: number, s: string) => void>()).toHaveBeenCalledWith(
'value',
123,
),
).type.toRaiseError();

expect(
jestExpect(jest.fn<(n: number, s?: string) => void>()).toHaveBeenCalledWith(
123,
'value',
),
).type.toBeVoid();
expect(
jestExpect(jest.fn<(n: number, s?: string) => void>()).toHaveBeenCalledWith(
123,
),
).type.toBeVoid();
expect(
jestExpect(jest.fn<(n: number, s?: string) => void>()).toHaveBeenCalledWith(),
).type.toRaiseError();
expect(
jestExpect(jest.fn<(n: number, s?: string) => void>()).toHaveBeenCalledWith(
'value',
),
).type.toRaiseError();
expect(
jestExpect(jest.fn<(n: number, s?: string) => void>()).toHaveBeenCalledWith(
'value',
'value',
),
).type.toRaiseError();
expect(
jestExpect(jest.fn<(n: number, s?: string) => void>()).toHaveBeenCalledWith(
'value',
123,
),
).type.toRaiseError();
expect(
jestExpect(jest.fn<(n: number, s?: string) => void>()).toHaveBeenCalledWith(
123,
123,
),
).type.toRaiseError();

// TODO test overloaded with union type?
function overloaded(): void;
// eslint-disable-next-line @typescript-eslint/unified-signatures
function overloaded(n: number): void;
// eslint-disable-next-line @typescript-eslint/unified-signatures
function overloaded(n: number, s: string): void;
function overloaded(n?: number, s?: string): void {
// noop
}

expect(
jestExpect(jest.fn<typeof overloaded>()).toHaveBeenCalledWith(),
).type.toBeVoid();
expect(
jestExpect(jest.fn<typeof overloaded>()).toHaveBeenCalledWith(123),
).type.toBeVoid();
expect(
jestExpect(jest.fn<typeof overloaded>()).toHaveBeenCalledWith(123, 'value'),
).type.toBeVoid();
expect(
jestExpect(jest.fn<typeof overloaded>()).toHaveBeenCalledWith(123, 123),
).type.toRaiseError();
expect(
jestExpect(jest.fn<typeof overloaded>()).toHaveBeenCalledWith('value'),
).type.toRaiseError();
expect(
jestExpect(jest.fn<typeof overloaded>()).toHaveBeenCalledWith(
'value',
'value',
),
).type.toRaiseError();

// TODO add typed parameters tests
expect(jestExpect(jest.fn()).toHaveBeenLastCalledWith()).type.toBeVoid();
expect(jestExpect(jest.fn()).toHaveBeenLastCalledWith('value')).type.toBeVoid();
expect(jestExpect(jest.fn()).toHaveBeenLastCalledWith(123)).type.toBeVoid();
Expand All @@ -322,6 +463,7 @@ expect(
).toHaveBeenLastCalledWith(jestExpect.stringContaining('value'), 123),
).type.toBeVoid();

// TODO add typed parameters tests
expect(jestExpect(jest.fn()).toHaveBeenNthCalledWith(2)).type.toBeVoid();
expect(
jestExpect(jest.fn()).toHaveBeenNthCalledWith(1, 'value'),
Expand Down

0 comments on commit 34acf3e

Please sign in to comment.