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

feat(expect): replace RawMatcherFn with MatcherFunction and MatcherFunctionWithState types #12376

Merged
merged 13 commits into from Feb 15, 2022
2 changes: 1 addition & 1 deletion CHANGELOG.md
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
Expand Up @@ -6,13 +6,13 @@
*/

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

const toBeWithinRange: RawMatcherFn = (
SimenB marked this conversation as resolved.
Show resolved Hide resolved
const toBeWithinRange = (
actual: number,
floor: number,
ceiling: number,
) => {
): ExpectationResult => {
mrazauskas marked this conversation as resolved.
Show resolved Hide resolved
const pass = actual >= floor && actual <= ceiling;
if (pass) {
return {
Expand Down
119 changes: 118 additions & 1 deletion packages/expect/__typetests__/expect.test.ts
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
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
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`
mrazauskas marked this conversation as resolved.
Show resolved Hide resolved
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
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
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