jest updated - corrected Matchers, added ExtendedExpect (#39243)

* corrected Matchers, added ExtendedExpect

* Matchers<R,T>

* updating dependent packages

* refactor and additional getState/setState/matcher context

* MatcherContext

* Matchers<R, T> for jest-axe
This commit is contained in:
Tony Hallett 2019-10-25 21:12:50 +01:00 committed by Wesley Wigham
parent b458bcb4ca
commit 9e6921fcd9
10 changed files with 211 additions and 59 deletions

View File

@ -51,7 +51,7 @@ interface ExpectPuppeteer {
declare global {
namespace jest {
// tslint:disable-next-line no-empty-interface
interface Matchers<R> {
interface Matchers<R, T> {
// These must all match the ExpectPuppeteer interface above.
// We can't extend from it directly because some method names conflict in type-incompatible ways.
toClick(selector: string, options?: ExpectToClickOptions): Promise<void>;

View File

@ -79,7 +79,7 @@ export const toHaveNoViolations: {
declare global {
namespace jest {
interface Matchers<R> {
interface Matchers<R, T> {
toHaveNoViolations(): R;
}
}

View File

@ -8,6 +8,6 @@
declare namespace jest {
interface Expect {
<T = any>(actual: T, message: string): Matchers<T>;
<T = any>(actual: T, message: string): JestMatchers<T>;
}
}

View File

@ -80,7 +80,7 @@ export function configureToMatchImageSnapshot(options: MatchImageSnapshotOptions
declare global {
namespace jest {
interface Matchers<R> {
interface Matchers<R, T> {
toMatchImageSnapshot(options?: MatchImageSnapshotOptions): R;
}
}

View File

@ -10,7 +10,7 @@ import * as ajv from 'ajv';
declare global {
namespace jest {
interface Matchers<R> {
interface Matchers<R, T> {
toBeValidSchema(): R;
toMatchSchema(schema: object): R;
}

View File

@ -6,7 +6,7 @@
/// <reference types="jest" />
declare namespace jest {
interface Matchers<R> {
interface Matchers<R, T> {
toBeOneOf(expected: any[]): R;
}
}

View File

@ -8,7 +8,7 @@
declare global {
namespace jest {
interface Matchers<R> {
interface Matchers<R, T> {
toMatchSpecificSnapshot(snapshotFilename: string): R;
}
}

104
types/jest/index.d.ts vendored
View File

@ -23,6 +23,7 @@
// ExE Boss <https://github.com/ExE-Boss>
// Alex Bolenok <https://github.com/quassnoi>
// Mario Beltrán Alarcón <https://github.com/Belco90>
// Tony Hallett <https://github.com/tonyhallett>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// TypeScript Version: 3.0
@ -396,8 +397,9 @@ declare namespace jest {
}
interface MatcherUtils {
readonly expand: boolean;
readonly isNot: boolean;
readonly dontThrow: () => void;
readonly promise: string;
utils: {
readonly EXPECTED_COLOR: (text: string) => string;
readonly RECEIVED_COLOR: (text: string) => string;
@ -426,21 +428,23 @@ declare namespace jest {
* This is a deep-equality function that will return true if two objects have the same values (recursively).
*/
equals(a: any, b: any): boolean;
[other: string]: any;
}
interface ExpectExtendMap {
[key: string]: CustomMatcher;
}
type MatcherContext = MatcherUtils & Readonly<MatcherState>;
type CustomMatcher = (
this: MatcherUtils,
this: MatcherContext,
received: any,
...actual: any[]
) => CustomMatcherResult | Promise<CustomMatcherResult>;
interface CustomMatcherResult {
pass: boolean;
message: string | (() => string);
message: () => string;
}
interface SnapshotSerializerOptions {
@ -516,7 +520,15 @@ declare namespace jest {
*/
stringContaining(str: string): any;
}
interface MatcherState {
assertionCalls: number;
currentTestName: string;
expand: boolean;
expectedAssertionsNumber: number;
isExpectingAssertions?: boolean;
suppressedErrors: Error[];
testPath: string;
}
/**
* The `expect` function is used every time you want to test a value.
* You will rarely call `expect` by itself.
@ -528,7 +540,7 @@ declare namespace jest {
*
* @param actual The value to apply matchers against.
*/
<T = any>(actual: T): Matchers<T>;
<T = any>(actual: T): JestMatchers<T>;
/**
* Matches anything but null or undefined. You can use it inside `toEqual` or `toBeCalledWith` instead
* of a literal value. For example, if you want to check that a mock function is called with a
@ -601,9 +613,30 @@ declare namespace jest {
stringContaining(str: string): any;
not: InverseAsymmetricMatchers;
setState(state: object): void;
getState(): MatcherState & Record<string, any>;
}
interface Matchers<R> {
type JestMatchers<T> = JestMatchersShape<Matchers<void, T>, Matchers<Promise<void>, T>>;
type JestMatchersShape<TNonPromise extends {} = {}, TPromise extends {} = {}> = {
/**
* 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: AndNot<TPromise>,
/**
* Unwraps the reason of a rejected promise so any other matcher can be chained.
* If the promise is fulfilled the assertion fails.
*/
rejects: AndNot<TPromise>
} & AndNot<TNonPromise>;
type AndNot<T> = T & {
not: T
};
// should be R extends void|Promise<void> but getting dtslint error
interface Matchers<R, T> {
/**
* Ensures the last call to a mock function was provided specific args.
*/
@ -612,10 +645,6 @@ declare namespace jest {
* Ensure that the last call to a mock function has returned a specified value.
*/
lastReturnedWith(value: any): R;
/**
* If you know how to test something, `.not` lets you test its opposite.
*/
not: Matchers<R>;
/**
* Ensure that a mock function is called with specific arguments on an Nth call.
*/
@ -624,16 +653,6 @@ declare namespace jest {
* Ensure that the nth call to a mock function has returned a specified value.
*/
nthReturnedWith(n: number, value: any): 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>>;
/**
* 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>>;
/**
* Checks that a value is what you expect. It uses `===` to check strict equality.
* Don't use `toBe` with floating-point numbers.
@ -816,7 +835,7 @@ declare namespace jest {
* This ensures that a value matches the most recent snapshot with property matchers.
* Check out [the Snapshot Testing guide](http://facebook.github.io/jest/docs/snapshot-testing.html) for more information.
*/
toMatchSnapshot<T extends { [P in keyof R]: any }>(propertyMatchers: Partial<T>, snapshotName?: string): R;
toMatchSnapshot<U extends { [P in keyof T]: any }>(propertyMatchers: Partial<U>, snapshotName?: string): R;
/**
* This ensures that a value matches the most recent snapshot.
* Check out [the Snapshot Testing guide](http://facebook.github.io/jest/docs/snapshot-testing.html) for more information.
@ -827,7 +846,7 @@ declare namespace jest {
* 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](http://facebook.github.io/jest/docs/snapshot-testing.html) for more information.
*/
toMatchInlineSnapshot<T extends { [P in keyof R]: any }>(propertyMatchers: Partial<T>, snapshot?: string): R;
toMatchInlineSnapshot<U extends { [P in keyof T]: any }>(propertyMatchers: Partial<U>, 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.
@ -869,6 +888,47 @@ declare namespace jest {
toThrowErrorMatchingInlineSnapshot(snapshot?: string): R;
}
type RemoveFirstFromTuple<T extends any[]> =
T['length'] extends 0 ? [] :
(((...b: T) => void) extends (a: any, ...b: infer I) => void ? I : []);
type Parameters<T extends (...args: any[]) => any> = T extends (...args: infer P) => any ? P : never;
interface AsymmetricMatcher {
asymmetricMatch(other: unknown): boolean;
}
type NonAsyncMatchers<TMatchers extends ExpectExtendMap> = {
[K in keyof TMatchers]: ReturnType<TMatchers[K]> extends Promise<CustomMatcherResult>? never: K
}[keyof TMatchers];
type CustomAsyncMatchers<TMatchers extends ExpectExtendMap> = {[K in NonAsyncMatchers<TMatchers>]: CustomAsymmetricMatcher<TMatchers[K]>};
type CustomAsymmetricMatcher<TMatcher extends (...args: any[]) => any> = (...args: RemoveFirstFromTuple<Parameters<TMatcher>>) => AsymmetricMatcher;
// should be TMatcherReturn extends void|Promise<void> but getting dtslint error
type CustomJestMatcher<TMatcher extends (...args: any[]) => any, TMatcherReturn> = (...args: RemoveFirstFromTuple<Parameters<TMatcher>>) => TMatcherReturn;
type ExpectProperties= {
[K in keyof Expect]: Expect[K]
};
// should be TMatcherReturn extends void|Promise<void> but getting dtslint error
// Use the `void` type for return types only. Otherwise, use `undefined`. See: https://github.com/Microsoft/dtslint/blob/master/docs/void-return.md
// have added issue https://github.com/microsoft/dtslint/issues/256 - Cannot have type union containing void ( to be used as return type only
type ExtendedMatchers<TMatchers extends ExpectExtendMap, TMatcherReturn, TActual> = Matchers<TMatcherReturn, TActual> & {[K in keyof TMatchers]: CustomJestMatcher<TMatchers[K], TMatcherReturn>};
type JestExtendedMatchers<TMatchers extends ExpectExtendMap, TActual> = JestMatchersShape<ExtendedMatchers<TMatchers, void, TActual>, ExtendedMatchers<TMatchers, Promise<void>, TActual>>;
// when have called expect.extend
type ExtendedExpectFunction<TMatchers extends ExpectExtendMap> = <TActual>(actual: TActual) => JestExtendedMatchers<TMatchers, TActual>;
type ExtendedExpect<TMatchers extends ExpectExtendMap>=
ExpectProperties &
AndNot<CustomAsyncMatchers<TMatchers>> &
ExtendedExpectFunction<TMatchers>;
/**
* Construct a type with the properties of T except for those in type K.
*/
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
type NonPromiseMatchers<T extends JestMatchersShape> = Omit<T, 'resolves' | 'rejects' | 'not'>;
type PromiseMatchers<T extends JestMatchersShape> = Omit<T['resolves'], 'not'>;
interface Constructable {
new (...args: any[]): any;
}

View File

@ -580,6 +580,28 @@ switch (mockResult.type) {
break;
}
/* getState and setState */
// $ExpectError
expect.setState(true);
expect.setState({for: 'state'});
const expectState = expect.getState();
// $ExpectType string
expectState.currentTestName;
// $ExpectType string
expectState.testPath;
// $ExpectType boolean
expectState.expand;
// $ExpectType number
expectState.assertionCalls;
// $ExpectType number
expectState.expectedAssertionsNumber;
// $ExpectType boolean | undefined
expectState.isExpectingAssertions;
// $ExpectType Error[]
expectState.suppressedErrors;
// allows additional state properties added by getState
expectState.for;
/* Snapshot serialization */
const snapshotSerializerPlugin: jest.SnapshotSerializerPlugin = {
@ -677,39 +699,26 @@ const expectExtendMap: jest.ExpectExtendMap = {};
expect.extend(expectExtendMap);
expect.extend({});
expect.extend({
foo(this: jest.MatcherUtils, received: {}, ...actual: Array<{}>) {
foo(this: jest.MatcherContext, received: {}, ...actual: Array<{}>) {
return {
message: () => JSON.stringify(received),
pass: false,
};
},
});
// $ExpectError
const customMatcherResultMessage: jest.CustomMatcherResult['message'] = 'msg';
expect.extend({
foo(this: jest.MatcherUtils, received: {}, ...actual: Array<{}>) {
return {
message: JSON.stringify(received),
pass: false,
};
},
});
expect.extend({
async foo(this: jest.MatcherUtils, received: {}, ...actual: Array<{}>) {
async foo(this: jest.MatcherContext, received: {}, ...actual: Array<{}>) {
return {
message: () => JSON.stringify(received),
pass: false,
};
},
});
expect.extend({
async foo(this: jest.MatcherUtils, received: {}, ...actual: Array<{}>) {
return {
message: JSON.stringify(received),
pass: false,
};
},
});
expect.extend({
foo(this: jest.MatcherUtils) {
foo(this: jest.MatcherContext) {
const isNot: boolean = this.isNot;
const expand: boolean = this.expand;
@ -760,8 +769,13 @@ expect.extend({
const equals: boolean = this.equals({}, {});
this.dontThrow();
this.fromState;
const currentTestName: string = this.currentTestName;
const testPath: string = this.testPath;
return {
message: () => '',
message: () => `Can use ${this.promise} for failure message`,
pass: false,
};
},
@ -771,6 +785,16 @@ expect.extend({
describe('', () => {
it('', () => {
/* Corrections of previous typings */
// $ExpectError
expect('').not.not;
// $ExpectError
expect('').resolves.resolves;
// $ExpectType void
expect('').toEqual('');
// $ExpectType Promise<void>
expect(Promise.resolve('')).resolves.toEqual('');
expect(jest.fn()).lastCalledWith();
expect(jest.fn()).lastCalledWith('jest');
expect(jest.fn()).lastCalledWith({}, {});
@ -969,16 +993,11 @@ describe('', () => {
/* Promise matchers */
expect(Promise.reject('jest')).rejects.toEqual('jest');
expect(Promise.reject({})).rejects.toEqual({});
expect(Promise.resolve('jest')).rejects.toEqual('jest');
expect(Promise.resolve({})).rejects.toEqual({});
expect(Promise.reject('jest')).resolves.toEqual('jest');
expect(Promise.reject({})).resolves.toEqual({});
expect(Promise.resolve('jest')).resolves.toEqual('jest');
expect(Promise.resolve({})).resolves.toEqual({});
expect(Promise.reject('jest')).rejects.toEqual('jest').then(() => {});
expect(Promise.reject('jest')).rejects.not.toEqual('other').then(() => {});
expect(Promise.resolve('jest')).resolves.toEqual('jest').then(() => {});
expect(Promise.resolve('jest')).resolves.not.toEqual('other').then(() => {});
/* type matchers */
expect({}).toBe(expect.anything());
@ -1020,6 +1039,79 @@ describe('', () => {
});
});
/* Custom matchers and CustomExpect */
describe('', () => {
it('', () => {
const customMatcher = (expected: any, actual: {prop: string}, option1: boolean) => {
return {pass: true, message: () => ''};
};
const asyncMatcher = () => {
return Promise.resolve({pass: true, message: () => ''});
};
const customMatchers = {customMatcher, asyncMatcher};
expect.extend(customMatchers);
const extendedExpect: jest.ExtendedExpect<typeof customMatchers> = expect as any;
// extracting matcher types
const matchers = extendedExpect({thing: true});
let nonPromiseMatchers: jest.NonPromiseMatchers<typeof matchers> = matchers;
const isNot = true;
if (isNot) {
nonPromiseMatchers = matchers.not;
}
// retains U from <U>(actual: U) => JestExtendedMatchers<T, U>; - BUT CANNOT DO THAT WITH CUSTOM...
nonPromiseMatchers.toMatchInlineSnapshot({thing: extendedExpect.any(Boolean)});
// $ExpectError
nonPromiseMatchers.toMatchInlineSnapshot({notthing: extendedExpect.any(Boolean)});
let promiseMatchers: jest.PromiseMatchers<typeof matchers> = matchers.rejects;
if (isNot) {
promiseMatchers = matchers.rejects.not;
}
// $ExpectType Promise<void>
promiseMatchers.customMatcher({prop: ''}, true);
// retains built in asymmetric matcher
extendedExpect.not.arrayContaining;
extendedExpect.customMatcher({prop: 'good'}, false).asymmetricMatch({}).valueOf();
// $ExpectError
extendedExpect.customMatcher({prop: {not: 'good'}}, false);
extendedExpect.not.customMatcher({prop: 'good'}, false).asymmetricMatch({}).valueOf();
// $ExpectError
extendedExpect.not.customMatcher({prop: 'good'}, 'bad').asymmetricMatch({}).valueOf();
// $ExpectError
const asynMatcherExcluded = extendedExpect.asyncMatcher;
extendedExpect('').customMatcher({prop: 'good'}, true);
// $ExpectError
extendedExpect('').customMatcher({prop: 'good'}, 'bad');
extendedExpect('').not.customMatcher({prop: 'good'}, true);
// $ExpectError
extendedExpect('').not.customMatcher({prop: 'good'}, 'bad');
extendedExpect(Promise.resolve('')).resolves.customMatcher({prop: 'good'}, true).then(() => {});
// $ExpectError
extendedExpect(Promise.resolve('')).resolves.customMatcher({prop: 'good'}, 'bad').then(() => {});
extendedExpect(Promise.resolve('')).resolves.not.customMatcher({prop: 'good'}, true).then(() => {});
// $ExpectError
extendedExpect(Promise.resolve('')).resolves.not.customMatcher({prop: 'good'}, 'bad').then(() => {});
extendedExpect(Promise.reject('')).rejects.customMatcher({prop: 'good'}, true).then(() => {});
// $ExpectError
extendedExpect(Promise.reject('')).rejects.customMatcher({prop: 'good'}, 'bad').then(() => {});
extendedExpect(Promise.reject('')).rejects.not.customMatcher({prop: 'good'}, true).then(() => {});
// $ExpectError
extendedExpect(Promise.reject('')).rejects.not.customMatcher({prop: 'good'}, 'bad').then(() => {});
});
});
/* Test framework and config */
const globalConfig: jest.GlobalConfig = {

View File

@ -7,7 +7,7 @@
/// <reference types="jest" />
declare namespace jest {
interface Matchers<R> {
interface Matchers<R, T> {
/**
* Ensure that `console.error` function was called.
*/