Skip to content
Merged
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
### Fixes

- `[expect]` Move typings of `.not`, `.rejects` and `.resolves` modifiers outside of `Matchers` interface ([#12346](https://github.com/facebook/jest/pull/12346))
- `[expect]` Expose `AsymmetricMatchers` and `RawMatcherFn` interfaces ([#12363](https://github.com/facebook/jest/pull/12363))
- `[expect]` Expose `AsymmetricMatchers` and `ExpectationResult` interfaces ([#12363](https://github.com/facebook/jest/pull/12363), [#12376](https://github.com/facebook/jest/pull/12376))
- `[jest-environment-jsdom]` Make `jsdom` accessible to extending environments again ([#12232](https://github.com/facebook/jest/pull/12232))
- `[jest-jasmine2, jest-types]` [**BREAKING**] Move all `jasmine` specific types from `@jest/types` to its own package ([#12125](https://github.com/facebook/jest/pull/12125))

Expand Down
6 changes: 3 additions & 3 deletions examples/expect-extend/toBeWithinRange.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@
*/

import {expect} from '@jest/globals';
import type {RawMatcherFn} from 'expect';
import type {ExpectationResult} from 'expect';

const toBeWithinRange: RawMatcherFn = (
const toBeWithinRange = (
actual: number,
floor: number,
ceiling: number,
) => {
): ExpectationResult => {
const pass = actual >= floor && actual <= ceiling;
if (pass) {
return {
Expand Down
119 changes: 118 additions & 1 deletion packages/expect/__typetests__/expect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,14 @@

import {expectError, expectType} from 'tsd-lite';
import type {EqualsFunction, Tester} from '@jest/expect-utils';
import {type Matchers, expect} from 'expect';
import {
type ExpectationResult,
type MatcherFunction,
type MatcherFunctionWithContext,
type MatcherState,
type Matchers,
expect,
} from 'expect';
import type * as jestMatcherUtils from 'jest-matcher-utils';

type M = Matchers<void, unknown>;
Expand All @@ -24,6 +31,7 @@ type MatcherUtils = typeof jestMatcherUtils & {
subsetEquality: Tester;
};

// TODO `actual` should be allowed to have only `unknown` type
expectType<void>(
expect.extend({
toBeWithinRange(actual: number, floor: number, ceiling: number) {
Expand Down Expand Up @@ -79,3 +87,112 @@ expectType<void>(
bananas: expect.not.toBeWithinRange(11, 20),
}),
);

// MatcherFunction

expectError(() => {
const actualMustBeUnknown: MatcherFunction = (actual: string) => {
return {
message: () => `result: ${actual}`,
pass: actual === 'result',
};
};
});

expectError(() => {
const lacksMessage: MatcherFunction = (actual: unknown) => {
return {
pass: actual === 'result',
};
};
});

expectError(() => {
const lacksPass: MatcherFunction = (actual: unknown) => {
return {
message: () => `result: ${actual}`,
};
};
});

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,
};
};

expectType<ToBeWithinRange>(toBeWithinRange);

type AllowOmittingExpected = (
this: MatcherState,
actual: unknown,
) => ExpectationResult;

const allowOmittingExpected: MatcherFunction = (actual: unknown) => {
return {
message: () => `actual ${actual}`,
pass: true,
};
};

expectType<AllowOmittingExpected>(allowOmittingExpected);

// MatcherState

const toHaveContext: MatcherFunction = function (actual: unknown) {
expectType<MatcherState>(this);

return {
message: () => `result: ${actual}`,
pass: actual === 'result',
};
};

interface CustomContext extends MatcherState {
customMethod(): void;
}

const customContext: MatcherFunctionWithContext<CustomContext> = function (
actual: unknown,
) {
expectType<CustomContext>(this);
expectType<void>(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<CustomContext>(this);
expectType<void>(this.customMethod());

return {
message: () => `count: ${count}`,
pass: actual === count,
};
};

expectType<CustomContextAndExpected>(customContextAndExpected);
4 changes: 3 additions & 1 deletion packages/expect/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,11 @@ import type {
export type {
AsymmetricMatchers,
Expect,
ExpectationResult,
MatcherFunction,
MatcherFunctionWithContext,
MatcherState,
Matchers,
RawMatcherFn,
} from './types';

export class JestAssertionError extends Error {
Expand Down
19 changes: 16 additions & 3 deletions packages/expect/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,29 @@ export type AsyncExpectationResult = Promise<SyncExpectationResult>;

export type ExpectationResult = SyncExpectationResult | AsyncExpectationResult;

export type MatcherFunctionWithContext<
Context extends MatcherState = MatcherState,
Expected extends Array<any> = [],
> = (
this: Context,
actual: unknown,
...expected: Expected
) => ExpectationResult;

export type MatcherFunction<Expected extends Array<any> = []> =
MatcherFunctionWithContext<MatcherState, Expected>;

// TODO should be replaced with `MatcherFunctionWithContext`
export type RawMatcherFn<T extends MatcherState = MatcherState> = {
(this: T, actual: any, expected: any, options?: any): ExpectationResult;
(this: T, actual: any, ...expected: Array<any>): ExpectationResult;
/** @internal */
[INTERNAL_MATCHER_FLAG]?: boolean;
};

export type ThrowingMatcherFn = (actual: any) => void;
export type PromiseMatcherFn = (actual: any) => Promise<void>;

export type MatcherState = {
export interface MatcherState {
assertionCalls: number;
currentTestName?: string;
dontThrow?(): void;
Expand All @@ -48,7 +61,7 @@ export type MatcherState = {
iterableEquality: Tester;
subsetEquality: Tester;
};
};
}

export interface AsymmetricMatcher {
asymmetricMatch(other: unknown): boolean;
Expand Down
16 changes: 4 additions & 12 deletions packages/jest-jasmine2/src/jestExpect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

/* eslint-disable local/prefer-spread-eventually */

import {type MatcherState, type RawMatcherFn, expect} from 'expect';
import {type MatcherState, expect} from 'expect';
import {
addSerializer,
toMatchInlineSnapshot,
Expand Down Expand Up @@ -41,23 +41,15 @@ export default function jestExpect(config: {expand: boolean}): void {
jestMatchersObject[name] = function (
this: MatcherState,
...args: Array<unknown>
): RawMatcherFn {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This function has inferred return type of ExpectationResult without this extra type.

) {
// use "expect.extend" if you need to use equality testers (via this.equal)
const result = jasmineMatchersObject[name](null, null);
// if there is no 'negativeCompare', both should be handled by `compare`
const negativeCompare = result.negativeCompare || result.compare;

return this.isNot
? negativeCompare.apply(
null,
// @ts-expect-error
args,
)
: result.compare.apply(
null,
// @ts-expect-error
args,
);
? negativeCompare.apply(null, args)
: result.compare.apply(null, args);
};
});

Expand Down
6 changes: 3 additions & 3 deletions packages/jest-jasmine2/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import type {AssertionError} from 'assert';
import type {Config} from '@jest/types';
import type {Expect, RawMatcherFn} from 'expect';
import type {Expect, ExpectationResult} from 'expect';
import type CallTracker from './jasmine/CallTracker';
import type Env from './jasmine/Env';
import type JsApiReporter from './jasmine/JsApiReporter';
Expand Down Expand Up @@ -48,8 +48,8 @@ export interface Spy extends Record<string, any> {

type JasmineMatcher = {
(matchersUtil: unknown, context: unknown): JasmineMatcher;
compare: () => RawMatcherFn;
negativeCompare: () => RawMatcherFn;
compare(...args: Array<unknown>): ExpectationResult;
negativeCompare(...args: Array<unknown>): ExpectationResult;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Puzzle. To be precise this type should be () => (...args: Array<unknown>) => ExpectationResult, but my version eliminates these @ts-expect-error: https://github.com/facebook/jest/blob/faef0b4b7082df574a0e4423b86d468847360f17/packages/jest-jasmine2/src/jestExpect.ts#L50-L60

};

export type JasmineMatchersObject = {[id: string]: JasmineMatcher};
Expand Down
Loading