Skip to content

Commit

Permalink
feat(expect, @jest/expect): support type inference for function param…
Browse files Browse the repository at this point in the history
…e… (#13268)

Co-authored-by: Tom Mrazauskas <tom@mrazauskas.de>
  • Loading branch information
royhadad and mrazauskas committed Sep 20, 2022
1 parent 4de8911 commit d0f1b0a
Show file tree
Hide file tree
Showing 4 changed files with 51 additions and 14 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -2,6 +2,7 @@

### Features

- `[expect, @jest/expect]` support type inference for function parameters in `CalledWith` assertions ([#13268](https://github.com/facebook/jest/pull/13268))
- `[@jest/environment, jest-runtime]` Allow `jest.requireActual` and `jest.requireMock` to take a type argument ([#13253](https://github.com/facebook/jest/pull/13253))
- `[@jest/environment]` Allow `jest.mock` and `jest.doMock` to take a type argument ([#13254](https://github.com/facebook/jest/pull/13254))
- `[@jest/fake-timers]` Add `jest.now()` to return the current fake clock time ([#13244](https://github.com/facebook/jest/pull/13244), [13246](https://github.com/facebook/jest/pull/13246))
Expand Down
31 changes: 18 additions & 13 deletions packages/expect/src/types.ts
Expand Up @@ -94,9 +94,9 @@ export interface BaseExpect {
}

export type Expect = {
<T = unknown>(actual: T): Matchers<void> &
Inverse<Matchers<void>> &
PromiseMatchers;
<T = unknown>(actual: T): Matchers<void, T> &
Inverse<Matchers<void, T>> &
PromiseMatchers<T>;
} & BaseExpect &
AsymmetricMatchers &
Inverse<Omit<AsymmetricMatchers, 'any' | 'anything'>>;
Expand All @@ -118,32 +118,34 @@ export interface AsymmetricMatchers {
stringMatching(sample: string | RegExp): AsymmetricMatcher;
}

type PromiseMatchers = {
type PromiseMatchers<T = unknown> = {
/**
* Unwraps the reason of a rejected promise so any other matcher can be chained.
* If the promise is fulfilled the assertion fails.
*/
rejects: Matchers<Promise<void>> & Inverse<Matchers<Promise<void>>>;
rejects: Matchers<Promise<void>> & Inverse<Matchers<Promise<void>, T>>;
/**
* Unwraps the value of a fulfilled promise so any other matcher can be chained.
* If the promise is rejected the assertion fails.
*/
resolves: Matchers<Promise<void>> & Inverse<Matchers<Promise<void>>>;
resolves: Matchers<Promise<void>> & Inverse<Matchers<Promise<void>, T>>;
};

export interface Matchers<R extends void | Promise<void>> {
type EnsureFunctionLike<T> = T extends (...args: any) => any ? T : never;

export interface Matchers<R extends void | Promise<void>, T = unknown> {
/**
* Ensures the last call to a mock function was provided specific args.
*/
lastCalledWith(...expected: Array<unknown>): R;
lastCalledWith(...expected: Parameters<EnsureFunctionLike<T>>): R;
/**
* Ensure that the last call to a mock function has returned a specified value.
*/
lastReturnedWith(expected: unknown): R;
/**
* Ensure that a mock function is called with specific arguments on an Nth call.
*/
nthCalledWith(nth: number, ...expected: Array<unknown>): R;
nthCalledWith(nth: number, ...expected: Parameters<EnsureFunctionLike<T>>): R;
/**
* Ensure that the nth call to a mock function has returned a specified value.
*/
Expand All @@ -164,7 +166,7 @@ export interface Matchers<R extends void | Promise<void>> {
/**
* Ensure that a mock function is called with specific arguments.
*/
toBeCalledWith(...expected: Array<unknown>): R;
toBeCalledWith(...expected: Parameters<EnsureFunctionLike<T>>): R;
/**
* Using exact equality with floating point numbers is a bad idea.
* Rounding means that intuitive things fail.
Expand Down Expand Up @@ -247,16 +249,19 @@ export interface Matchers<R extends void | Promise<void>> {
/**
* Ensure that a mock function is called with specific arguments.
*/
toHaveBeenCalledWith(...expected: Array<unknown>): R;
toHaveBeenCalledWith(...expected: Parameters<EnsureFunctionLike<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: Parameters<EnsureFunctionLike<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: Parameters<EnsureFunctionLike<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
2 changes: 1 addition & 1 deletion packages/jest-expect/src/types.ts
Expand Up @@ -29,7 +29,7 @@ type Inverse<Matchers> = {
not: Matchers;
};

type JestMatchers<R extends void | Promise<void>, T> = Matchers<R> &
type JestMatchers<R extends void | Promise<void>, T> = Matchers<R, T> &
SnapshotMatchers<R, T>;

type PromiseMatchers<T = unknown> = {
Expand Down
31 changes: 31 additions & 0 deletions packages/jest-types/__typetests__/expect.test.ts
Expand Up @@ -218,6 +218,37 @@ expectType<void>(expect(jest.fn()).toHaveBeenCalledWith());
expectType<void>(expect(jest.fn()).toHaveBeenCalledWith(123));
expectType<void>(expect(jest.fn()).toHaveBeenCalledWith(123, 'value'));

/**
* type inference for "CalledWith" matchers parameters
*/
expectError(expect(jest.fn<(a: string) => void>()).toHaveBeenCalledWith(123));
expectError(
expect(jest.fn<(a: string) => void>()).toHaveBeenNthCalledWith(1, 123),
);
expectError(
expect(jest.fn<(a: string) => void>()).toHaveBeenLastCalledWith(123),
);
expectType<void>(
expect(
jest.fn<(a: string, b: number, c?: boolean) => void>(),
).toHaveBeenCalledWith('value', 123),
);
expectType<void>(
expect(
jest.fn<(a: string, b: number, c?: boolean) => void>(),
).toHaveBeenCalledWith('value', 123, true),
);
expectError(
expect(
jest.fn<(a: string, b: number, c?: boolean) => void>(),
).toHaveBeenCalledWith(123, 'value'),
);
expectError(
expect(
jest.fn<(a: string, b: number, c?: boolean) => void>(),
).toHaveBeenCalledWith('value', 123, 'not a boolean'),
);

expectType<void>(expect(jest.fn()).lastCalledWith());
expectType<void>(expect(jest.fn()).lastCalledWith('value'));
expectType<void>(expect(jest.fn()).lastCalledWith('value', 123));
Expand Down

0 comments on commit d0f1b0a

Please sign in to comment.