diff --git a/README.md b/README.md index 80790dceb..6f1b6a3ad 100644 --- a/README.md +++ b/README.md @@ -220,6 +220,7 @@ installations requiring long-term consistency. | [no-jasmine-globals](docs/rules/no-jasmine-globals.md) | Disallow Jasmine globals | ![recommended][] | ![fixable][] | | [no-large-snapshots](docs/rules/no-large-snapshots.md) | disallow large snapshots | | | | [no-mocks-import](docs/rules/no-mocks-import.md) | Disallow manually importing from `__mocks__` | ![recommended][] | | +| [no-restricted-jest-methods](docs/rules/no-restricted-jest-methods.md) | Disallow specific `jest.` methods | | | | [no-restricted-matchers](docs/rules/no-restricted-matchers.md) | Disallow specific matchers & modifiers | | | | [no-standalone-expect](docs/rules/no-standalone-expect.md) | Disallow using `expect` outside of `it` or `test` blocks | ![recommended][] | | | [no-test-prefixes](docs/rules/no-test-prefixes.md) | Use `.only` and `.skip` over `f` and `x` | ![recommended][] | ![fixable][] | diff --git a/docs/rules/no-restricted-jest-methods.md b/docs/rules/no-restricted-jest-methods.md new file mode 100644 index 000000000..43a2685d6 --- /dev/null +++ b/docs/rules/no-restricted-jest-methods.md @@ -0,0 +1,55 @@ +# Disallow specific `jest.` methods (`no-restricted-jest-methods`) + +💼 This rule is enabled in the following +[configs](https://github.com/jest-community/eslint-plugin-jest/blob/main/README.md#shareable-configurations): +`all`. + + + +You may wish to restrict the use of specific `jest` methods. + +## Rule details + +This rule checks for the usage of specific methods on the `jest` object, which +can be used to disallow curtain patterns such as spies and mocks. + +## Options + +Restrictions are expressed in the form of a map, with the value being either a +string message to be shown, or `null` if a generic default message should be +used. + +By default, this map is empty, meaning no `jest` methods are banned. + +For example: + +```json +{ + "jest/no-restricted-jest-methods": [ + "error", + { + "advanceTimersByTime": null, + "spyOn": "Don't use spies" + } + ] +} +``` + +Examples of **incorrect** code for this rule with the above configuration + +```js +jest.useFakeTimers(); +it('calls the callback after 1 second via advanceTimersByTime', () => { + // ... + + jest.advanceTimersByTime(1000); + + // ... +}); + +test('plays video', () => { + const spy = jest.spyOn(video, 'play'); + + // ... +}); +``` diff --git a/src/__tests__/__snapshots__/rules.test.ts.snap b/src/__tests__/__snapshots__/rules.test.ts.snap index 34c265f89..bb7d785e7 100644 --- a/src/__tests__/__snapshots__/rules.test.ts.snap +++ b/src/__tests__/__snapshots__/rules.test.ts.snap @@ -30,6 +30,7 @@ exports[`rules should export configs that refer to actual rules 1`] = ` "jest/no-jasmine-globals": "error", "jest/no-large-snapshots": "error", "jest/no-mocks-import": "error", + "jest/no-restricted-jest-methods": "error", "jest/no-restricted-matchers": "error", "jest/no-standalone-expect": "error", "jest/no-test-prefixes": "error", diff --git a/src/__tests__/rules.test.ts b/src/__tests__/rules.test.ts index a8c66cc18..86fdf0a38 100644 --- a/src/__tests__/rules.test.ts +++ b/src/__tests__/rules.test.ts @@ -2,7 +2,7 @@ import { existsSync } from 'fs'; import { resolve } from 'path'; import plugin from '../'; -const numberOfRules = 50; +const numberOfRules = 51; const ruleNames = Object.keys(plugin.rules); const deprecatedRules = Object.entries(plugin.rules) .filter(([, rule]) => rule.meta.deprecated) diff --git a/src/rules/__tests__/no-restricted-jest-methods.test.ts b/src/rules/__tests__/no-restricted-jest-methods.test.ts new file mode 100644 index 000000000..39c4081ca --- /dev/null +++ b/src/rules/__tests__/no-restricted-jest-methods.test.ts @@ -0,0 +1,110 @@ +import { TSESLint } from '@typescript-eslint/utils'; +import dedent from 'dedent'; +import rule from '../no-restricted-jest-methods'; +import { espreeParser } from './test-utils'; + +const ruleTester = new TSESLint.RuleTester({ + parser: espreeParser, + parserOptions: { + ecmaVersion: 2017, + }, +}); + +ruleTester.run('no-restricted-jest-methods', rule, { + valid: [ + 'jest', + 'jest.mock()', + 'expect(a).rejects;', + 'expect(a);', + { + code: dedent` + import { jest } from '@jest/globals'; + + jest; + `, + parserOptions: { sourceType: 'module' }, + }, + ], + invalid: [ + { + code: 'jest.fn()', + options: [{ fn: null }], + errors: [ + { + messageId: 'restrictedJestMethod', + data: { + message: null, + restriction: 'fn', + }, + column: 6, + line: 1, + }, + ], + }, + { + code: 'jest["fn"]()', + options: [{ fn: null }], + errors: [ + { + messageId: 'restrictedJestMethod', + data: { + message: null, + restriction: 'fn', + }, + column: 6, + line: 1, + }, + ], + }, + { + code: 'jest.mock()', + options: [{ mock: 'Do not use mocks' }], + errors: [ + { + messageId: 'restrictedJestMethodWithMessage', + data: { + message: 'Do not use mocks', + restriction: 'mock', + }, + column: 6, + line: 1, + }, + ], + }, + { + code: 'jest["mock"]()', + options: [{ mock: 'Do not use mocks' }], + errors: [ + { + messageId: 'restrictedJestMethodWithMessage', + data: { + message: 'Do not use mocks', + restriction: 'mock', + }, + column: 6, + line: 1, + }, + ], + }, + { + code: dedent` + import { jest } from '@jest/globals'; + + jest.advanceTimersByTime(); + `, + options: [{ advanceTimersByTime: null }], + parserOptions: { sourceType: 'module' }, + errors: [ + { + messageId: 'restrictedJestMethod', + data: { + message: null, + restriction: 'advanceTimersByTime', + }, + column: 6, + line: 3, + }, + ], + }, + ], +}); diff --git a/src/rules/no-restricted-jest-methods.ts b/src/rules/no-restricted-jest-methods.ts new file mode 100644 index 000000000..a2a4a6ded --- /dev/null +++ b/src/rules/no-restricted-jest-methods.ts @@ -0,0 +1,59 @@ +import { createRule, getAccessorValue, parseJestFnCall } from './utils'; + +const messages = { + restrictedJestMethod: 'Use of `{{ restriction }}` is disallowed', + restrictedJestMethodWithMessage: '{{ message }}', +}; + +export default createRule< + [Record], + keyof typeof messages +>({ + name: __filename, + meta: { + docs: { + category: 'Best Practices', + description: 'Disallow specific `jest.` methods', + recommended: false, + }, + type: 'suggestion', + schema: [ + { + type: 'object', + additionalProperties: { + type: ['string', 'null'], + }, + }, + ], + messages, + }, + defaultOptions: [{}], + create(context, [restrictedMethods]) { + return { + CallExpression(node) { + const jestFnCall = parseJestFnCall(node, context); + + if (jestFnCall?.type !== 'jest') { + return; + } + + const method = getAccessorValue(jestFnCall.members[0]); + + if (method in restrictedMethods) { + const message = restrictedMethods[method]; + + context.report({ + messageId: message + ? 'restrictedJestMethodWithMessage' + : 'restrictedJestMethod', + data: { message, restriction: method }, + loc: { + start: jestFnCall.members[0].loc.start, + end: jestFnCall.members[jestFnCall.members.length - 1].loc.end, + }, + }); + } + }, + }; + }, +});