diff --git a/docs/ExpectAPI.md b/docs/ExpectAPI.md index aa25136d861b..34291ecbec20 100644 --- a/docs/ExpectAPI.md +++ b/docs/ExpectAPI.md @@ -7,6 +7,16 @@ When you're writing tests, you often need to check that values meet certain cond For additional Jest matchers maintained by the Jest Community check out [`jest-extended`](https://github.com/jest-community/jest-extended). +:::info + +The TypeScript examples from this page will only work as documented if you import `expect` from `'@jest/globals'`: + +```ts +import {expect} from '@jest/globals'; +``` + +::: + ## Methods import TOCInline from '@theme/TOCInline'; @@ -37,29 +47,141 @@ The argument to `expect` should be the value that your code produces, and any ar You can use `expect.extend` to add your own matchers to Jest. For example, let's say that you're testing a number utility library and you're frequently asserting that numbers appear within particular ranges of other numbers. You could abstract that into a `toBeWithinRange` matcher: -```js +```js tab={"span":3} title="toBeWithinRange.js" +import {expect} from '@jest/globals'; + +function toBeWithinRange(actual, floor, ceiling) { + if ( + typeof actual !== 'number' || + typeof floor !== 'number' || + typeof ceiling !== 'number' + ) { + throw new Error('These must be of type number!'); + } + + const pass = actual >= floor && actual <= ceiling; + if (pass) { + return { + message: () => + `expected ${this.utils.printReceived( + actual, + )} not to be within range ${this.utils.printExpected( + `${floor} - ${ceiling}`, + )}`, + pass: true, + }; + } else { + return { + message: () => + `expected ${this.utils.printReceived( + actual, + )} to be within range ${this.utils.printExpected( + `${floor} - ${ceiling}`, + )}`, + pass: false, + }; + } +} + expect.extend({ - toBeWithinRange(received, floor, ceiling) { - const pass = received >= floor && received <= ceiling; + toBeWithinRange, +}); +``` + +```js title="__tests__/ranges.test.js" +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), + }); +}); +``` + +```ts title="toBeWithinRange.d.ts" +// optionally add a type declaration, e.g. it enables autocompletion in IDEs +declare module 'expect' { + interface AsymmetricMatchers { + toBeWithinRange(floor: number, ceiling: number): void; + } + interface Matchers { + toBeWithinRange(floor: number, ceiling: number): R; + } +} + +export {}; +``` + +```ts tab={"span":2} title="toBeWithinRange.ts" +import {expect} from '@jest/globals'; +import type {MatcherFunction} from 'expect'; + +const toBeWithinRange: MatcherFunction<[floor: unknown, ceiling: unknown]> = + // `floor` and `ceiling` get types from the line above + // it is recommended to type them as `unknown` and to validate the values + function (actual, floor, ceiling) { + if ( + typeof actual !== 'number' || + typeof floor !== 'number' || + typeof ceiling !== 'number' + ) { + throw new Error('These must be of type number!'); + } + + const pass = actual >= floor && actual <= ceiling; if (pass) { return { message: () => - `expected ${received} not to be within range ${floor} - ${ceiling}`, + // `this` context will have correct typings + `expected ${this.utils.printReceived( + actual, + )} not to be within range ${this.utils.printExpected( + `${floor} - ${ceiling}`, + )}`, pass: true, }; } else { return { message: () => - `expected ${received} to be within range ${floor} - ${ceiling}`, + `expected ${this.utils.printReceived( + actual, + )} to be within range ${this.utils.printExpected( + `${floor} - ${ceiling}`, + )}`, pass: false, }; } - }, + }; + +expect.extend({ + toBeWithinRange, }); -test('numeric ranges', () => { - expect(100).toBeWithinRange(90, 110); - expect(101).not.toBeWithinRange(0, 100); +declare module 'expect' { + interface AsymmetricMatchers { + toBeWithinRange(floor: number, ceiling: number): void; + } + interface Matchers { + toBeWithinRange(floor: number, ceiling: number): R; + } +} +``` + +```ts tab title="__tests__/ranges.test.ts" +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), @@ -67,22 +189,24 @@ test('numeric ranges', () => { }); ``` -:::note +:::tip -In TypeScript, when using `@types/jest` for example, you can declare the new `toBeWithinRange` matcher in the imported module like this: +The type declaration of the matcher can live in a `.d.ts` file or in an imported `.ts` module (see JS and TS examples above respectively). If you keep the declaration in a `.d.ts` file, make sure that it is included in the program and that it is a valid module, i.e. it has at least an empty `export {}`. -```ts -interface CustomMatchers { - toBeWithinRange(floor: number, ceiling: number): R; -} +::: -declare global { - namespace jest { - interface Expect extends CustomMatchers {} - interface Matchers extends CustomMatchers {} - interface InverseAsymmetricMatchers extends CustomMatchers {} - } -} +:::tip + +Instead of importing `toBeWithinRange` module to the test file, you can enable the matcher for all tests by moving the `expect.extend` call to a [`setupFilesAfterEnv`](Configuration.md/#setupfilesafterenv-array) script: + +```js +import {expect} from '@jest/globals'; +// remember to export `toBeWithinRange` as well +import {toBeWithinRange} from './toBeWithinRange'; + +expect.extend({ + toBeWithinRange, +}); ``` ::: diff --git a/examples/expect-extend/package.json b/examples/expect-extend/package.json index 161c3c2b5b3b..c1c5e8ea2798 100644 --- a/examples/expect-extend/package.json +++ b/examples/expect-extend/package.json @@ -9,7 +9,8 @@ "@jest/globals": "workspace:*", "babel-jest": "workspace:*", "expect": "workspace:*", - "jest": "workspace:*" + "jest": "workspace:*", + "typescript": "^4.8.2" }, "scripts": { "test": "jest" diff --git a/examples/expect-extend/toBeWithinRange.ts b/examples/expect-extend/toBeWithinRange.ts index 43be8cfe431a..a7492603b9ab 100644 --- a/examples/expect-extend/toBeWithinRange.ts +++ b/examples/expect-extend/toBeWithinRange.ts @@ -8,8 +8,8 @@ import {expect} from '@jest/globals'; import type {MatcherFunction} from 'expect'; -const toBeWithinRange: MatcherFunction<[floor: number, ceiling: number]> = - function (actual: unknown, floor: unknown, ceiling: unknown) { +const toBeWithinRange: MatcherFunction<[floor: unknown, ceiling: unknown]> = + function (actual, floor, ceiling) { if ( typeof actual !== 'number' || typeof floor !== 'number' || diff --git a/yarn.lock b/yarn.lock index 751b3a9ff96c..9082e4108ef3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9392,6 +9392,7 @@ __metadata: babel-jest: "workspace:*" expect: "workspace:*" jest: "workspace:*" + typescript: ^4.8.2 languageName: unknown linkType: soft