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(jest): allow enabling Jest global types through "types": ["jest"] in tsconfig.json #12856

Closed
wants to merge 20 commits into from
Closed
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

### Features

- `[jest]` Allow enabling Jest global types through `"types": ["jest"]` in `tsconfig.json` ([#12856](https://github.com/facebook/jest/pull/12856))
- `[@jest/reporters]` Improve `GitHubActionsReporter`s annotation format ([#12826](https://github.com/facebook/jest/pull/12826))

### Fixes
Expand Down
124 changes: 99 additions & 25 deletions docs/ExpectAPI.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ 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 assume you are using type definitions from [`jest`](GettingStarted.md/#type-definitions).

:::

## Methods

import TOCInline from '@theme/TOCInline';
Expand All @@ -17,6 +23,9 @@ import TOCInline from '@theme/TOCInline';

## Reference

import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

### `expect(value)`

The `expect` function is used every time you want to test a value. You will rarely call `expect` by itself. Instead, you will use `expect` along with a "matcher" function to assert something about a value.
Expand All @@ -37,56 +46,121 @@ 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:

<Tabs groupId="examples">
<TabItem value="js" label="JavaScript">

```js
const toBeWithinRange = 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 ${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,
});
```

</TabItem>

<TabItem value="ts" label="TypeScript">

```ts
import type {MatcherFunction} from 'expect';

const toBeWithinRange: MatcherFunction<[floor: number, ceiling: number]> =
function (actual: unknown, floor: unknown, ceiling: unknown) {
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}`,
`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,
});

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

</TabItem>
</Tabs>

And use it in a test:

```js
test('numeric ranges', () => {
expect(100).toBeWithinRange(90, 110);

expect(101).not.toBeWithinRange(0, 100);

expect({apples: 6, bananas: 3}).toEqual({
apples: expect.toBeWithinRange(1, 10),
bananas: expect.not.toBeWithinRange(11, 20),
});
});
```

:::note

In TypeScript, when using `@types/jest` for example, you can declare the new `toBeWithinRange` matcher in the imported module like this:
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This page can have more TS examples, of course. I just wanted to remove this old pain.


```ts
interface CustomMatchers<R = unknown> {
toBeWithinRange(floor: number, ceiling: number): R;
}

declare global {
namespace jest {
interface Expect extends CustomMatchers {}
interface Matchers<R> extends CustomMatchers<R> {}
interface InverseAsymmetricMatchers extends CustomMatchers {}
}
}
```

:::

#### Async Matchers

`expect.extend` also supports async matchers. Async matchers return a Promise so you will need to await the returned value. Let's use an example matcher to illustrate the usage of them. We are going to implement a matcher called `toBeDivisibleByExternalValue`, where the divisible number is going to be pulled from an external source.
Expand Down
29 changes: 25 additions & 4 deletions docs/GettingStarted.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,10 +156,31 @@ npm install --save-dev ts-jest

#### Type definitions

You may also want to install the [`@types/jest`](https://www.npmjs.com/package/@types/jest) module for the version of Jest you're using. This will help provide full typing when writing your tests with TypeScript.
To enable type definitions of [Jest globals](GlobalAPI.md) add `"jest"` to the `"types"` list of your `tsconfig.json`:

> For `@types/*` modules it's recommended to try to match the version of the associated module. For example, if you are using `26.4.0` of `jest` then using `26.4.x` of `@types/jest` is ideal. In general, try to match the major (`26`) and minor (`4`) version as closely as possible.
```json title="tsconfig.json"
{
"compilerOptions": {
"types": ["jest"]
}
}
```

```bash npm2yarn
npm install --save-dev @types/jest
Alternatively you may use explicit imports from `@jest/globals` package:

```ts title="sum.test.ts"
import {describe, expect, test} from '@jest/globals';
import {sum} from './sum';

describe('sum module', () => {
test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3);
});
});
```

:::info

If you had `@types/jest` installed in your project before, remember to remove it.

:::
8 changes: 2 additions & 6 deletions docs/MockFunctionAPI.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,7 @@ Mock functions are also known as "spies", because they let you spy on the behavi

:::info

The TypeScript examples from this page will only work as document if you import `jest` from `'@jest/globals'`:

```ts
import {jest} from '@jest/globals';
```
The TypeScript examples from this page assume you are using type definitions from [`jest`](GettingStarted.md/#type-definitions).

:::

Expand Down Expand Up @@ -178,7 +174,7 @@ mockFn(3); // 39

<TabItem value="ts" label="TypeScript">

```js
```ts
const mockFn = jest.fn((scalar: number) => 42 + scalar);

mockFn(0); // 42
Expand Down
19 changes: 14 additions & 5 deletions docs/UpgradingToJest28.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,16 +189,25 @@ Known examples of packages that fails in Jest 28 are [`uuid`](https://npmjs.com/

## TypeScript

:::info
From Jest 28.2 you can replace `@types/jest` package with native type definitions. Simply add `"jest"` to the `"types"` list of your `tsconfig.json` and enjoy typed testing!

```diff title="tsconfig.json"
{
"compilerOptions": {
- "types": ["@types/jest"]
+ "types": ["jest"]
}
}
```

The TypeScript examples from this page will only work as document if you import `jest` from `'@jest/globals'`:
:::info

```ts
import {jest} from '@jest/globals';
```
If you had `@types/jest` installed in your project before, remember to remove it.

:::

The TypeScript examples bellow assume you are using type definitions from `jest`.

### `jest.fn()`

`jest.fn()` now takes only one generic type argument. See [Mock Functions API](MockFunctionAPI.md) page for more usage examples.
Expand Down
4 changes: 3 additions & 1 deletion e2e/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
"moduleResolution": "node",
"isolatedModules": true,
"importsNotUsedAsValues": "error",
"resolveJsonModule": true
"resolveJsonModule": true,

mrazauskas marked this conversation as resolved.
Show resolved Hide resolved
"types": ["jest"]
}
}
1 change: 0 additions & 1 deletion examples/angular/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
"@babel/plugin-proposal-decorators": "*",
"@babel/preset-env": "^7.1.0",
"@babel/preset-typescript": "^7.0.0",
"@types/jest": "^27.4.0",
"babel-jest": "workspace:*",
"babel-plugin-transform-typescript-metadata": "*",
"jest": "workspace:*",
Expand Down
1 change: 0 additions & 1 deletion examples/expect-extend/__tests__/ranges.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
* 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));
Expand Down
1 change: 0 additions & 1 deletion examples/expect-extend/toBeWithinRange.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
* LICENSE file in the root directory of this source tree.
*/

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

const toBeWithinRange: MatcherFunction<[floor: number, ceiling: number]> =
Expand Down
3 changes: 2 additions & 1 deletion examples/expect-extend/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"compilerOptions": {
"strict": true
"strict": true,
"types": ["jest"]
},
"include": ["./**/*"]
}
1 change: 0 additions & 1 deletion examples/typescript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
"@babel/preset-env": "^7.1.0",
"@babel/preset-react": "^7.12.1",
"@babel/preset-typescript": "^7.0.0",
"@types/jest": "^27.4.0",
"babel-jest": "workspace:*",
"jest": "workspace:*"
},
Expand Down
3 changes: 2 additions & 1 deletion examples/typescript/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"compilerOptions": {
"module": "commonjs",
"jsx": "react-jsx"
"jsx": "react-jsx",
"types": ["jest"]
}
}
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
"@types/babel__generator": "^7.0.0",
"@types/babel__template": "^7.0.2",
"@types/dedent": "^0.7.0",
"@types/jest": "^27.4.0",
"@types/node": "~12.12.0",
"@types/which": "^2.0.0",
"@typescript-eslint/eslint-plugin": "^5.14.0",
Expand Down
10 changes: 10 additions & 0 deletions packages/babel-jest/src/__tests__/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

__tests__ and e2e directories needs additional tsconfig.json with "types": ["jest"]. That’s a lot of new files. For now I keep them all the same hoping this would help to review the PR. In few test suites some type fixing here and there is needed. Not touching these now. I will come back with fix PRs case-by-case.

Also here I have to extend the root tsconfig.json, because tsconfig.json in the enclosing directory is excluding __tests__. Seems like excludes can not be overridden.

"extends": "../../../../tsconfig.json",
"compilerOptions": {
"composite": false,
"esModuleInterop": true,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need this? (applies to all)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some tests needs it. For example, commenting out "esModuleInterop": true give type error here:

https://github.com/facebook/jest/blob/ae8cf9a78cb0cd2f54d3bacbc1c8837e540e21dc/packages/pretty-format/src/__tests__/react.test.tsx#L8

My idea was to use the same tsconfig.json file for all packages to simplify review. But I can go through and enable it case by case. This option applies only for files inside __tests__ directories. Perhaps it is fine to keep it?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A separate PR making it unneeded would be awesome, but shouldn't clutter up this pr

"rootDir": "../",
"types": ["jest"]
},
"include": ["../**/*"]
}
10 changes: 10 additions & 0 deletions packages/babel-plugin-jest-hoist/src/__tests__/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"extends": "../../../../tsconfig.json",
"compilerOptions": {
"composite": false,
"esModuleInterop": true,
"rootDir": "../",
"types": ["jest"]
},
"include": ["../**/*"]
}
10 changes: 10 additions & 0 deletions packages/diff-sequences/src/__tests__/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"extends": "../../../../tsconfig.json",
"compilerOptions": {
"composite": false,
"esModuleInterop": true,
"rootDir": "../",
"types": ["jest"]
},
"include": ["../**/*"]
}
10 changes: 10 additions & 0 deletions packages/expect-utils/src/__tests__/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"extends": "../../../../tsconfig.json",
"compilerOptions": {
"composite": false,
"esModuleInterop": true,
"rootDir": "../",
"types": ["jest"]
},
"include": ["../**/*"]
}
10 changes: 10 additions & 0 deletions packages/expect/src/__tests__/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"extends": "../../../../tsconfig.json",
"compilerOptions": {
"composite": false,
"esModuleInterop": true,
"rootDir": "../",
"types": ["jest"]
},
"include": ["../**/*"]
}