diff --git a/README.md b/README.md index 871f058e7..e9622e747 100644 --- a/README.md +++ b/README.md @@ -220,6 +220,7 @@ set to warn in.\ | [no-commented-out-tests](docs/rules/no-commented-out-tests.md) | Disallow commented out tests | | ✅ | | | | | [no-conditional-expect](docs/rules/no-conditional-expect.md) | Disallow calling `expect` conditionally | ✅ | | | | | | [no-conditional-in-test](docs/rules/no-conditional-in-test.md) | Disallow conditional logic in tests | | | | | | +| [no-confusing-set-timeout](docs/rules/no-confusing-set-timeout.md) | Disallow confusing usages of jest.setTimeout | | | | | | | [no-deprecated-functions](docs/rules/no-deprecated-functions.md) | Disallow use of deprecated functions | ✅ | | 🔧 | | | | [no-disabled-tests](docs/rules/no-disabled-tests.md) | Disallow disabled tests | | ✅ | | | | | [no-done-callback](docs/rules/no-done-callback.md) | Disallow using a callback in asynchronous tests and hooks | ✅ | | | 💡 | | diff --git a/docs/rules/no-confusing-set-timeout.md b/docs/rules/no-confusing-set-timeout.md new file mode 100644 index 000000000..936201ca2 --- /dev/null +++ b/docs/rules/no-confusing-set-timeout.md @@ -0,0 +1,62 @@ +# Disallow confusing usages of jest.setTimeout (`no-confusing-set-timeout`) + + + +While `jest.setTimeout` can be called multiple times anywhere within a single +test file only the last call before any test functions run will have an effect. + +## Rule details + +this rule checks for several confusing usages of `jest.setTimeout` that looks +like it applies to specific tests within the same file, such as: + +- being called anywhere other than in global scope +- being called multiple times +- being called after other Jest functions like hooks, `describe`, `test`, or + `it` + +Examples of **incorrect** code for this rule: + +```js +describe('test foo', () => { + jest.setTimeout(1000); + it('test-description', () => { + // test logic; + }); +}); + +describe('test bar', () => { + it('test-description', () => { + jest.setTimeout(1000); + // test logic; + }); +}); + +test('foo-bar', () => { + jest.setTimeout(1000); +}); + +describe('unit test', () => { + beforeEach(() => { + jest.setTimeout(1000); + }); +}); +``` + +Examples of **correct** code for this rule: + +```js +jest.setTimeout(500); +test('test test', () => { + // do some stuff +}); +``` + +```js +jest.setTimeout(1000); +describe('test bar bar', () => { + it('test-description', () => { + // test logic; + }); +}); +``` diff --git a/src/__tests__/__snapshots__/rules.test.ts.snap b/src/__tests__/__snapshots__/rules.test.ts.snap index 2e5f25df7..7d2de014b 100644 --- a/src/__tests__/__snapshots__/rules.test.ts.snap +++ b/src/__tests__/__snapshots__/rules.test.ts.snap @@ -18,6 +18,7 @@ exports[`rules should export configs that refer to actual rules 1`] = ` "jest/no-commented-out-tests": "error", "jest/no-conditional-expect": "error", "jest/no-conditional-in-test": "error", + "jest/no-confusing-set-timeout": "error", "jest/no-deprecated-functions": "error", "jest/no-disabled-tests": "error", "jest/no-done-callback": "error", diff --git a/src/__tests__/rules.test.ts b/src/__tests__/rules.test.ts index fa080af37..2948c2414 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 = 52; +const numberOfRules = 53; const ruleNames = Object.keys(plugin.rules); const deprecatedRules = Object.entries(plugin.rules) .filter(([, rule]) => rule.meta.deprecated) diff --git a/src/rules/__tests__/no-confusing-set-timeout.test.ts b/src/rules/__tests__/no-confusing-set-timeout.test.ts new file mode 100644 index 000000000..c7369d1ac --- /dev/null +++ b/src/rules/__tests__/no-confusing-set-timeout.test.ts @@ -0,0 +1,258 @@ +import { TSESLint } from '@typescript-eslint/utils'; +import dedent from 'dedent'; +import rule from '../no-confusing-set-timeout'; +import { espreeParser } from './test-utils'; + +const ruleTester = new TSESLint.RuleTester({ + parser: espreeParser, + parserOptions: { + ecmaVersion: 2020, + }, +}); + +ruleTester.run('no-confusing-set-timeout', rule, { + valid: [ + dedent` + jest.setTimeout(1000); + describe('A', () => { + beforeEach(async () => { await new Promise(resolve => { setTimeout(resolve, 10000).unref(); });}); + it('A.1', async () => { await new Promise(resolve => { setTimeout(resolve, 10000).unref(); });}); + it('A.2', async () => { await new Promise(resolve => { setTimeout(resolve, 10000).unref(); });}); + }); + `, + dedent` + jest.setTimeout(1000); + window.setTimeout(6000) + describe('A', () => { + beforeEach(async () => { await new Promise(resolve => { setTimeout(resolve, 10000).unref(); });}); + it('test foo', async () => { await new Promise(resolve => { setTimeout(resolve, 10000).unref(); });}); + }); + `, + { + code: dedent` + import { handler } from 'dep/mod'; + jest.setTimeout(800); + describe('A', () => { + beforeEach(async () => { await new Promise(resolve => { setTimeout(resolve, 10000).unref(); });}); + it('A.1', async () => { await new Promise(resolve => { setTimeout(resolve, 10000).unref(); });}); + it('A.2', async () => { await new Promise(resolve => { setTimeout(resolve, 10000).unref(); });}); + }); + `, + parserOptions: { sourceType: 'module' }, + }, + dedent` + function handler() {} + jest.setTimeout(800); + describe('A', () => { + beforeEach(async () => { await new Promise(resolve => { setTimeout(resolve, 10000).unref(); });}); + it('A.1', async () => { await new Promise(resolve => { setTimeout(resolve, 10000).unref(); });}); + it('A.2', async () => { await new Promise(resolve => { setTimeout(resolve, 10000).unref(); });}); + }); + `, + dedent` + const { handler } = require('dep/mod'); + jest.setTimeout(800); + describe('A', () => { + beforeEach(async () => { await new Promise(resolve => { setTimeout(resolve, 10000).unref(); });}); + it('A.1', async () => { await new Promise(resolve => { setTimeout(resolve, 10000).unref(); });}); + it('A.2', async () => { await new Promise(resolve => { setTimeout(resolve, 10000).unref(); });}); + }); + `, + dedent` + jest.setTimeout(1000); + window.setTimeout(60000); + `, + 'window.setTimeout(60000);', + 'setTimeout(1000);', + dedent` + jest.setTimeout(1000); + test('test case', () => { + setTimeout(() => { + Promise.resolv(); + }, 5000); + }); + `, + dedent` + test('test case', () => { + setTimeout(() => { + Promise.resolv(); + }, 5000); + }); + `, + ], + invalid: [ + { + code: dedent` + jest.setTimeout(1000); + setTimeout(1000); + window.setTimeout(1000); + describe('A', () => { + beforeEach(async () => { await new Promise(resolve => { setTimeout(resolve, 10000).unref(); });}); + it('A.1', async () => { await new Promise(resolve => { setTimeout(resolve, 10000).unref(); });}); + it('A.2', async () => { await new Promise(resolve => { setTimeout(resolve, 10000).unref(); });}); + }); + jest.setTimeout(800); + `, + errors: [ + { + messageId: 'orderSetTimeout', + line: 9, + column: 1, + }, + { + messageId: 'multipleSetTimeouts', + line: 9, + column: 1, + }, + ], + }, + { + code: dedent` + describe('A', () => { + jest.setTimeout(800); + beforeEach(async () => { await new Promise(resolve => { setTimeout(resolve, 10000).unref(); });}); + it('A.1', async () => { await new Promise(resolve => { setTimeout(resolve, 10000).unref(); });}); + it('A.2', async () => { await new Promise(resolve => { setTimeout(resolve, 10000).unref(); });}); + }); + `, + errors: [ + { + messageId: 'globalSetTimeout', + line: 2, + column: 3, + }, + { + messageId: 'orderSetTimeout', + line: 2, + column: 3, + }, + ], + }, + { + code: dedent` + describe('B', () => { + it('B.1', async () => { + await new Promise((resolve) => { + jest.setTimeout(1000); + setTimeout(resolve, 10000).unref(); + }); + }); + it('B.2', async () => { + await new Promise((resolve) => { setTimeout(resolve, 10000).unref(); }); + }); + }); + `, + errors: [ + { + messageId: 'globalSetTimeout', + line: 4, + column: 7, + }, + { + messageId: 'orderSetTimeout', + line: 4, + column: 7, + }, + ], + }, + { + code: dedent` + test('test-suite', () => { + jest.setTimeout(1000); + }); + `, + errors: [ + { + messageId: 'globalSetTimeout', + line: 2, + column: 3, + }, + { + messageId: 'orderSetTimeout', + line: 2, + column: 3, + }, + ], + }, + { + code: dedent` + describe('A', () => { + beforeEach(async () => { await new Promise(resolve => { setTimeout(resolve, 10000).unref(); });}); + it('A.1', async () => { await new Promise(resolve => { setTimeout(resolve, 10000).unref(); });}); + it('A.2', async () => { await new Promise(resolve => { setTimeout(resolve, 10000).unref(); });}); + }); + jest.setTimeout(1000); + `, + errors: [ + { + messageId: 'orderSetTimeout', + line: 6, + column: 1, + }, + ], + }, + { + code: dedent` + import { jest } from '@jest/globals'; + { + jest.setTimeout(800); + } + describe('A', () => { + beforeEach(async () => { await new Promise(resolve => { setTimeout(resolve, 10000).unref(); });}); + it('A.1', async () => { await new Promise(resolve => { setTimeout(resolve, 10000).unref(); });}); + it('A.2', async () => { await new Promise(resolve => { setTimeout(resolve, 10000).unref(); });}); + }); + `, + parserOptions: { sourceType: 'module' }, + errors: [ + { + messageId: 'globalSetTimeout', + line: 3, + column: 3, + }, + ], + }, + { + code: dedent` + jest.setTimeout(800); + jest.setTimeout(900); + `, + errors: [ + { + messageId: 'multipleSetTimeouts', + line: 2, + column: 1, + }, + ], + }, + { + code: dedent` + expect(1 + 2).toEqual(3); + jest.setTimeout(800); + `, + errors: [ + { + messageId: 'orderSetTimeout', + line: 2, + column: 1, + }, + ], + }, + { + code: dedent` + import { jest as Jest } from '@jest/globals'; + { + Jest.setTimeout(800); + } + `, + parserOptions: { sourceType: 'module' }, + errors: [ + { + messageId: 'globalSetTimeout', + line: 3, + column: 3, + }, + ], + }, + ], +}); diff --git a/src/rules/no-confusing-set-timeout.ts b/src/rules/no-confusing-set-timeout.ts new file mode 100644 index 000000000..c7cf42589 --- /dev/null +++ b/src/rules/no-confusing-set-timeout.ts @@ -0,0 +1,70 @@ +import { + type ParsedJestFnCall, + createRule, + isIdentifier, + parseJestFnCall, +} from './utils'; + +function isJestSetTimeout(jestFnCall: ParsedJestFnCall) { + return ( + jestFnCall.type === 'jest' && + jestFnCall.members.length === 1 && + isIdentifier(jestFnCall.members[0], 'setTimeout') + ); +} + +export default createRule({ + name: __filename, + meta: { + docs: { + category: 'Best Practices', + description: 'Disallow confusing usages of jest.setTimeout', + recommended: false, + }, + messages: { + globalSetTimeout: '`jest.setTimeout` should be call in `global` scope', + multipleSetTimeouts: + 'Do not call `jest.setTimeout` multiple times, as only the last call will have an effect', + orderSetTimeout: + '`jest.setTimeout` should be placed before any other jest methods', + }, + type: 'problem', + schema: [], + }, + defaultOptions: [], + create(context) { + let seenJestTimeout = false; + let shouldEmitOrderSetTimeout = false; + + return { + CallExpression(node) { + const scope = context.getScope(); + const jestFnCall = parseJestFnCall(node, context); + + if (!jestFnCall) { + return; + } + + if (!isJestSetTimeout(jestFnCall)) { + shouldEmitOrderSetTimeout = true; + + return; + } + + if (!['global', 'module'].includes(scope.type)) { + context.report({ messageId: 'globalSetTimeout', node }); + } + + if (shouldEmitOrderSetTimeout) { + context.report({ messageId: 'orderSetTimeout', node }); + } + + if (seenJestTimeout) { + context.report({ messageId: 'multipleSetTimeouts', node }); + } + + seenJestTimeout = true; + }, + }; + }, +});