Skip to content

Commit faef0b4

Browse files
authored
fix(expect): expose AsymmetricMatchers and RawMatcherFn interfaces (#12363)
1 parent 60eb416 commit faef0b4

File tree

14 files changed

+293
-105
lines changed

14 files changed

+293
-105
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
### Fixes
1515

1616
- `[expect]` Move typings of `.not`, `.rejects` and `.resolves` modifiers outside of `Matchers` interface ([#12346](https://github.com/facebook/jest/pull/12346))
17+
- `[expect]` Expose `AsymmetricMatchers` and `RawMatcherFn` interfaces ([#12363](https://github.com/facebook/jest/pull/12363))
1718
- `[jest-environment-jsdom]` Make `jsdom` accessible to extending environments again ([#12232](https://github.com/facebook/jest/pull/12232))
1819
- `[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))
1920

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import {expect, test} from '@jest/globals';
9+
import '../toBeWithinRange';
10+
11+
test('is within range', () => expect(100).toBeWithinRange(90, 110));
12+
13+
test('is NOT within range', () => expect(101).not.toBeWithinRange(0, 100));
14+
15+
test('asymmetric ranges', () => {
16+
expect({apples: 6, bananas: 3}).toEqual({
17+
apples: expect.toBeWithinRange(1, 10),
18+
bananas: expect.not.toBeWithinRange(11, 20),
19+
});
20+
});
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"private": true,
3+
"version": "0.0.0",
4+
"name": "example-expect-extend",
5+
"devDependencies": {
6+
"@babel/core": "*",
7+
"@babel/preset-env": "*",
8+
"@babel/preset-typescript": "*",
9+
"@jest/globals": "workspace:*",
10+
"babel-jest": "workspace:*",
11+
"expect": "workspace:*",
12+
"jest": "workspace:*"
13+
},
14+
"scripts": {
15+
"test": "jest"
16+
},
17+
"babel": {
18+
"presets": [
19+
[
20+
"@babel/preset-env",
21+
{
22+
"targets": {
23+
"node": "current"
24+
}
25+
}
26+
],
27+
"@babel/preset-typescript"
28+
]
29+
}
30+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import {expect} from '@jest/globals';
9+
import type {RawMatcherFn} from 'expect';
10+
11+
const toBeWithinRange: RawMatcherFn = (
12+
actual: number,
13+
floor: number,
14+
ceiling: number,
15+
) => {
16+
const pass = actual >= floor && actual <= ceiling;
17+
if (pass) {
18+
return {
19+
message: () =>
20+
`expected ${actual} not to be within range ${floor} - ${ceiling}`,
21+
pass: true,
22+
};
23+
} else {
24+
return {
25+
message: () =>
26+
`expected ${actual} to be within range ${floor} - ${ceiling}`,
27+
pass: false,
28+
};
29+
}
30+
};
31+
32+
expect.extend({
33+
toBeWithinRange,
34+
});
35+
36+
declare module 'expect' {
37+
interface AsymmetricMatchers {
38+
toBeWithinRange(a: number, b: number): void;
39+
}
40+
interface Matchers<R> {
41+
toBeWithinRange(a: number, b: number): R;
42+
}
43+
}

packages/expect/__typetests__/expect.test.ts

Lines changed: 70 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,77 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8-
import {expectError} from 'tsd-lite';
9-
import type * as expect from 'expect';
8+
import {expectError, expectType} from 'tsd-lite';
9+
import type {EqualsFunction, Tester} from '@jest/expect-utils';
10+
import {type Matchers, expect} from 'expect';
11+
import type * as jestMatcherUtils from 'jest-matcher-utils';
1012

11-
type M = expect.Matchers<void, unknown>;
12-
type N = expect.Matchers<void>;
13+
type M = Matchers<void, unknown>;
14+
type N = Matchers<void>;
1315

1416
expectError(() => {
15-
type E = expect.Matchers;
17+
type E = Matchers;
1618
});
19+
20+
// extend
21+
22+
type MatcherUtils = typeof jestMatcherUtils & {
23+
iterableEquality: Tester;
24+
subsetEquality: Tester;
25+
};
26+
27+
expectType<void>(
28+
expect.extend({
29+
toBeWithinRange(actual: number, floor: number, ceiling: number) {
30+
expectType<number>(this.assertionCalls);
31+
expectType<string | undefined>(this.currentTestName);
32+
expectType<(() => void) | undefined>(this.dontThrow);
33+
expectType<Error | undefined>(this.error);
34+
expectType<EqualsFunction>(this.equals);
35+
expectType<boolean | undefined>(this.expand);
36+
expectType<number | null | undefined>(this.expectedAssertionsNumber);
37+
expectType<Error | undefined>(this.expectedAssertionsNumberError);
38+
expectType<boolean | undefined>(this.isExpectingAssertions);
39+
expectType<Error | undefined>(this.isExpectingAssertionsError);
40+
expectType<boolean>(this.isNot);
41+
expectType<string>(this.promise);
42+
expectType<Array<Error>>(this.suppressedErrors);
43+
expectType<string | undefined>(this.testPath);
44+
expectType<MatcherUtils>(this.utils);
45+
46+
const pass = actual >= floor && actual <= ceiling;
47+
if (pass) {
48+
return {
49+
message: () =>
50+
`expected ${actual} not to be within range ${floor} - ${ceiling}`,
51+
pass: true,
52+
};
53+
} else {
54+
return {
55+
message: () =>
56+
`expected ${actual} to be within range ${floor} - ${ceiling}`,
57+
pass: false,
58+
};
59+
}
60+
},
61+
}),
62+
);
63+
64+
declare module 'expect' {
65+
interface AsymmetricMatchers {
66+
toBeWithinRange(floor: number, ceiling: number): void;
67+
}
68+
interface Matchers<R> {
69+
toBeWithinRange(floor: number, ceiling: number): void;
70+
}
71+
}
72+
73+
expectType<void>(expect(100).toBeWithinRange(90, 110));
74+
expectType<void>(expect(101).not.toBeWithinRange(0, 100));
75+
76+
expectType<void>(
77+
expect({apples: 6, bananas: 3}).toEqual({
78+
apples: expect.toBeWithinRange(1, 10),
79+
bananas: expect.not.toBeWithinRange(11, 20),
80+
}),
81+
);

packages/expect/src/index.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,13 @@ import type {
4949
ThrowingMatcherFn,
5050
} from './types';
5151

52-
export type {Expect, MatcherState, Matchers} from './types';
52+
export type {
53+
AsymmetricMatchers,
54+
Expect,
55+
MatcherState,
56+
Matchers,
57+
RawMatcherFn,
58+
} from './types';
5359

5460
export class JestAssertionError extends Error {
5561
matcherResult?: Omit<SyncExpectationResult, 'message'> & {message: string};
@@ -358,7 +364,7 @@ const makeThrowingMatcher = (
358364

359365
expect.extend = <T extends MatcherState = MatcherState>(
360366
matchers: MatchersObject<T>,
361-
): void => setMatchers(matchers, false, expect);
367+
) => setMatchers(matchers, false, expect);
362368

363369
expect.anything = anything;
364370
expect.any = any;

packages/expect/src/types.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,16 @@ import {INTERNAL_MATCHER_FLAG} from './jestMatchersObject';
1313

1414
export type SyncExpectationResult = {
1515
pass: boolean;
16-
message: () => string;
16+
message(): string;
1717
};
1818

1919
export type AsyncExpectationResult = Promise<SyncExpectationResult>;
2020

2121
export type ExpectationResult = SyncExpectationResult | AsyncExpectationResult;
2222

2323
export type RawMatcherFn<T extends MatcherState = MatcherState> = {
24-
(this: T, received: any, expected: any, options?: any): ExpectationResult;
24+
(this: T, actual: any, expected: any, options?: any): ExpectationResult;
25+
/** @internal */
2526
[INTERNAL_MATCHER_FLAG]?: boolean;
2627
};
2728

@@ -31,7 +32,7 @@ export type PromiseMatcherFn = (actual: any) => Promise<void>;
3132
export type MatcherState = {
3233
assertionCalls: number;
3334
currentTestName?: string;
34-
dontThrow?: () => void;
35+
dontThrow?(): void;
3536
error?: Error;
3637
equals: EqualsFunction;
3738
expand?: boolean;
@@ -56,7 +57,7 @@ export interface AsymmetricMatcher {
5657
toAsymmetricMatcher?(): string;
5758
}
5859
export type MatchersObject<T extends MatcherState = MatcherState> = {
59-
[id: string]: RawMatcherFn<T>;
60+
[name: string]: RawMatcherFn<T>;
6061
};
6162
export type ExpectedAssertionsErrors = Array<{
6263
actual: string | number;
@@ -73,7 +74,7 @@ export type Expect<State extends MatcherState = MatcherState> = {
7374
assertions(numberOfAssertions: number): void;
7475
// TODO: remove this `T extends` - should get from some interface merging
7576
extend<T extends MatcherState = State>(matchers: MatchersObject<T>): void;
76-
extractExpectedAssertionsErrors: () => ExpectedAssertionsErrors;
77+
extractExpectedAssertionsErrors(): ExpectedAssertionsErrors;
7778
getState(): State;
7879
hasAssertions(): void;
7980
setState(state: Partial<State>): void;

packages/jest-jasmine2/src/jestExpect.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,15 @@
77

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

10-
import {MatcherState, expect} from 'expect';
10+
import {type MatcherState, type RawMatcherFn, expect} from 'expect';
1111
import {
1212
addSerializer,
1313
toMatchInlineSnapshot,
1414
toMatchSnapshot,
1515
toThrowErrorMatchingInlineSnapshot,
1616
toThrowErrorMatchingSnapshot,
1717
} from 'jest-snapshot';
18-
import type {JasmineMatchersObject, RawMatcherFn} from './types';
18+
import type {JasmineMatchersObject} from './types';
1919

2020
export default function jestExpect(config: {expand: boolean}): void {
2121
global.expect = expect;

packages/jest-jasmine2/src/types.ts

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
import type {AssertionError} from 'assert';
99
import type {Config} from '@jest/types';
10-
import type {Expect} from 'expect';
10+
import type {Expect, RawMatcherFn} from 'expect';
1111
import type CallTracker from './jasmine/CallTracker';
1212
import type Env from './jasmine/Env';
1313
import type JsApiReporter from './jasmine/JsApiReporter';
@@ -25,25 +25,6 @@ export interface AssertionErrorWithStack extends AssertionError {
2525
stack: string;
2626
}
2727

28-
// TODO Add expect types to @jest/types or leave it here
29-
// Borrowed from "expect"
30-
// -------START-------
31-
export type SyncExpectationResult = {
32-
pass: boolean;
33-
message: () => string;
34-
};
35-
36-
export type AsyncExpectationResult = Promise<SyncExpectationResult>;
37-
38-
export type ExpectationResult = SyncExpectationResult | AsyncExpectationResult;
39-
40-
export type RawMatcherFn = (
41-
expected: unknown,
42-
actual: unknown,
43-
options?: unknown,
44-
) => ExpectationResult;
45-
// -------END-------
46-
4728
export type RunDetails = {
4829
totalSpecsDefined?: number;
4930
failedExpectations?: SuiteResult['failedExpectations'];

0 commit comments

Comments
 (0)