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): allow extending matcher interfaces by moving expect types to @jest/types package #12059

Closed
wants to merge 13 commits into from
4 changes: 2 additions & 2 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ module.exports = {
},
},
{
files: ['test-types/*.test.ts', '*.md'],
files: ['**/__typechecks__/**', '*.md'],
mrazauskas marked this conversation as resolved.
Show resolved Hide resolved
rules: {
'jest/no-focused-tests': 'off',
'jest/no-identical-title': 'off',
Expand Down Expand Up @@ -301,9 +301,9 @@ module.exports = {
'error',
{
devDependencies: [
'/test-types/**',
'**/__tests__/**',
'**/__mocks__/**',
'**/__typechecks__/**',
'**/?(*.)(spec|test).js?(x)',
'scripts/**',
'babel.config.js',
Expand Down
20 changes: 20 additions & 0 deletions examples/expect-extend/__tests__/ranges.test.ts
Original file line number Diff line number Diff line change
@@ -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),
});
});
29 changes: 29 additions & 0 deletions examples/expect-extend/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"private": true,
"version": "0.0.0",
"name": "example-expect-extend",
"devDependencies": {
"@babel/core": "*",
"@babel/preset-env": "*",
"@babel/preset-typescript": "*",
"@jest/globals": "*",
"babel-jest": "*",
"jest": "*"
},
"scripts": {
"test": "jest"
},
"babel": {
"presets": [
[
"@babel/preset-env",
{
"targets": {
"node": "current"
}
}
],
"@babel/preset-typescript"
]
}
}
38 changes: 38 additions & 0 deletions examples/expect-extend/toBeWithinRange.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* 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';

expect.extend({
toBeWithinRange(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,
};
}
},
});

declare module '@jest/types' {
namespace Expect {
interface AsymmetricMatchers {
toBeWithinRange(a: number, b: number): AsymmetricMatcher;
}
interface Matchers<R> {
toBeWithinRange(a: number, b: number): R;
}
}
}
2 changes: 1 addition & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ module.exports = {
require.resolve('jest-snapshot-serializer-raw'),
],
testPathIgnorePatterns: [
'/test-types/',
'/__arbitraries__/',
'/__typechecks__/',
'/node_modules/',
'/examples/',
'/e2e/.*/__tests__',
Expand Down
20 changes: 3 additions & 17 deletions jest.config.types.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,15 @@

'use strict';

const assert = require('assert');
const baseConfig = require('./jest.config');

const {
modulePathIgnorePatterns,
testPathIgnorePatterns,
watchPathIgnorePatterns,
} = baseConfig;

assert.strictEqual(
testPathIgnorePatterns[0],
'/test-types/',
'First entry must be types',
);
const {modulePathIgnorePatterns} = require('./jest.config');

module.exports = {
displayName: {
color: 'blue',
name: 'types',
},
modulePathIgnorePatterns,
roots: ['<rootDir>/packages'],
runner: 'jest-runner-tsd',
testMatch: ['<rootDir>/test-types/*.test.ts'],
testPathIgnorePatterns: testPathIgnorePatterns.slice(1),
watchPathIgnorePatterns,
testMatch: ['**/__typechecks__/**/*.ts'],
};
105 changes: 105 additions & 0 deletions packages/expect/__typechecks__/expect.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/**
* 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 {expectError, expectType} from 'mlh-tsd';
import * as expect from 'expect';
import type * as jestMatcherUtils from 'jest-matcher-utils';

export type M = expect.Matchers<void, unknown>;
export type N = expect.Matchers<void>;
// @ts-expect-error
export type E = expect.Matchers<>;

// extend

type Tester = (a: any, b: any) => boolean | undefined;

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<
(
a: unknown,
b: unknown,
customTesters?: Array<Tester>,
strictCheck?: boolean,
) => boolean
>(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);

// `snapshotState` type should not leak from `@jest/types`

expectError(this.snapshotState);

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 '@jest/types' {
namespace Expect {
interface AsymmetricMatchers {
toBeWithinRange(floor: number, ceiling: number): AsymmetricMatcher;
}
interface Matchers<R> {
toBeWithinRange(floor: number, ceiling: number): R;
}
}
}

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

// `addSnapshotSerializer` type should not leak from `@jest/types`

expectError(expect.addSnapshotSerializer());

// snapshot matchers types should not leak from `@jest/types`

expectError(expect({a: 1}).toMatchSnapshot());
expectError(expect('abc').toMatchInlineSnapshot());

expectError(expect(jest.fn()).toThrowErrorMatchingSnapshot());
expectError(expect(jest.fn()).toThrowErrorMatchingInlineSnapshot());
3 changes: 2 additions & 1 deletion packages/expect/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
"@jest/test-utils": "^27.3.1",
"chalk": "^4.0.0",
"fast-check": "^2.0.0",
"immutable": "^4.0.0-rc.12"
"immutable": "^4.0.0-rc.12",
"mlh-tsd": "^0.14.1"
},
"engines": {
"node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
Expand Down
15 changes: 4 additions & 11 deletions packages/expect/src/asymmetricMatchers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,10 @@
*
*/

import type {Expect} from '@jest/types';
import * as matcherUtils from 'jest-matcher-utils';
import {equals, fnNameFor, hasProperty, isA, isUndefined} from './jasmineUtils';
import {getState} from './jestMatchersObject';
import type {
AsymmetricMatcher as AsymmetricMatcherInterface,
MatcherState,
} from './types';
import {iterableEquality, subsetEquality} from './utils';

const utils = Object.freeze({
Expand All @@ -21,22 +18,18 @@ const utils = Object.freeze({
subsetEquality,
});

export abstract class AsymmetricMatcher<
T,
State extends MatcherState = MatcherState,
> implements AsymmetricMatcherInterface
{
export abstract class AsymmetricMatcher<T> implements Expect.AsymmetricMatcher {
$$typeof = Symbol.for('jest.asymmetricMatcher');

constructor(protected sample: T, protected inverse = false) {}

protected getMatcherContext(): State {
protected getMatcherContext(): Expect.MatcherState {
return {
...getState(),
equals,
isNot: this.inverse,
utils,
} as State;
};
}

abstract asymmetricMatch(other: unknown): boolean;
Expand Down
6 changes: 3 additions & 3 deletions packages/expect/src/extractExpectedAssertionsErrors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@
*
*/

import type {Expect} from '@jest/types';
import {
EXPECTED_COLOR,
RECEIVED_COLOR,
matcherHint,
pluralize,
} from 'jest-matcher-utils';
import {getState, setState} from './jestMatchersObject';
import type {Expect, ExpectedAssertionsErrors} from './types';

const resetAssertionsLocalState = () => {
setState({
Expand All @@ -25,9 +25,9 @@ const resetAssertionsLocalState = () => {

// Create and format all errors related to the mismatched number of `expect`
// calls and reset the matcher's state.
const extractExpectedAssertionsErrors: Expect['extractExpectedAssertionsErrors'] =
const extractExpectedAssertionsErrors: Expect.Expect['extractExpectedAssertionsErrors'] =
() => {
const result: ExpectedAssertionsErrors = [];
const result: Expect.ExpectedAssertionsErrors = [];
const {
assertionCalls,
expectedAssertionsNumber,
Expand Down
Loading