diff --git a/types/expect-puppeteer/index.d.ts b/types/expect-puppeteer/index.d.ts index 72283c05e4be68..5a4a2d9e8a04e1 100644 --- a/types/expect-puppeteer/index.d.ts +++ b/types/expect-puppeteer/index.d.ts @@ -51,7 +51,7 @@ interface ExpectPuppeteer { declare global { namespace jest { // tslint:disable-next-line no-empty-interface - interface Matchers { + interface Matchers { // 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; diff --git a/types/jest-axe/index.d.ts b/types/jest-axe/index.d.ts index d04364c0fe7de1..4757c319c2d5e1 100644 --- a/types/jest-axe/index.d.ts +++ b/types/jest-axe/index.d.ts @@ -79,7 +79,7 @@ export const toHaveNoViolations: { declare global { namespace jest { - interface Matchers { + interface Matchers { toHaveNoViolations(): R; } } diff --git a/types/jest-expect-message/index.d.ts b/types/jest-expect-message/index.d.ts index ae9b85aea9cf9f..3ba5f5ceaf3249 100644 --- a/types/jest-expect-message/index.d.ts +++ b/types/jest-expect-message/index.d.ts @@ -8,6 +8,6 @@ declare namespace jest { interface Expect { - (actual: T, message: string): Matchers; + (actual: T, message: string): JestMatchers; } } diff --git a/types/jest-image-snapshot/index.d.ts b/types/jest-image-snapshot/index.d.ts index 10d6a2a17dfc57..075e2c0506fd26 100644 --- a/types/jest-image-snapshot/index.d.ts +++ b/types/jest-image-snapshot/index.d.ts @@ -80,7 +80,7 @@ export function configureToMatchImageSnapshot(options: MatchImageSnapshotOptions declare global { namespace jest { - interface Matchers { + interface Matchers { toMatchImageSnapshot(options?: MatchImageSnapshotOptions): R; } } diff --git a/types/jest-json-schema/index.d.ts b/types/jest-json-schema/index.d.ts index cc9636a36df522..8fbcfaa8f44850 100644 --- a/types/jest-json-schema/index.d.ts +++ b/types/jest-json-schema/index.d.ts @@ -10,7 +10,7 @@ import * as ajv from 'ajv'; declare global { namespace jest { - interface Matchers { + interface Matchers { toBeValidSchema(): R; toMatchSchema(schema: object): R; } diff --git a/types/jest-matcher-one-of/index.d.ts b/types/jest-matcher-one-of/index.d.ts index 63ab484473f378..4725d88a00c356 100644 --- a/types/jest-matcher-one-of/index.d.ts +++ b/types/jest-matcher-one-of/index.d.ts @@ -6,7 +6,7 @@ /// declare namespace jest { - interface Matchers { + interface Matchers { toBeOneOf(expected: any[]): R; } } diff --git a/types/jest-specific-snapshot/index.d.ts b/types/jest-specific-snapshot/index.d.ts index 25c168f5d26a73..b4b076fa556858 100644 --- a/types/jest-specific-snapshot/index.d.ts +++ b/types/jest-specific-snapshot/index.d.ts @@ -8,7 +8,7 @@ declare global { namespace jest { - interface Matchers { + interface Matchers { toMatchSpecificSnapshot(snapshotFilename: string): R; } } diff --git a/types/jest/index.d.ts b/types/jest/index.d.ts index ade34870b60622..394e68d88891ab 100644 --- a/types/jest/index.d.ts +++ b/types/jest/index.d.ts @@ -23,6 +23,7 @@ // ExE Boss // Alex Bolenok // Mario Beltrán Alarcón +// Tony Hallett // 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; type CustomMatcher = ( - this: MatcherUtils, + this: MatcherContext, received: any, ...actual: any[] ) => CustomMatcherResult | Promise; 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. */ - (actual: T): Matchers; + (actual: T): JestMatchers; /** * 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; } - interface Matchers { + type JestMatchers = JestMatchersShape, Matchers, T>>; + + type JestMatchersShape = { + /** + * 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, + /** + * Unwraps the reason of a rejected promise so any other matcher can be chained. + * If the promise is fulfilled the assertion fails. + */ + rejects: AndNot + } & AndNot; + type AndNot = T & { + not: T + }; + // should be R extends void|Promise but getting dtslint error + interface Matchers { /** * 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; /** * 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>; - /** - * Unwraps the reason of a rejected promise so any other matcher can be chained. - * If the promise is fulfilled the assertion fails. - */ - rejects: Matchers>; /** * 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(propertyMatchers: Partial, snapshotName?: string): R; + toMatchSnapshot(propertyMatchers: Partial, 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(propertyMatchers: Partial, snapshot?: string): R; + toMatchInlineSnapshot(propertyMatchers: Partial, 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['length'] extends 0 ? [] : + (((...b: T) => void) extends (a: any, ...b: infer I) => void ? I : []); + + type Parameters any> = T extends (...args: infer P) => any ? P : never; + + interface AsymmetricMatcher { + asymmetricMatch(other: unknown): boolean; + } + type NonAsyncMatchers = { + [K in keyof TMatchers]: ReturnType extends Promise? never: K + }[keyof TMatchers]; + type CustomAsyncMatchers = {[K in NonAsyncMatchers]: CustomAsymmetricMatcher}; + type CustomAsymmetricMatcher any> = (...args: RemoveFirstFromTuple>) => AsymmetricMatcher; + + // should be TMatcherReturn extends void|Promise but getting dtslint error + type CustomJestMatcher any, TMatcherReturn> = (...args: RemoveFirstFromTuple>) => TMatcherReturn; + + type ExpectProperties= { + [K in keyof Expect]: Expect[K] + }; + // should be TMatcherReturn extends void|Promise 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 = Matchers & {[K in keyof TMatchers]: CustomJestMatcher}; + type JestExtendedMatchers = JestMatchersShape, ExtendedMatchers, TActual>>; + + // when have called expect.extend + type ExtendedExpectFunction = (actual: TActual) => JestExtendedMatchers; + + type ExtendedExpect= + ExpectProperties & + AndNot> & + ExtendedExpectFunction; + /** + * Construct a type with the properties of T except for those in type K. + */ + type Omit = Pick>; + type NonPromiseMatchers = Omit; + type PromiseMatchers = Omit; + interface Constructable { new (...args: any[]): any; } diff --git a/types/jest/jest-tests.ts b/types/jest/jest-tests.ts index bf5eaf9239f8e6..9122ce0f15b9e6 100644 --- a/types/jest/jest-tests.ts +++ b/types/jest/jest-tests.ts @@ -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 + 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 = expect as any; + + // extracting matcher types + const matchers = extendedExpect({thing: true}); + let nonPromiseMatchers: jest.NonPromiseMatchers = matchers; + const isNot = true; + if (isNot) { + nonPromiseMatchers = matchers.not; + } + // retains U from (actual: U) => JestExtendedMatchers; - BUT CANNOT DO THAT WITH CUSTOM... + nonPromiseMatchers.toMatchInlineSnapshot({thing: extendedExpect.any(Boolean)}); + // $ExpectError + nonPromiseMatchers.toMatchInlineSnapshot({notthing: extendedExpect.any(Boolean)}); + + let promiseMatchers: jest.PromiseMatchers = matchers.rejects; + if (isNot) { + promiseMatchers = matchers.rejects.not; + } + // $ExpectType Promise + 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 = { diff --git a/types/wordpress__jest-console/index.d.ts b/types/wordpress__jest-console/index.d.ts index ecda81d6f776ae..247dfcb32f7b54 100644 --- a/types/wordpress__jest-console/index.d.ts +++ b/types/wordpress__jest-console/index.d.ts @@ -7,7 +7,7 @@ /// declare namespace jest { - interface Matchers { + interface Matchers { /** * Ensure that `console.error` function was called. */