-
Notifications
You must be signed in to change notification settings - Fork 238
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: add no-confusing-set-time rule #1425
Changes from 9 commits
b0d4ff4
a1eec07
0a98694
7e2ce4b
b21df04
59da5f3
25b2d93
a2e9080
b0338c0
c39b755
40fa046
80f525e
382ddbb
c5e73ee
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,63 @@ | ||||||||||||||||
# Disallow using confusing setTimeout in test (`no-confusing-set-timeout`) | ||||||||||||||||
|
||||||||||||||||
<!-- end auto-generated rule header --> | ||||||||||||||||
|
||||||||||||||||
`jest.setTimeout` can be called multiple times anywhere within a single test | ||||||||||||||||
file. However, only the last call will have an effect, and it will actually be | ||||||||||||||||
invoked before any other jest functions. | ||||||||||||||||
|
||||||||||||||||
## Rule details | ||||||||||||||||
|
||||||||||||||||
This rule describes some tricky ways about `jest.setTimeout` that should not | ||||||||||||||||
SimenB marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||
recommend in Jest: | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How about this:
Suggested change
|
||||||||||||||||
|
||||||||||||||||
- should set `jest.setTimeout` in any testsuite methods before(such as | ||||||||||||||||
SimenB marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||
`describe`, `test` or `it`); | ||||||||||||||||
- should set `jest.setTimeout` in global scope. | ||||||||||||||||
- should only call `jest.setTimeout` once in a single test file; | ||||||||||||||||
|
||||||||||||||||
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; | ||||||||||||||||
}); | ||||||||||||||||
}); | ||||||||||||||||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,242 @@ | ||
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, { | ||
G-Rath marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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, | ||
}, | ||
], | ||
}, | ||
], | ||
}); |
Original file line number | Diff line number | Diff line change | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,66 @@ | ||||||||||||||
import type { TSESTree } from '@typescript-eslint/utils'; | ||||||||||||||
import { createRule, getNodeName, parseJestFnCall } from './utils'; | ||||||||||||||
|
||||||||||||||
function isJestSetTimeout(node: TSESTree.Node) { | ||||||||||||||
return getNodeName(node) === 'jest.setTimeout'; | ||||||||||||||
} | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This check is not correct because it does not account for aliases i.e. this will not get caught:
While a bit contrived, this is why
|
||||||||||||||
|
||||||||||||||
export default createRule({ | ||||||||||||||
G-Rath marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||
name: __filename, | ||||||||||||||
meta: { | ||||||||||||||
docs: { | ||||||||||||||
category: 'Best Practices', | ||||||||||||||
description: 'Disallow using confusing setTimeout in test', | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||
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.', | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Linting messages shouldn't use full stops because they might appear in sentences constructed by IDEs and such:
Suggested change
|
||||||||||||||
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) { | ||||||||||||||
G-Rath marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||
const scope = context.getScope(); | ||||||||||||||
const jestFnCall = parseJestFnCall(node, context); | ||||||||||||||
|
||||||||||||||
if (!jestFnCall) { | ||||||||||||||
return; | ||||||||||||||
} | ||||||||||||||
|
||||||||||||||
const result = isJestSetTimeout(node); | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. let's call this something like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, naming makes my head burn out. |
||||||||||||||
|
||||||||||||||
if (!result) { | ||||||||||||||
if (jestFnCall.type !== 'unknown') { | ||||||||||||||
shouldEmitOrderSetTimeout = true; | ||||||||||||||
} | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. By definition there is no known Jest function call of type
Suggested change
|
||||||||||||||
|
||||||||||||||
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 }); | ||||||||||||||
} else { | ||||||||||||||
seenJestTimeout = result; | ||||||||||||||
} | ||||||||||||||
}, | ||||||||||||||
}; | ||||||||||||||
}, | ||||||||||||||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.