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

Add type tests for all expect matchers #11949

Merged
merged 5 commits into from Oct 15, 2021
Merged
Show file tree
Hide file tree
Changes from 3 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
1 change: 1 addition & 0 deletions packages/expect/src/__tests__/assertionCounts.test.ts
Expand Up @@ -37,6 +37,7 @@ describe('.hasAssertions()', () => {

it('throws if expected is not undefined', () => {
jestExpect(() => {
// @ts-expect-error
SimenB marked this conversation as resolved.
Show resolved Hide resolved
jestExpect.hasAssertions(2);
}).toThrowErrorMatchingSnapshot();
});
Expand Down
2 changes: 1 addition & 1 deletion packages/expect/src/index.ts
Expand Up @@ -431,7 +431,7 @@ const expectExport = expect as Expect;

declare namespace expectExport {
export type MatcherState = JestMatcherState;
export interface Matchers<R> extends MatcherInterface<R> {}
export interface Matchers<R, T> extends MatcherInterface<R, T> {}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

T is consumed in toMatchSnapshot and toMatchInlineSnapshot. Might be useful somewhere else too.

}

export = expectExport;
17 changes: 8 additions & 9 deletions packages/expect/src/jestMatchersObject.ts
Expand Up @@ -98,15 +98,14 @@ export const setMatchers = <State extends MatcherState = MatcherState>(
}
}

expect[key] = (...sample: [unknown, ...Array<unknown>]) =>
new CustomMatcher(false, ...sample);
if (!expect.not) {
throw new Error(
'`expect.not` is not defined - please report this bug to https://github.com/facebook/jest',
);
}
expect.not[key] = (...sample: [unknown, ...Array<unknown>]) =>
new CustomMatcher(true, ...sample);
Object.defineProperty(expect, key, {
value: (...sample: [unknown, ...Array<unknown>]) =>
new CustomMatcher(false, ...sample),
});
Object.defineProperty(expect.not, key, {
value: (...sample: [unknown, ...Array<unknown>]) =>
new CustomMatcher(true, ...sample),
});
}
});

Expand Down
102 changes: 49 additions & 53 deletions packages/expect/src/types.ts
Expand Up @@ -6,8 +6,6 @@
*
*/

/* eslint-disable local/ban-types-eventually */

import type {Config} from '@jest/types';
import type * as jestMatcherUtils from 'jest-matcher-utils';
import {INTERNAL_MATCHER_FLAG} from './jestMatchersObject';
Expand Down Expand Up @@ -72,26 +70,17 @@ export type ExpectedAssertionsErrors = Array<{
expected: string;
}>;

interface InverseAsymmetricMatchers {
interface AsymmetricMatchers {
any(sample: unknown): AsymmetricMatcher;
anything(): AsymmetricMatcher;
arrayContaining(sample: Array<unknown>): AsymmetricMatcher;
objectContaining(sample: Record<string, unknown>): AsymmetricMatcher;
stringContaining(expected: string): AsymmetricMatcher;
stringMatching(expected: string | RegExp): AsymmetricMatcher;
}

interface AsymmetricMatchers extends InverseAsymmetricMatchers {
any(expectedObject: unknown): AsymmetricMatcher;
anything(): AsymmetricMatcher;
}

// Should use interface merging somehow
interface ExtraAsymmetricMatchers {
// at least one argument is needed - that's probably wrong. Should allow `expect.toBeDivisibleBy2()` like `expect.anything()`
[id: string]: (...sample: [unknown, ...Array<unknown>]) => AsymmetricMatcher;
stringContaining(sample: string): AsymmetricMatcher;
stringMatching(sample: string | RegExp): AsymmetricMatcher;
}

export type Expect<State extends MatcherState = MatcherState> = {
<T = unknown>(actual: T): Matchers<void>;
<T = unknown>(actual: T): Matchers<void, T>;
// TODO: this is added by test runners, not `expect` itself
addSnapshotSerializer(serializer: unknown): void;
assertions(numberOfAssertions: number): void;
Expand All @@ -101,43 +90,42 @@ export type Expect<State extends MatcherState = MatcherState> = {
getState(): State;
hasAssertions(): void;
setState(state: Partial<State>): void;
} & AsymmetricMatchers &
ExtraAsymmetricMatchers & {
not: InverseAsymmetricMatchers & ExtraAsymmetricMatchers;
} & AsymmetricMatchers & {
not: Omit<AsymmetricMatchers, 'any' | 'anything'>;
};

// This is a copy from https://github.com/DefinitelyTyped/DefinitelyTyped/blob/de6730f4463cba69904698035fafd906a72b9664/types/jest/index.d.ts#L570-L817
export interface Matchers<R> {
export interface Matchers<R, T = unknown> {
/**
* Ensures the last call to a mock function was provided specific args.
*/
lastCalledWith(...args: Array<unknown>): R;
lastCalledWith(...expected: [unknown, ...Array<unknown>]): R;
/**
* Ensure that the last call to a mock function has returned a specified value.
*/
lastReturnedWith(value: unknown): R;
lastReturnedWith(expected: unknown): R;
/**
* If you know how to test something, `.not` lets you test its opposite.
*/
not: Matchers<R>;
not: Matchers<R, T>;
/**
* Ensure that a mock function is called with specific arguments on an Nth call.
*/
nthCalledWith(nthCall: number, ...args: Array<unknown>): R;
nthCalledWith(nth: number, ...expected: [unknown, ...Array<unknown>]): R;
/**
* Ensure that the nth call to a mock function has returned a specified value.
*/
nthReturnedWith(n: number, value: unknown): R;
nthReturnedWith(nth: number, expected: unknown): R;
/**
* Use resolves to unwrap the value of a fulfilled promise so any other
* matcher can be chained. If the promise is rejected the assertion fails.
*/
resolves: Matchers<Promise<R>>;
resolves: Matchers<Promise<R>, T>;
/**
* 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<R>>;
rejects: Matchers<Promise<R>, T>;
/**
* Checks that a value is what you expect. It uses `===` to check strict equality.
* Don't use `toBe` with floating-point numbers.
Expand All @@ -154,13 +142,13 @@ export interface Matchers<R> {
/**
* Ensure that a mock function is called with specific arguments.
*/
toBeCalledWith(...args: Array<unknown>): R;
toBeCalledWith(...expected: [unknown, ...Array<unknown>]): R;
/**
* Using exact equality with floating point numbers is a bad idea.
* Rounding means that intuitive things fail.
* The default for numDigits is 2.
* The default for `precision` is 2.
*/
toBeCloseTo(expected: number, numDigits?: number): R;
toBeCloseTo(expected: number, precision?: number): R;
/**
* Ensure that a variable is not undefined.
*/
Expand All @@ -182,7 +170,7 @@ export interface Matchers<R> {
* Ensure that an object is an instance of a class.
* This matcher uses `instanceof` underneath.
*/
toBeInstanceOf(expected: Function): R;
toBeInstanceOf(expected: unknown): R;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Somehow unknown here feels like better idea, or?

Copy link

Choose a reason for hiding this comment

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

I honestly do not understand why it was Function before
since one can pass anything in it I agree with unknown

/**
* For comparing floating point numbers.
*/
Expand Down Expand Up @@ -237,16 +225,19 @@ export interface Matchers<R> {
/**
* Ensure that a mock function is called with specific arguments.
*/
toHaveBeenCalledWith(...args: Array<unknown>): R;
toHaveBeenCalledWith(...expected: [unknown, ...Array<unknown>]): R;
/**
* Ensure that a mock function is called with specific arguments on an Nth call.
*/
toHaveBeenNthCalledWith(nthCall: number, ...args: Array<unknown>): R;
toHaveBeenNthCalledWith(
nth: number,
...expected: [unknown, ...Array<unknown>]
Copy link

@Smrtnyk Smrtnyk Oct 14, 2021

Choose a reason for hiding this comment

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

why is ...expected: [unknown, ...Array<unknown>] better than ...expected: unknown[]? isn't it the same?

Copy link
Member

Choose a reason for hiding this comment

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

it ensures length of array is >= 1 instead of >= 0

): R;
/**
* If you have a mock function, you can use `.toHaveBeenLastCalledWith`
* to test what arguments it was last called with.
*/
toHaveBeenLastCalledWith(...args: Array<unknown>): R;
toHaveBeenLastCalledWith(...expected: [unknown, ...Array<unknown>]): 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 All @@ -263,7 +254,7 @@ export interface Matchers<R> {
* If the nth call to the mock function threw an error, then this matcher will fail
* no matter what value you provided as the expected return value.
*/
toHaveNthReturnedWith(nthCall: number, expected: unknown): R;
toHaveNthReturnedWith(nth: number, expected: unknown): R;
/**
* Use to check if property at provided reference keyPath exists for an object.
* For checking deeply nested properties in an object you may use dot notation or an array containing
Expand All @@ -277,7 +268,10 @@ export interface Matchers<R> {
*
* expect(houseForSale).toHaveProperty('kitchen.area', 20);
*/
toHaveProperty(keyPath: string | Array<string>, value?: unknown): R;
toHaveProperty(
expectedPath: string | Array<string>,
expectedValue?: unknown,
): R;
/**
* Use to test that the mock function successfully returned (i.e., did not throw an error) at least one time
*/
Expand All @@ -298,65 +292,67 @@ export interface Matchers<R> {
/**
* Used to check that a JavaScript object matches a subset of the properties of an object
*/
toMatchObject(expected: Record<string, unknown> | Array<unknown>): R;
toMatchObject(
expected: Record<string, unknown> | Array<Record<string, unknown>>,
): R;
/**
* Ensure that a mock function has returned (as opposed to thrown) at least once.
*/
toReturn(): R;
/**
* Ensure that a mock function has returned (as opposed to thrown) a specified number of times.
*/
toReturnTimes(count: number): R;
toReturnTimes(expected: number): R;
/**
* Ensure that a mock function has returned a specified value at least once.
*/
toReturnWith(value: unknown): R;
toReturnWith(expected: unknown): R;
/**
* Use to test that objects have the same types as well as structure.
*/
toStrictEqual(expected: unknown): R;
/**
* Used to test that a function throws when it is called.
*/
toThrow(error?: unknown): R;
toThrow(expected?: unknown): R;
/**
* If you want to test that a specific error is thrown inside a function.
*/
toThrowError(error?: unknown): R;
toThrowError(expected?: unknown): R;

/* TODO: START snapshot matchers are not from `expect`, the types should not be here */
/**
* This ensures that a value matches the most recent snapshot with property matchers.
* Check out [the Snapshot Testing guide](https://jestjs.io/docs/snapshot-testing) for more information.
*/
toMatchSnapshot<T extends {[P in keyof R]: unknown}>(
propertyMatchers: Partial<T>,
snapshotName?: string,
): R;
toMatchSnapshot(hint?: string): R;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Less specific overloads goes first for better errors.

/**
* This ensures that a value matches the most recent snapshot.
* Check out [the Snapshot Testing guide](https://jestjs.io/docs/snapshot-testing) for more information.
*/
toMatchSnapshot(snapshotName?: string): R;
toMatchSnapshot<U extends Record<keyof T, unknown>>(
propertyMatchers: Partial<U>,
hint?: string,
): R;
/**
* This ensures that a value matches the most recent snapshot with property matchers.
* Instead of writing the snapshot value to a .snap file, it will be written into the source code automatically.
* Check out [the Snapshot Testing guide](https://jestjs.io/docs/snapshot-testing) for more information.
*/
toMatchInlineSnapshot<T extends {[P in keyof R]: unknown}>(
propertyMatchers: Partial<T>,
snapshot?: string,
): R;
toMatchInlineSnapshot(snapshot?: string): R;
/**
* This ensures that a value matches the most recent snapshot with property matchers.
* Instead of writing the snapshot value to a .snap file, it will be written into the source code automatically.
* Check out [the Snapshot Testing guide](https://jestjs.io/docs/snapshot-testing) for more information.
*/
toMatchInlineSnapshot(snapshot?: string): R;
toMatchInlineSnapshot<U extends Record<keyof T, unknown>>(
propertyMatchers: Partial<U>,
snapshot?: string,
): R;
/**
* Used to test that a function throws a error matching the most recent snapshot when it is called.
*/
toThrowErrorMatchingSnapshot(): R;
toThrowErrorMatchingSnapshot(hint?: string): R;
/**
* Used to test that a function throws a error matching the most recent snapshot when it is called.
* Instead of writing the snapshot value to a .snap file, it will be written into the source code automatically.
Expand Down