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: add no-confusing-set-time rule #1425

Merged
merged 14 commits into from
Sep 15, 2023
61 changes: 61 additions & 0 deletions docs/rules/no-confusing-set-timeout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Disallow using confusing setTimeout in test (`no-confusing-set-timeout`)

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

This rule will raise a warning about confusing `jest-setTimeout` in test.
G-Rath marked this conversation as resolved.
Show resolved Hide resolved

## 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:
Copy link
Collaborator

Choose a reason for hiding this comment

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

How about this:

Suggested change
This rule describes some tricky ways about `jest.setTimeout` that should not
recommend in Jest:
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`


- 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('A', () => {
G-Rath marked this conversation as resolved.
Show resolved Hide resolved
jest.setTimeout(1000);
it('test-description', () => {
// test logic;
});
});

describe('B', () => {
it('test-description', () => {
jest.setTimeout(1000);
// test logic;
});
});

test('C', () => {
jest.setTimeout(1000);
});

describe('D', () => {
beforeEach(() => {
jest.setTimeout(1000);
});
});
```

Examples of **correct** code for this rule:

```js
jest.setTimeout(500);
test('A', () => {
// do some stuff
});
```

```js
jest.setTimeout(1000);
describe('B', () => {
it('test-description', () => {
// test logic;
});
});
```
1 change: 1 addition & 0 deletions src/__tests__/__snapshots__/rules.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
172 changes: 172 additions & 0 deletions src/rules/__tests__/no-confusing-set-timeout.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
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(); });});
});
`,
{
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' },
},
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 jest = require('@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(); });});
});
`,
],
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: 'topSetTimeout',
line: 9,
column: 1,
},
{
messageId: 'onlyOneSetTimeout',
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,
},
],
},
{
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,
},
],
},
{
code: dedent`
test('test-suite', () => {
jest.setTimeout(1000);
});
`,
errors: [
{
messageId: 'globalSetTimeout',
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: 'topSetTimeout',
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,
},
],
},
],
});
108 changes: 108 additions & 0 deletions src/rules/no-confusing-set-timeout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { AST_NODE_TYPES, type TSESTree } from '@typescript-eslint/utils';
import type { Scope } from '@typescript-eslint/utils/dist/ts-eslint';
import { createRule, getNodeName } from './utils';

interface ErrorType {
messageId: 'globalSetTimeout' | 'onlyOneSetTimeout' | 'topSetTimeout';
node: TSESTree.Node;
}

function isJestSetTimeout(node: TSESTree.Node) {
if (node.type === AST_NODE_TYPES.ExpressionStatement) {
return getNodeName(node.expression) === 'jest.setTimeout';
}

return getNodeName(node) === 'jest.setTimeout';
}

function checkIsGlobal(scope: Scope.Scope) {
// for ESModule Case
if (scope.type === 'module' && scope.upper.type === 'global') return true;
if (scope.type === 'global') return true;

return false;
}

function isTestsuiteFunction(node: TSESTree.Node) {
if (node.type !== AST_NODE_TYPES.ExpressionStatement) return false;

const name = getNodeName(node.expression);

if (!name) return false;

return [
'describe',
'test',
'it',
'beforeEach',
'afterEach',
'before',
'after',
].includes(name);
}

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',
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
description: 'Disallow using confusing setTimeout in test',
description: 'Disallow confusing usages of jest.setTimeout',

recommended: false,
},
messages: {
globalSetTimeout: '`jest.setTimeout` should be call in `global` scope.',
onlyOneSetTimeout: 'Only the last one `jest.setTimeout` call work.',
topSetTimeout:
'The `jest.setTimout` should be placed before all of testsuite methods.',
G-Rath marked this conversation as resolved.
Show resolved Hide resolved
},
type: 'problem',
schema: [],
},
defaultOptions: [],
create(context) {
const errors: ErrorType[] = [];
let jestTimeoutcount = 0;
let nonJestTimeout = 0;

return {
Program(node) {
const { body } = node;

body.forEach((n, index) => {
if (isJestSetTimeout(n)) {
if (nonJestTimeout > 0) {
errors.push({
messageId: 'topSetTimeout',
node: node.body[index],
});
}
} else if (isTestsuiteFunction(n)) {
nonJestTimeout += 1;
}
});
},
MemberExpression(node) {
const scope = context.getScope();

if (!isJestSetTimeout(node)) return;

jestTimeoutcount += 1;

if (!checkIsGlobal(scope)) {
errors.push({ messageId: 'globalSetTimeout', node });
}

if (jestTimeoutcount > 1) {
errors.push({ messageId: 'onlyOneSetTimeout', node });
}
},
'Program:exit'() {
if (errors.length > 0) {
errors.forEach((error: ErrorType) => {
context.report(error);
});
}
},
};
},
});
Loading