Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: jest-community/eslint-plugin-jest
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v28.5.0
Choose a base ref
...
head repository: jest-community/eslint-plugin-jest
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v28.6.0
Choose a head ref
  • 17 commits
  • 14 files changed
  • 7 contributors

Commits on May 6, 2024

  1. chore(deps): lock file maintenance

    renovate[bot] committed May 6, 2024
    1
    Copy the full SHA
    01b21e9 View commit details

Commits on May 8, 2024

  1. chore(deps): update danger/danger-js action to v12.2.0 (#1580)

    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
    renovate[bot] authored May 8, 2024
    Copy the full SHA
    bd0f8c6 View commit details

Commits on May 11, 2024

  1. Copy the full SHA
    f522573 View commit details
  2. chore: update semantic release (#1586)

    SimenB authored May 11, 2024
    Copy the full SHA
    aaaa36b View commit details

Commits on May 12, 2024

  1. chore(deps): update codecov/codecov-action action to v4 (#1494)

    renovate[bot] authored May 12, 2024

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    c5ebd4e View commit details

Commits on May 13, 2024

  1. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    10c9726 View commit details

Commits on May 20, 2024

  1. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    c581996 View commit details

Commits on May 21, 2024

  1. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    da993a5 View commit details

Commits on May 27, 2024

  1. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    70c8c5e View commit details

Commits on Jun 3, 2024

  1. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    d465125 View commit details

Commits on Jun 6, 2024

  1. chore: run prettier (#1604)

    SimenB authored Jun 6, 2024

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    04421cf View commit details
  2. chore(deps): update danger/danger-js action to v12.3.1 (#1591)

    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
    renovate[bot] authored Jun 6, 2024

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    db31890 View commit details
  3. chore(deps): update dependency semantic-release to v24 (#1602)

    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
    renovate[bot] authored Jun 6, 2024

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    0a14446 View commit details
  4. feat(valid-expect): supporting automatically fixing adding async in s…

    …ome cases (#1579)
    
    * feat: add async
    
    * test: tests for adding async
    
    * feat: add await
    
    * fix: valid-expect test
    
    * Revert "fix: valid-expect test"
    
    This reverts commit e652a25.
    
    * fix: refactor to return an array
    
    * fix: valid-expect logic
    
    * Revert "fix: valid-expect logic"
    
    This reverts commit ae8ecac.
    
    * fix: valid-expect fixer logic
    
    * refactor: fix import
    
    * fix: write format
    y-hsgw authored Jun 6, 2024

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    5b9b47e View commit details
  5. feat(prefer-jest-mocked): add new rule (#1599)

    Co-authored-by: s.v.zaytsev <s.v.zaytsev@tinkoff.ru>
    Co-authored-by: Gareth Jones <Jones258@Gmail.com>
    3 people authored Jun 6, 2024

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    4b6a4f2 View commit details
  6. ci: run docs job on push (#1605)

    G-Rath authored Jun 6, 2024

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    430f024 View commit details
  7. chore(release): 28.6.0 [skip ci]

    # [28.6.0](v28.5.0...v28.6.0) (2024-06-06)
    
    ### Features
    
    * **prefer-jest-mocked:** add new rule ([#1599](#1599)) ([4b6a4f2](4b6a4f2))
    * **valid-expect:** supporting automatically fixing adding async in some cases ([#1579](#1579)) ([5b9b47e](5b9b47e))
    semantic-release-bot committed Jun 6, 2024
    Copy the full SHA
    afdcddd View commit details
2 changes: 1 addition & 1 deletion .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -29,6 +29,6 @@ jobs:
with:
persist-credentials: false
- name: Danger
uses: danger/danger-js@12.1.0
uses: danger/danger-js@12.3.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
8 changes: 5 additions & 3 deletions .github/workflows/nodejs.yml
Original file line number Diff line number Diff line change
@@ -107,8 +107,11 @@ jobs:
run: yarn test --coverage ${{ matrix.eslint-version == 8 }}
env:
CI: true
- uses: codecov/codecov-action@v3
if: ${{ matrix.eslint-version >= 8 }}
- uses: codecov/codecov-action@v4
if: ${{ matrix.eslint-version == 8 }}
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
test-ubuntu:
uses: ./.github/workflows/test.yml
needs: prepare-yarn-cache-ubuntu
@@ -126,7 +129,6 @@ jobs:
os: windows-latest

docs:
if: ${{ github.event_name == 'pull_request' }}
needs: prepare-yarn-cache-ubuntu
runs-on: ubuntu-latest
steps:
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
# [28.6.0](https://github.com/jest-community/eslint-plugin-jest/compare/v28.5.0...v28.6.0) (2024-06-06)


### Features

* **prefer-jest-mocked:** add new rule ([#1599](https://github.com/jest-community/eslint-plugin-jest/issues/1599)) ([4b6a4f2](https://github.com/jest-community/eslint-plugin-jest/commit/4b6a4f29c51ccc2dbb79a2f24d4a5cecd8195a8b))
* **valid-expect:** supporting automatically fixing adding async in some cases ([#1579](https://github.com/jest-community/eslint-plugin-jest/issues/1579)) ([5b9b47e](https://github.com/jest-community/eslint-plugin-jest/commit/5b9b47e3822e7895f8d74d73b0e07e3eff406523))

# [28.5.0](https://github.com/jest-community/eslint-plugin-jest/compare/v28.4.0...v28.5.0) (2024-05-03)


12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -307,10 +307,15 @@ enabled in.\
set to warn in.\
✅ Set in the `recommended`
[configuration](https://github.com/jest-community/eslint-plugin-jest/blob/main/README.md#shareable-configurations).\
🎨 Set in the `style` [configuration](https://github.com/jest-community/eslint-plugin-jest/blob/main/README.md#shareable-configurations).\
🔧 Automatically fixable by the
🎨
Set in the `style`
[configuration](https://github.com/jest-community/eslint-plugin-jest/blob/main/README.md#shareable-configurations).\
🔧
Automatically fixable by the
[`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix).\
💡 Manually fixable by [editor suggestions](https://eslint.org/docs/latest/use/core-concepts#rule-suggestions).
💡
Manually fixable by
[editor suggestions](https://eslint.org/docs/latest/use/core-concepts#rule-suggestions).

| Name                          | Description | 💼 | ⚠️ | 🔧 | 💡 |
| :--------------------------------------------------------------------------- | :------------------------------------------------------------------------ | :-- | :-- | :-- | :-- |
@@ -350,6 +355,7 @@ set to warn in.\
| [prefer-hooks-in-order](docs/rules/prefer-hooks-in-order.md) | Prefer having hooks in a consistent order | | | | |
| [prefer-hooks-on-top](docs/rules/prefer-hooks-on-top.md) | Suggest having hooks before any test cases | | | | |
| [prefer-importing-jest-globals](docs/rules/prefer-importing-jest-globals.md) | Prefer importing Jest globals | | | 🔧 | |
| [prefer-jest-mocked](docs/rules/prefer-jest-mocked.md) | Prefer `jest.mocked()` over `fn as jest.Mock` | | | 🔧 | |
| [prefer-lowercase-title](docs/rules/prefer-lowercase-title.md) | Enforce lowercase test names | | | 🔧 | |
| [prefer-mock-promise-shorthand](docs/rules/prefer-mock-promise-shorthand.md) | Prefer mock resolved/rejected shorthands for promises | | | 🔧 | |
| [prefer-snapshot-hint](docs/rules/prefer-snapshot-hint.md) | Prefer including a hint with external snapshots | | | | |
38 changes: 38 additions & 0 deletions docs/rules/prefer-jest-mocked.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Prefer `jest.mocked()` over `fn as jest.Mock` (`prefer-jest-mocked`)

🔧 This rule is automatically fixable by the
[`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).

<!-- end auto-generated rule header -->

When working with mocks of functions using Jest, it's recommended to use the
`jest.mocked()` helper function to properly type the mocked functions. This rule
enforces the use of `jest.mocked()` for better type safety and readability.

Restricted types:

- `jest.Mock`
- `jest.MockedFunction`
- `jest.MockedClass`
- `jest.MockedObject`

## Rule details

The following patterns are warnings:

```typescript
(foo as jest.Mock).mockReturnValue(1);
const mock = (foo as jest.Mock).mockReturnValue(1);
(foo as unknown as jest.Mock).mockReturnValue(1);
(Obj.foo as jest.Mock).mockReturnValue(1);
([].foo as jest.Mock).mockReturnValue(1);
```

The following patterns are not warnings:

```js
jest.mocked(foo).mockReturnValue(1);
const mock = jest.mocked(foo).mockReturnValue(1);
jest.mocked(Obj.foo).mockReturnValue(1);
jest.mocked([].foo).mockReturnValue(1);
```
3 changes: 2 additions & 1 deletion jest.config.ts
Original file line number Diff line number Diff line change
@@ -37,7 +37,8 @@ const config = {
],
} satisfies Config;

if (semver.major(eslintVersion) >= 9) {
if (semver.major(eslintVersion) !== 8) {
// our eslint config only works for v8
config.projects = config.projects.filter(
({ displayName }) => displayName !== 'lint',
);
11 changes: 2 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "eslint-plugin-jest",
"version": "28.5.0",
"version": "28.6.0",
"description": "ESLint rules for Jest",
"keywords": [
"eslint",
@@ -48,13 +48,6 @@
"singleQuote": true
},
"release": {
"branches": [
"main",
{
"name": "next",
"prerelease": true
}
],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
@@ -106,7 +99,7 @@
"pinst": "^3.0.0",
"prettier": "^3.0.0",
"rimraf": "^5.0.0",
"semantic-release": "^23.0.0",
"semantic-release": "^24.0.0",
"semver": "^7.3.5",
"strip-ansi": "^6.0.0",
"ts-node": "^10.2.1",
2 changes: 2 additions & 0 deletions src/__tests__/__snapshots__/rules.test.ts.snap
Original file line number Diff line number Diff line change
@@ -46,6 +46,7 @@ exports[`rules should export configs that refer to actual rules 1`] = `
"jest/prefer-hooks-in-order": "error",
"jest/prefer-hooks-on-top": "error",
"jest/prefer-importing-jest-globals": "error",
"jest/prefer-jest-mocked": "error",
"jest/prefer-lowercase-title": "error",
"jest/prefer-mock-promise-shorthand": "error",
"jest/prefer-snapshot-hint": "error",
@@ -128,6 +129,7 @@ exports[`rules should export configs that refer to actual rules 1`] = `
"jest/prefer-hooks-in-order": "error",
"jest/prefer-hooks-on-top": "error",
"jest/prefer-importing-jest-globals": "error",
"jest/prefer-jest-mocked": "error",
"jest/prefer-lowercase-title": "error",
"jest/prefer-mock-promise-shorthand": "error",
"jest/prefer-snapshot-hint": "error",
2 changes: 1 addition & 1 deletion src/__tests__/rules.test.ts
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@ import { existsSync } from 'fs';
import { resolve } from 'path';
import plugin from '../';

const numberOfRules = 53;
const numberOfRules = 54;
const ruleNames = Object.keys(plugin.rules);
const deprecatedRules = Object.entries(plugin.rules)
.filter(([, rule]) => rule.meta.deprecated)
347 changes: 347 additions & 0 deletions src/rules/__tests__/prefer-jest-mocked.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,347 @@
import dedent from 'dedent';
import rule from '../prefer-jest-mocked';
import { FlatCompatRuleTester as RuleTester } from './test-utils';

const ruleTester = new RuleTester({
parser: require.resolve('@typescript-eslint/parser'),
});

ruleTester.run('prefer-jest-mocked', rule, {
valid: [
`foo();`,
`jest.mocked(foo).mockReturnValue(1);`,
`bar.mockReturnValue(1);`,
`sinon.stub(foo).returns(1);`,
`foo.mockImplementation(() => 1);`,
`obj.foo();`,
`mockFn.mockReturnValue(1);`,
`arr[0]();`,
`obj.foo.mockReturnValue(1);`,
`jest.spyOn(obj, 'foo').mockReturnValue(1);`,
`(foo as Mock.jest).mockReturnValue(1);`,

dedent`
type MockType = jest.Mock;
const mockFn = jest.fn();
(mockFn as MockType).mockReturnValue(1);
`,
],
invalid: [
{
code: `(foo as jest.Mock).mockReturnValue(1);`,
output: `(jest.mocked(foo)).mockReturnValue(1);`,
options: [],
errors: [
{
messageId: 'useJestMocked',
column: 2,
line: 1,
endColumn: 18,
endLine: 1,
},
],
},
{
code: `(foo as unknown as string as unknown as jest.Mock).mockReturnValue(1);`,
output: `(jest.mocked(foo)).mockReturnValue(1);`,
options: [],
errors: [
{
messageId: 'useJestMocked',
column: 2,
line: 1,
endColumn: 50,
endLine: 1,
},
],
},
{
code: `(foo as unknown as jest.Mock as unknown as jest.Mock).mockReturnValue(1);`,
output: `(jest.mocked(foo)).mockReturnValue(1);`,
options: [],
errors: [
{
messageId: 'useJestMocked',
column: 2,
line: 1,
endColumn: 53,
endLine: 1,
},
],
},
{
code: `(<jest.Mock>foo).mockReturnValue(1);`,
output: `(jest.mocked(foo)).mockReturnValue(1);`,
options: [],
errors: [
{
messageId: 'useJestMocked',
column: 2,
line: 1,
endColumn: 16,
endLine: 1,
},
],
},
{
code: `(foo as jest.Mock).mockImplementation(1);`,
output: `(jest.mocked(foo)).mockImplementation(1);`,
options: [],
errors: [
{
messageId: 'useJestMocked',
column: 2,
line: 1,
endColumn: 18,
endLine: 1,
},
],
},
{
code: `(foo as unknown as jest.Mock).mockReturnValue(1);`,
output: `(jest.mocked(foo)).mockReturnValue(1);`,
options: [],
errors: [
{
messageId: 'useJestMocked',
column: 2,
line: 1,
endColumn: 29,
endLine: 1,
},
],
},
{
code: `(<jest.Mock>foo as unknown).mockReturnValue(1);`,
output: `(jest.mocked(foo) as unknown).mockReturnValue(1);`,
options: [],
errors: [
{
messageId: 'useJestMocked',
column: 2,
line: 1,
endColumn: 16,
endLine: 1,
},
],
},
{
code: `(Obj.foo as jest.Mock).mockReturnValue(1);`,
output: `(jest.mocked(Obj.foo)).mockReturnValue(1);`,
options: [],
errors: [
{
messageId: 'useJestMocked',
column: 2,
line: 1,
endColumn: 22,
endLine: 1,
},
],
},
{
code: `([].foo as jest.Mock).mockReturnValue(1);`,
output: `(jest.mocked([].foo)).mockReturnValue(1);`,
options: [],
errors: [
{
messageId: 'useJestMocked',
column: 2,
line: 1,
endColumn: 21,
endLine: 1,
},
],
},
{
code: `(foo as jest.MockedFunction).mockReturnValue(1);`,
output: `(jest.mocked(foo)).mockReturnValue(1);`,
options: [],
errors: [
{
messageId: 'useJestMocked',
column: 2,
line: 1,
endColumn: 28,
endLine: 1,
},
],
},
{
code: `(foo as jest.MockedFunction).mockImplementation(1);`,
output: `(jest.mocked(foo)).mockImplementation(1);`,
options: [],
errors: [
{
messageId: 'useJestMocked',
column: 2,
line: 1,
endColumn: 28,
endLine: 1,
},
],
},
{
code: `(foo as unknown as jest.MockedFunction).mockReturnValue(1);`,
output: `(jest.mocked(foo)).mockReturnValue(1);`,
options: [],
errors: [
{
messageId: 'useJestMocked',
column: 2,
line: 1,
endColumn: 39,
endLine: 1,
},
],
},
{
code: `(Obj.foo as jest.MockedFunction).mockReturnValue(1);`,
output: `(jest.mocked(Obj.foo)).mockReturnValue(1);`,
options: [],
errors: [
{
messageId: 'useJestMocked',
column: 2,
line: 1,
endColumn: 32,
endLine: 1,
},
],
},
{
code: `(new Array(0).fill(null).foo as jest.MockedFunction).mockReturnValue(1);`,
output: `(jest.mocked(new Array(0).fill(null).foo)).mockReturnValue(1);`,
options: [],
errors: [
{
messageId: 'useJestMocked',
column: 2,
line: 1,
endColumn: 52,
endLine: 1,
},
],
},
{
code: `(jest.fn(() => foo) as jest.MockedFunction).mockReturnValue(1);`,
output: `(jest.mocked(jest.fn(() => foo))).mockReturnValue(1);`,
options: [],
errors: [
{
messageId: 'useJestMocked',
column: 2,
line: 1,
endColumn: 43,
endLine: 1,
},
],
},
{
code: `const mockedUseFocused = useFocused as jest.MockedFunction<typeof useFocused>;`,
output: `const mockedUseFocused = jest.mocked(useFocused);`,
options: [],
errors: [
{
messageId: 'useJestMocked',
column: 26,
line: 1,
endColumn: 78,
endLine: 1,
},
],
},
{
code: `const filter = (MessageService.getMessage as jest.Mock).mock.calls[0][0];`,
output: `const filter = (jest.mocked(MessageService.getMessage)).mock.calls[0][0];`,
options: [],
errors: [
{
messageId: 'useJestMocked',
column: 17,
line: 1,
endColumn: 55,
endLine: 1,
},
],
},
{
code: dedent`
class A {}
(foo as jest.MockedClass<A>)
`,
output: dedent`
class A {}
(jest.mocked(foo))
`,
options: [],
errors: [
{
messageId: 'useJestMocked',
column: 2,
line: 2,
endColumn: 28,
endLine: 2,
},
],
},
{
code: `(foo as jest.MockedObject<{method: () => void}>)`,
output: `(jest.mocked(foo))`,
options: [],
errors: [
{
messageId: 'useJestMocked',
column: 2,
line: 1,
endColumn: 48,
endLine: 1,
},
],
},
{
code: `(Obj['foo'] as jest.MockedFunction).mockReturnValue(1);`,
output: `(jest.mocked(Obj['foo'])).mockReturnValue(1);`,
options: [],
errors: [
{
messageId: 'useJestMocked',
column: 2,
line: 1,
endColumn: 35,
endLine: 1,
},
],
},
{
code: dedent`
(
new Array(100)
.fill(undefined)
.map(x => x.value)
.filter(v => !!v).myProperty as jest.MockedFunction<{
method: () => void;
}>
).mockReturnValue(1);
`,
output: dedent`
(
jest.mocked(new Array(100)
.fill(undefined)
.map(x => x.value)
.filter(v => !!v).myProperty)
).mockReturnValue(1);
`,
options: [],
errors: [
{
messageId: 'useJestMocked',
column: 3,
line: 2,
endColumn: 5,
endLine: 7,
},
],
},
],
});
119 changes: 116 additions & 3 deletions src/rules/__tests__/valid-expect.test.ts
Original file line number Diff line number Diff line change
@@ -144,7 +144,6 @@ ruleTester.run('valid-expect', rule, {
},
],
},

{
code: 'expect().toBe(true);',
errors: [
@@ -417,7 +416,6 @@ ruleTester.run('valid-expect', rule, {
},
],
},

{
code: dedent`
expect.extend({
@@ -428,6 +426,15 @@ ruleTester.run('valid-expect', rule, {
}
});
`,
output: dedent`
expect.extend({
async toResolve(obj) {
this.isNot
? expect(obj).toBe(true)
: await expect(obj).resolves.not.toThrow();
}
});
`,
errors: [
{
column: 9,
@@ -446,6 +453,15 @@ ruleTester.run('valid-expect', rule, {
}
});
`,
output: dedent`
expect.extend({
async toResolve(obj) {
this.isNot
? await expect(obj).resolves.not.toThrow()
: expect(obj).toBe(true);
}
});
`,
errors: [
{
column: 9,
@@ -466,6 +482,17 @@ ruleTester.run('valid-expect', rule, {
}
});
`,
output: dedent`
expect.extend({
async toResolve(obj) {
this.isNot
? expect(obj).toBe(true)
: anotherCondition
? await expect(obj).resolves.not.toThrow()
: expect(obj).toBe(false)
}
});
`,
errors: [
{
column: 9,
@@ -478,6 +505,8 @@ ruleTester.run('valid-expect', rule, {
// expect().resolves
{
code: 'test("valid-expect", () => { expect(Promise.resolve(2)).resolves.toBeDefined(); });',
output:
'test("valid-expect", async () => { await expect(Promise.resolve(2)).resolves.toBeDefined(); });',
errors: [
{
column: 30,
@@ -489,6 +518,8 @@ ruleTester.run('valid-expect', rule, {
},
{
code: 'test("valid-expect", () => { expect(Promise.resolve(2)).toResolve(); });',
output:
'test("valid-expect", async () => { await expect(Promise.resolve(2)).toResolve(); });',
errors: [
{
messageId: 'asyncMustBeAwaited',
@@ -500,6 +531,8 @@ ruleTester.run('valid-expect', rule, {
},
{
code: 'test("valid-expect", () => { expect(Promise.resolve(2)).toResolve(); });',
output:
'test("valid-expect", async () => { await expect(Promise.resolve(2)).toResolve(); });',
options: [{ asyncMatchers: undefined }],
errors: [
{
@@ -512,6 +545,8 @@ ruleTester.run('valid-expect', rule, {
},
{
code: 'test("valid-expect", () => { expect(Promise.resolve(2)).toReject(); });',
output:
'test("valid-expect", async () => { await expect(Promise.resolve(2)).toReject(); });',
errors: [
{
messageId: 'asyncMustBeAwaited',
@@ -523,6 +558,8 @@ ruleTester.run('valid-expect', rule, {
},
{
code: 'test("valid-expect", () => { expect(Promise.resolve(2)).not.toReject(); });',
output:
'test("valid-expect", async () => { await expect(Promise.resolve(2)).not.toReject(); });',
errors: [
{
messageId: 'asyncMustBeAwaited',
@@ -535,6 +572,8 @@ ruleTester.run('valid-expect', rule, {
// expect().resolves.not
{
code: 'test("valid-expect", () => { expect(Promise.resolve(2)).resolves.not.toBeDefined(); });',
output:
'test("valid-expect", async () => { await expect(Promise.resolve(2)).resolves.not.toBeDefined(); });',
errors: [
{
column: 30,
@@ -547,6 +586,8 @@ ruleTester.run('valid-expect', rule, {
// expect().rejects
{
code: 'test("valid-expect", () => { expect(Promise.resolve(2)).rejects.toBeDefined(); });',
output:
'test("valid-expect", async () => { await expect(Promise.resolve(2)).rejects.toBeDefined(); });',
errors: [
{
column: 30,
@@ -559,6 +600,8 @@ ruleTester.run('valid-expect', rule, {
// expect().rejects.not
{
code: 'test("valid-expect", () => { expect(Promise.resolve(2)).rejects.not.toBeDefined(); });',
output:
'test("valid-expect", async () => { await expect(Promise.resolve(2)).rejects.not.toBeDefined(); });',
errors: [
{
column: 30,
@@ -597,6 +640,8 @@ ruleTester.run('valid-expect', rule, {
},
{
code: 'test("valid-expect", () => { expect(Promise.reject(2)).toRejectWith(2); });',
output:
'test("valid-expect", async () => { await expect(Promise.reject(2)).toRejectWith(2); });',
options: [{ asyncMatchers: ['toRejectWith'] }],
errors: [
{
@@ -608,6 +653,8 @@ ruleTester.run('valid-expect', rule, {
},
{
code: 'test("valid-expect", () => { expect(Promise.reject(2)).rejects.toBe(2); });',
output:
'test("valid-expect", async () => { await expect(Promise.reject(2)).rejects.toBe(2); });',
options: [{ asyncMatchers: ['toRejectWith'] }],
errors: [
{
@@ -785,6 +832,11 @@ ruleTester.run('valid-expect', rule, {
Promise.resolve(expect(Promise.resolve(2)).resolves.not.toBeDefined());
});
`,
output: dedent`
test("valid-expect", async () => {
await Promise.resolve(expect(Promise.resolve(2)).resolves.not.toBeDefined());
});
`,
errors: [
{
line: 2,
@@ -801,6 +853,11 @@ ruleTester.run('valid-expect', rule, {
Promise.reject(expect(Promise.resolve(2)).resolves.not.toBeDefined());
});
`,
output: dedent`
test("valid-expect", async () => {
await Promise.reject(expect(Promise.resolve(2)).resolves.not.toBeDefined());
});
`,
errors: [
{
line: 2,
@@ -838,6 +895,11 @@ ruleTester.run('valid-expect', rule, {
Promise.x(expect(Promise.resolve(2)).resolves.not.toBeDefined());
});
`,
output: dedent`
test("valid-expect", async () => {
await Promise.x(expect(Promise.resolve(2)).resolves.not.toBeDefined());
});
`,
errors: [
{
line: 2,
@@ -855,6 +917,11 @@ ruleTester.run('valid-expect', rule, {
Promise.resolve(expect(Promise.resolve(2)).resolves.not.toBeDefined());
});
`,
output: dedent`
test("valid-expect", async () => {
await Promise.resolve(expect(Promise.resolve(2)).resolves.not.toBeDefined());
});
`,
options: [{ alwaysAwait: true }],
errors: [
{
@@ -875,6 +942,14 @@ ruleTester.run('valid-expect', rule, {
]);
});
`,
output: dedent`
test("valid-expect", async () => {
await Promise.all([
expect(Promise.resolve(2)).resolves.not.toBeDefined(),
expect(Promise.resolve(3)).resolves.not.toBeDefined(),
]);
});
`,
errors: [
{
line: 2,
@@ -896,6 +971,14 @@ ruleTester.run('valid-expect', rule, {
]);
});
`,
output: dedent`
test("valid-expect", async () => {
await Promise.x([
expect(Promise.resolve(2)).resolves.not.toBeDefined(),
expect(Promise.resolve(3)).resolves.not.toBeDefined(),
]);
});
`,
errors: [
{
line: 2,
@@ -907,7 +990,6 @@ ruleTester.run('valid-expect', rule, {
},
],
},
//
{
code: dedent`
test("valid-expect", () => {
@@ -917,6 +999,14 @@ ruleTester.run('valid-expect', rule, {
]
});
`,
output: dedent`
test("valid-expect", async () => {
const assertions = [
await expect(Promise.resolve(2)).resolves.not.toBeDefined(),
await expect(Promise.resolve(3)).resolves.not.toBeDefined(),
]
});
`,
errors: [
{
line: 3,
@@ -945,6 +1035,14 @@ ruleTester.run('valid-expect', rule, {
]
});
`,
output: dedent`
test("valid-expect", async () => {
const assertions = [
await expect(Promise.resolve(2)).toResolve(),
await expect(Promise.resolve(3)).toReject(),
]
});
`,
errors: [
{
messageId: 'asyncMustBeAwaited',
@@ -969,6 +1067,14 @@ ruleTester.run('valid-expect', rule, {
]
});
`,
output: dedent`
test("valid-expect", async () => {
const assertions = [
await expect(Promise.resolve(2)).not.toResolve(),
await expect(Promise.resolve(3)).resolves.toReject(),
]
});
`,
errors: [
{
messageId: 'asyncMustBeAwaited',
@@ -1002,6 +1108,13 @@ ruleTester.run('valid-expect', rule, {
});
});
`,
output: dedent`
test("valid-expect", () => {
return expect(functionReturningAPromise()).resolves.toEqual(1).then(async () => {
await expect(Promise.resolve(2)).resolves.toBe(1);
});
});
`,
errors: [
{
line: 3,
71 changes: 71 additions & 0 deletions src/rules/prefer-jest-mocked.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { AST_NODE_TYPES, type TSESTree } from '@typescript-eslint/utils';
import { createRule, followTypeAssertionChain, getSourceCode } from './utils';

const mockTypes = ['Mock', 'MockedFunction', 'MockedClass', 'MockedObject'];

export default createRule({
name: __filename,
meta: {
docs: {
description: 'Prefer `jest.mocked()` over `fn as jest.Mock`',
},
messages: {
useJestMocked: 'Prefer `jest.mocked()`',
},
schema: [],
type: 'suggestion',
fixable: 'code',
},
defaultOptions: [],
create(context) {
function check(node: TSESTree.TSAsExpression | TSESTree.TSTypeAssertion) {
const { typeAnnotation } = node;

if (typeAnnotation.type !== AST_NODE_TYPES.TSTypeReference) {
return;
}

const { typeName } = typeAnnotation;

if (typeName.type !== AST_NODE_TYPES.TSQualifiedName) {
return;
}

const { left, right } = typeName;

if (
left.type !== AST_NODE_TYPES.Identifier ||
right.type !== AST_NODE_TYPES.Identifier ||
left.name !== 'jest' ||
!mockTypes.includes(right.name)
) {
return;
}

const fnName = getSourceCode(context).text.slice(
...followTypeAssertionChain(node.expression).range,
);

context.report({
node: node,
messageId: 'useJestMocked',
fix(fixer) {
return fixer.replaceText(node, `jest.mocked(${fnName})`);
},
});
}

return {
TSAsExpression(node) {
if (node.parent.type === AST_NODE_TYPES.TSAsExpression) {
return;
}

check(node);
},
TSTypeAssertion(node) {
check(node);
},
};
},
});
95 changes: 72 additions & 23 deletions src/rules/valid-expect.ts
Original file line number Diff line number Diff line change
@@ -3,8 +3,13 @@
* MIT license, Tom Vincent.
*/

import { AST_NODE_TYPES, type TSESTree } from '@typescript-eslint/utils';
import {
AST_NODE_TYPES,
type TSESLint,
type TSESTree,
} from '@typescript-eslint/utils';
import {
type FunctionExpression,
ModifierName,
createRule,
getAccessorValue,
@@ -50,16 +55,30 @@ const findPromiseCallExpressionNode = (node: TSESTree.Node) =>
? getPromiseCallExpressionNode(node.parent)
: null;

const findFirstAsyncFunction = ({
const findFirstFunctionExpression = ({
parent,
}: TSESTree.Node): TSESTree.Node | null => {
}: TSESTree.Node): FunctionExpression | null => {
if (!parent) {
return null;
}

return isFunction(parent) && parent.async
? parent
: findFirstAsyncFunction(parent);
return isFunction(parent) ? parent : findFirstFunctionExpression(parent);
};

const getNormalizeFunctionExpression = (
functionExpression: FunctionExpression,
):
| TSESTree.PropertyComputedName
| TSESTree.PropertyNonComputedName
| FunctionExpression => {
if (
functionExpression.parent.type === AST_NODE_TYPES.Property &&
functionExpression.type === AST_NODE_TYPES.FunctionExpression
) {
return functionExpression.parent;
}

return functionExpression;
};

const getParentIfThenified = (node: TSESTree.Node): TSESTree.Node => {
@@ -189,6 +208,13 @@ export default createRule<[Options], MessageIds>({
) {
// Context state
const arrayExceptions = new Set<string>();
const descriptors: Array<{
node: TSESTree.Node;
messageId: Extract<
MessageIds,
'asyncMustBeAwaited' | 'promisesWithAsyncAssertionsMustBeAwaited'
>;
}> = [];

const pushPromiseArrayException = (loc: TSESTree.SourceLocation) =>
arrayExceptions.add(promiseArrayExceptionKey(loc));
@@ -320,7 +346,7 @@ export default createRule<[Options], MessageIds>({
jestFnCall.modifiers.some(nod => getAccessorValue(nod) !== 'not') ||
asyncMatchers.includes(getAccessorValue(matcher));

if (!parentNode?.parent || !shouldBeAwaited) {
if (!parentNode.parent || !shouldBeAwaited) {
return;
}
/**
@@ -329,7 +355,6 @@ export default createRule<[Options], MessageIds>({
*/
const isParentArrayExpression =
parentNode.parent.type === AST_NODE_TYPES.ArrayExpression;
const orReturned = alwaysAwait ? '' : ' or returned';
/**
* An async assertion can be chained with `then` or `catch` statements.
* In that case our target CallExpression node is the one with
@@ -346,39 +371,63 @@ export default createRule<[Options], MessageIds>({
// if we didn't warn user already
!promiseArrayExceptionExists(finalNode.loc)
) {
context.report({
loc: finalNode.loc,
data: { orReturned },
descriptors.push({
node: finalNode,
messageId:
finalNode === targetNode
targetNode === finalNode
? 'asyncMustBeAwaited'
: 'promisesWithAsyncAssertionsMustBeAwaited',
});
}
if (isParentArrayExpression) {
pushPromiseArrayException(finalNode.loc);
}
},
'Program:exit'() {
const fixes: TSESLint.RuleFix[] = [];

descriptors.forEach(({ node, messageId }, index) => {
const orReturned = alwaysAwait ? '' : ' or returned';

context.report({
loc: node.loc,
data: { orReturned },
messageId,
node,
fix(fixer) {
if (!findFirstAsyncFunction(finalNode)) {
return [];
const functionExpression = findFirstFunctionExpression(node);

if (!functionExpression) {
return null;
}
const foundAsyncFixer = fixes.some(fix => fix.text === 'async ');

if (!functionExpression.async && !foundAsyncFixer) {
const targetFunction =
getNormalizeFunctionExpression(functionExpression);

fixes.push(fixer.insertTextBefore(targetFunction, 'async '));
}

const returnStatement =
finalNode.parent?.type === AST_NODE_TYPES.ReturnStatement
? finalNode.parent
node.parent?.type === AST_NODE_TYPES.ReturnStatement
? node.parent
: null;

if (alwaysAwait && returnStatement) {
const sourceCodeText =
getSourceCode(context).getText(returnStatement);
const replacedText = sourceCodeText.replace('return', 'await');

return fixer.replaceText(returnStatement, replacedText);
fixes.push(fixer.replaceText(returnStatement, replacedText));
} else {
fixes.push(fixer.insertTextBefore(node, 'await '));
}

return fixer.insertTextBefore(finalNode, 'await ');
return index === descriptors.length - 1 ? fixes : null;
},
});

if (isParentArrayExpression) {
pushPromiseArrayException(finalNode.loc);
}
}
});
},
};
},
2,517 changes: 1,235 additions & 1,282 deletions yarn.lock

Large diffs are not rendered by default.