Skip to content

Commit

Permalink
expose MatcherFunction
Browse files Browse the repository at this point in the history
  • Loading branch information
mrazauskas committed Feb 14, 2022
1 parent f0407fc commit b16b3a4
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 66 deletions.
158 changes: 96 additions & 62 deletions packages/expect/__typetests__/expect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -29,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 @@ -85,80 +88,111 @@ expectType<void>(
}),
);

// 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<void>(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>(toBeWithinRange);

type AllowOmittingExpected = (
this: MatcherState,
received: string,
): ExpectationResult {
expectType<number>(this.assertionCalls);
expectType<string | undefined>(this.currentTestName);
expectType<(() => void) | undefined>(this.dontThrow);
expectType<Error | undefined>(this.error);
expectType<EqualsFunction>(this.equals);
expectType<boolean | undefined>(this.expand);
expectType<number | null | undefined>(this.expectedAssertionsNumber);
expectType<Error | undefined>(this.expectedAssertionsNumberError);
expectType<boolean | undefined>(this.isExpectingAssertions);
expectType<Error | undefined>(this.isExpectingAssertionsError);
expectType<boolean>(this.isNot);
expectType<string>(this.promise);
expectType<Array<Error>>(this.suppressedErrors);
expectType<string | undefined>(this.testPath);
expectType<MatcherUtils>(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>(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);
2 changes: 2 additions & 0 deletions packages/expect/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ export type {
AsymmetricMatchers,
Expect,
ExpectationResult,
MatcherFunction,
MatcherFunctionWithContext,
MatcherState,
Matchers,
} from './types';
Expand Down
17 changes: 15 additions & 2 deletions packages/expect/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,19 @@ 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: Array<any>): ExpectationResult;
/** @internal */
Expand All @@ -29,7 +42,7 @@ export type RawMatcherFn<T extends MatcherState = MatcherState> = {
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
4 changes: 2 additions & 2 deletions packages/jest-snapshot/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down

0 comments on commit b16b3a4

Please sign in to comment.