diff --git a/packages/expect/__typetests__/expect.test.ts b/packages/expect/__typetests__/expect.test.ts index 3db1270ee699..122f92985d09 100644 --- a/packages/expect/__typetests__/expect.test.ts +++ b/packages/expect/__typetests__/expect.test.ts @@ -9,6 +9,8 @@ import {expectError, expectType} from 'tsd-lite'; import type {EqualsFunction, Tester} from '@jest/expect-utils'; import { type ExpectationResult, + type MatcherFunction, + type MatcherFunctionWithContext, type MatcherState, type Matchers, expect, @@ -29,6 +31,7 @@ type MatcherUtils = typeof jestMatcherUtils & { subsetEquality: Tester; }; +// TODO `actual` should be allowed to have only `unknown` type expectType( expect.extend({ toBeWithinRange(actual: number, floor: number, ceiling: number) { @@ -85,80 +88,111 @@ expectType( }), ); -// ExpectationResult +// MatcherFunction -const toBeResult = (received: string): ExpectationResult => { - if (received === 'result') { - return { - message: () => 'is result', - pass: true, - }; - } else { +expectError(() => { + const actualMustBeUnknown: MatcherFunction = (actual: string) => { return { - message: () => 'is not result', - pass: false, + message: () => `result: ${actual}`, + pass: actual === 'result', }; - } -}; - -expectType(expect.extend({toBeResult})); + }; +}); expectError(() => { - const lacksElseBranch = (received: string): ExpectationResult => { - if (received === 'result') { - return { - message: () => 'is result', - pass: true, - }; - } + const lacksMessage: MatcherFunction = (actual: unknown) => { + return { + pass: actual === 'result', + }; }; }); expectError(() => { - const lacksMessage = (received: string): ExpectationResult => { - if (received === 'result') { - return { - pass: true, - }; - } else { - return { - pass: false, - }; - } + const lacksPass: MatcherFunction = (actual: unknown) => { + return { + message: () => `result: ${actual}`, + }; }; }); -// MatcherState +type ToBeWithinRange = ( + this: MatcherState, + actual: unknown, + floor: number, + ceiling: number, +) => ExpectationResult; + +const toBeWithinRange: MatcherFunction<[floor: number, ceiling: number]> = ( + actual: unknown, + floor: unknown, + ceiling: unknown, +) => { + return { + message: () => `actual ${actual}; range ${floor}-${ceiling}`, + pass: true, + }; +}; -function toHaveContext( +expectType(toBeWithinRange); + +type AllowOmittingExpected = ( this: MatcherState, - received: string, -): ExpectationResult { - expectType(this.assertionCalls); - expectType(this.currentTestName); - expectType<(() => void) | undefined>(this.dontThrow); - expectType(this.error); - expectType(this.equals); - expectType(this.expand); - expectType(this.expectedAssertionsNumber); - expectType(this.expectedAssertionsNumberError); - expectType(this.isExpectingAssertions); - expectType(this.isExpectingAssertionsError); - expectType(this.isNot); - expectType(this.promise); - expectType>(this.suppressedErrors); - expectType(this.testPath); - expectType(this.utils); - - if (received === 'result') { - return { - message: () => 'is result', - pass: true, - }; - } else { - return { - message: () => 'is not result', - pass: false, - }; - } + actual: unknown, +) => ExpectationResult; + +const allowOmittingExpected: MatcherFunction = (actual: unknown) => { + return { + message: () => `actual ${actual}`, + pass: true, + }; +}; + +expectType(allowOmittingExpected); + +// MatcherState + +const toHaveContext: MatcherFunction = function (actual: unknown) { + expectType(this); + + return { + message: () => `result: ${actual}`, + pass: actual === 'result', + }; +}; + +interface CustomContext extends MatcherState { + customMethod(): void; } + +const customContext: MatcherFunctionWithContext = function ( + actual: unknown, +) { + expectType(this); + expectType(this.customMethod()); + + return { + message: () => `result: ${actual}`, + pass: actual === 'result', + }; +}; + +type CustomContextAndExpected = ( + this: CustomContext, + actual: unknown, + count: number, +) => ExpectationResult; + +const customContextAndExpected: MatcherFunctionWithContext< + CustomContext, + [count: number] +> = function (actual: unknown, count: unknown) { + expectType(this); + expectType(this.customMethod()); + + return { + message: () => `count: ${count}`, + pass: actual === count, + }; +}; + +expectType(customContextAndExpected); diff --git a/packages/expect/src/index.ts b/packages/expect/src/index.ts index 66f88b5c08b2..ac6a2d9db36b 100644 --- a/packages/expect/src/index.ts +++ b/packages/expect/src/index.ts @@ -53,6 +53,8 @@ export type { AsymmetricMatchers, Expect, ExpectationResult, + MatcherFunction, + MatcherFunctionWithContext, MatcherState, Matchers, } from './types'; diff --git a/packages/expect/src/types.ts b/packages/expect/src/types.ts index e53f09493335..232c970ab759 100644 --- a/packages/expect/src/types.ts +++ b/packages/expect/src/types.ts @@ -20,6 +20,19 @@ export type AsyncExpectationResult = Promise; export type ExpectationResult = SyncExpectationResult | AsyncExpectationResult; +export type MatcherFunctionWithContext< + Context extends MatcherState = MatcherState, + Expected extends Array = [], +> = ( + this: Context, + actual: unknown, + ...expected: Expected +) => ExpectationResult; + +export type MatcherFunction = []> = + MatcherFunctionWithContext; + +// TODO should be replaced with `MatcherFunctionWithContext` export type RawMatcherFn = { (this: T, actual: any, ...expected: Array): ExpectationResult; /** @internal */ @@ -29,7 +42,7 @@ export type RawMatcherFn = { export type ThrowingMatcherFn = (actual: any) => void; export type PromiseMatcherFn = (actual: any) => Promise; -export type MatcherState = { +export interface MatcherState { assertionCalls: number; currentTestName?: string; dontThrow?(): void; @@ -48,7 +61,7 @@ export type MatcherState = { iterableEquality: Tester; subsetEquality: Tester; }; -}; +} export interface AsymmetricMatcher { asymmetricMatch(other: unknown): boolean; diff --git a/packages/jest-snapshot/src/types.ts b/packages/jest-snapshot/src/types.ts index dcc0c8e12de5..0013a08d081f 100644 --- a/packages/jest-snapshot/src/types.ts +++ b/packages/jest-snapshot/src/types.ts @@ -8,9 +8,9 @@ import type {MatcherState} from 'expect'; import type SnapshotState from './State'; -export type Context = MatcherState & { +export interface Context extends MatcherState { snapshotState: SnapshotState; -}; +} export type MatchSnapshotConfig = { context: Context;