Skip to content

Commit

Permalink
fix(expect): expose AsymmetricMatchers and RawMatcherFn interfaces (
Browse files Browse the repository at this point in the history
  • Loading branch information
mrazauskas committed Feb 12, 2022
1 parent 60eb416 commit faef0b4
Show file tree
Hide file tree
Showing 14 changed files with 293 additions and 105 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -14,6 +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))
- `[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
20 changes: 20 additions & 0 deletions examples/expect-extend/__tests__/ranges.test.ts
@@ -0,0 +1,20 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

import {expect, test} from '@jest/globals';
import '../toBeWithinRange';

test('is within range', () => expect(100).toBeWithinRange(90, 110));

test('is NOT within range', () => expect(101).not.toBeWithinRange(0, 100));

test('asymmetric ranges', () => {
expect({apples: 6, bananas: 3}).toEqual({
apples: expect.toBeWithinRange(1, 10),
bananas: expect.not.toBeWithinRange(11, 20),
});
});
30 changes: 30 additions & 0 deletions examples/expect-extend/package.json
@@ -0,0 +1,30 @@
{
"private": true,
"version": "0.0.0",
"name": "example-expect-extend",
"devDependencies": {
"@babel/core": "*",
"@babel/preset-env": "*",
"@babel/preset-typescript": "*",
"@jest/globals": "workspace:*",
"babel-jest": "workspace:*",
"expect": "workspace:*",
"jest": "workspace:*"
},
"scripts": {
"test": "jest"
},
"babel": {
"presets": [
[
"@babel/preset-env",
{
"targets": {
"node": "current"
}
}
],
"@babel/preset-typescript"
]
}
}
43 changes: 43 additions & 0 deletions examples/expect-extend/toBeWithinRange.ts
@@ -0,0 +1,43 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

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

const toBeWithinRange: RawMatcherFn = (
actual: number,
floor: number,
ceiling: number,
) => {
const pass = actual >= floor && actual <= ceiling;
if (pass) {
return {
message: () =>
`expected ${actual} not to be within range ${floor} - ${ceiling}`,
pass: true,
};
} else {
return {
message: () =>
`expected ${actual} to be within range ${floor} - ${ceiling}`,
pass: false,
};
}
};

expect.extend({
toBeWithinRange,
});

declare module 'expect' {
interface AsymmetricMatchers {
toBeWithinRange(a: number, b: number): void;
}
interface Matchers<R> {
toBeWithinRange(a: number, b: number): R;
}
}
75 changes: 70 additions & 5 deletions packages/expect/__typetests__/expect.test.ts
Expand Up @@ -5,12 +5,77 @@
* LICENSE file in the root directory of this source tree.
*/

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

type M = expect.Matchers<void, unknown>;
type N = expect.Matchers<void>;
type M = Matchers<void, unknown>;
type N = Matchers<void>;

expectError(() => {
type E = expect.Matchers;
type E = Matchers;
});

// extend

type MatcherUtils = typeof jestMatcherUtils & {
iterableEquality: Tester;
subsetEquality: Tester;
};

expectType<void>(
expect.extend({
toBeWithinRange(actual: number, floor: number, ceiling: number) {
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);

const pass = actual >= floor && actual <= ceiling;
if (pass) {
return {
message: () =>
`expected ${actual} not to be within range ${floor} - ${ceiling}`,
pass: true,
};
} else {
return {
message: () =>
`expected ${actual} to be within range ${floor} - ${ceiling}`,
pass: false,
};
}
},
}),
);

declare module 'expect' {
interface AsymmetricMatchers {
toBeWithinRange(floor: number, ceiling: number): void;
}
interface Matchers<R> {
toBeWithinRange(floor: number, ceiling: number): void;
}
}

expectType<void>(expect(100).toBeWithinRange(90, 110));
expectType<void>(expect(101).not.toBeWithinRange(0, 100));

expectType<void>(
expect({apples: 6, bananas: 3}).toEqual({
apples: expect.toBeWithinRange(1, 10),
bananas: expect.not.toBeWithinRange(11, 20),
}),
);
10 changes: 8 additions & 2 deletions packages/expect/src/index.ts
Expand Up @@ -49,7 +49,13 @@ import type {
ThrowingMatcherFn,
} from './types';

export type {Expect, MatcherState, Matchers} from './types';
export type {
AsymmetricMatchers,
Expect,
MatcherState,
Matchers,
RawMatcherFn,
} from './types';

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

expect.extend = <T extends MatcherState = MatcherState>(
matchers: MatchersObject<T>,
): void => setMatchers(matchers, false, expect);
) => setMatchers(matchers, false, expect);

expect.anything = anything;
expect.any = any;
Expand Down
11 changes: 6 additions & 5 deletions packages/expect/src/types.ts
Expand Up @@ -13,15 +13,16 @@ import {INTERNAL_MATCHER_FLAG} from './jestMatchersObject';

export type SyncExpectationResult = {
pass: boolean;
message: () => string;
message(): string;
};

export type AsyncExpectationResult = Promise<SyncExpectationResult>;

export type ExpectationResult = SyncExpectationResult | AsyncExpectationResult;

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

Expand All @@ -31,7 +32,7 @@ export type PromiseMatcherFn = (actual: any) => Promise<void>;
export type MatcherState = {
assertionCalls: number;
currentTestName?: string;
dontThrow?: () => void;
dontThrow?(): void;
error?: Error;
equals: EqualsFunction;
expand?: boolean;
Expand All @@ -56,7 +57,7 @@ export interface AsymmetricMatcher {
toAsymmetricMatcher?(): string;
}
export type MatchersObject<T extends MatcherState = MatcherState> = {
[id: string]: RawMatcherFn<T>;
[name: string]: RawMatcherFn<T>;
};
export type ExpectedAssertionsErrors = Array<{
actual: string | number;
Expand All @@ -73,7 +74,7 @@ export type Expect<State extends MatcherState = MatcherState> = {
assertions(numberOfAssertions: number): void;
// TODO: remove this `T extends` - should get from some interface merging
extend<T extends MatcherState = State>(matchers: MatchersObject<T>): void;
extractExpectedAssertionsErrors: () => ExpectedAssertionsErrors;
extractExpectedAssertionsErrors(): ExpectedAssertionsErrors;
getState(): State;
hasAssertions(): void;
setState(state: Partial<State>): void;
Expand Down
4 changes: 2 additions & 2 deletions packages/jest-jasmine2/src/jestExpect.ts
Expand Up @@ -7,15 +7,15 @@

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

import {MatcherState, expect} from 'expect';
import {type MatcherState, type RawMatcherFn, expect} from 'expect';
import {
addSerializer,
toMatchInlineSnapshot,
toMatchSnapshot,
toThrowErrorMatchingInlineSnapshot,
toThrowErrorMatchingSnapshot,
} from 'jest-snapshot';
import type {JasmineMatchersObject, RawMatcherFn} from './types';
import type {JasmineMatchersObject} from './types';

export default function jestExpect(config: {expand: boolean}): void {
global.expect = expect;
Expand Down
21 changes: 1 addition & 20 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} from 'expect';
import type {Expect, RawMatcherFn} from 'expect';
import type CallTracker from './jasmine/CallTracker';
import type Env from './jasmine/Env';
import type JsApiReporter from './jasmine/JsApiReporter';
Expand All @@ -25,25 +25,6 @@ export interface AssertionErrorWithStack extends AssertionError {
stack: string;
}

// TODO Add expect types to @jest/types or leave it here
// Borrowed from "expect"
// -------START-------
export type SyncExpectationResult = {
pass: boolean;
message: () => string;
};

export type AsyncExpectationResult = Promise<SyncExpectationResult>;

export type ExpectationResult = SyncExpectationResult | AsyncExpectationResult;

export type RawMatcherFn = (
expected: unknown,
actual: unknown,
options?: unknown,
) => ExpectationResult;
// -------END-------

export type RunDetails = {
totalSpecsDefined?: number;
failedExpectations?: SuiteResult['failedExpectations'];
Expand Down

0 comments on commit faef0b4

Please sign in to comment.