Skip to content

Commit 1c40d1a

Browse files
thomaslombartSimenB
authored andcommitted
feat(rules): add require-top-level-describe rule (#407)
Closes #401
1 parent adcf82e commit 1c40d1a

File tree

5 files changed

+208
-41
lines changed

5 files changed

+208
-41
lines changed

README.md

Lines changed: 42 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -104,46 +104,47 @@ installations requiring long-term consistency.
104104

105105
## Rules
106106

107-
| Rule | Description | Recommended | Fixable |
108-
| ---------------------------- | ----------------------------------------------------------------- | ---------------- | ------------------- |
109-
| [consistent-test-it][] | Enforce consistent test or it keyword | | ![fixable-green][] |
110-
| [expect-expect][] | Enforce assertion to be made in a test body | | |
111-
| [lowercase-name][] | Disallow capitalized test names | | ![fixable-green][] |
112-
| [no-alias-methods][] | Disallow alias methods | ![recommended][] | ![fixable-green][] |
113-
| [no-commented-out-tests][] | Disallow commented out tests | | |
114-
| [no-disabled-tests][] | Disallow disabled tests | ![recommended][] | |
115-
| [no-duplicate-hooks][] | Disallow duplicate hooks within a `describe` block | | |
116-
| [no-empty-title][] | Disallow empty titles | | |
117-
| [no-expect-resolves][] | Disallow using `expect().resolves` | | |
118-
| [no-export][] | Disallow export from test files | | |
119-
| [no-focused-tests][] | Disallow focused tests | ![recommended][] | |
120-
| [no-hooks][] | Disallow setup and teardown hooks | | |
121-
| [no-identical-title][] | Disallow identical titles | ![recommended][] | |
122-
| [no-if][] | Disallow conditional logic | | |
123-
| [no-jasmine-globals][] | Disallow Jasmine globals | ![recommended][] | ![fixable-yellow][] |
124-
| [no-jest-import][] | Disallow importing `jest` | ![recommended][] | |
125-
| [no-large-snapshots][] | Disallow large snapshots | | |
126-
| [no-mocks-import][] | Disallow manually importing from `__mocks__` | | |
127-
| [no-standalone-expect][] | Prevents `expect` statements outside of a `test` or `it` block | | |
128-
| [no-test-callback][] | Using a callback in asynchronous tests | | ![fixable-green][] |
129-
| [no-test-prefixes][] | Disallow using `f` & `x` prefixes to define focused/skipped tests | ![recommended][] | ![fixable-green][] |
130-
| [no-test-return-statement][] | Disallow explicitly returning from tests | | |
131-
| [no-truthy-falsy][] | Disallow using `toBeTruthy()` & `toBeFalsy()` | | |
132-
| [no-try-expect][] | Prevent `catch` assertions in tests | | |
133-
| [prefer-called-with][] | Suggest using `toBeCalledWith()` OR `toHaveBeenCalledWith()` | | |
134-
| [prefer-expect-assertions][] | Suggest using `expect.assertions()` OR `expect.hasAssertions()` | | |
135-
| [prefer-inline-snapshots][] | Suggest using `toMatchInlineSnapshot()` | | ![fixable-green][] |
136-
| [prefer-spy-on][] | Suggest using `jest.spyOn()` | | ![fixable-green][] |
137-
| [prefer-strict-equal][] | Suggest using `toStrictEqual()` | | ![fixable-green][] |
138-
| [prefer-to-be-null][] | Suggest using `toBeNull()` | | ![fixable-green][] |
139-
| [prefer-to-be-undefined][] | Suggest using `toBeUndefined()` | | ![fixable-green][] |
140-
| [prefer-to-contain][] | Suggest using `toContain()` | | ![fixable-green][] |
141-
| [prefer-to-have-length][] | Suggest using `toHaveLength()` | | ![fixable-green][] |
142-
| [prefer-todo][] | Suggest using `test.todo()` | | ![fixable-green][] |
143-
| [require-tothrow-message][] | Require that `toThrow()` and `toThrowError` includes a message | | |
144-
| [valid-describe][] | Enforce valid `describe()` callback | ![recommended][] | |
145-
| [valid-expect-in-promise][] | Enforce having return statement when testing with promises | ![recommended][] | |
146-
| [valid-expect][] | Enforce valid `expect()` usage | ![recommended][] | |
107+
| Rule | Description | Recommended | Fixable |
108+
| ------------------------------ | ----------------------------------------------------------------- | ---------------- | ------------------- |
109+
| [consistent-test-it][] | Enforce consistent test or it keyword | | ![fixable-green][] |
110+
| [expect-expect][] | Enforce assertion to be made in a test body | | |
111+
| [lowercase-name][] | Disallow capitalized test names | | ![fixable-green][] |
112+
| [no-alias-methods][] | Disallow alias methods | ![recommended][] | ![fixable-green][] |
113+
| [no-commented-out-tests][] | Disallow commented out tests | | |
114+
| [no-disabled-tests][] | Disallow disabled tests | ![recommended][] | |
115+
| [no-duplicate-hooks][] | Disallow duplicate hooks within a `describe` block | | |
116+
| [no-empty-title][] | Disallow empty titles | | |
117+
| [no-expect-resolves][] | Disallow using `expect().resolves` | | |
118+
| [no-export][] | Disallow export from test files | | |
119+
| [no-focused-tests][] | Disallow focused tests | ![recommended][] | |
120+
| [no-hooks][] | Disallow setup and teardown hooks | | |
121+
| [no-identical-title][] | Disallow identical titles | ![recommended][] | |
122+
| [no-if][] | Disallow conditional logic | | |
123+
| [no-jasmine-globals][] | Disallow Jasmine globals | ![recommended][] | ![fixable-yellow][] |
124+
| [no-jest-import][] | Disallow importing `jest` | ![recommended][] | |
125+
| [no-large-snapshots][] | Disallow large snapshots | | |
126+
| [no-mocks-import][] | Disallow manually importing from `__mocks__` | | |
127+
| [no-standalone-expect][] | Prevents `expect` statements outside of a `test` or `it` block | | |
128+
| [no-test-callback][] | Using a callback in asynchronous tests | | ![fixable-green][] |
129+
| [no-test-prefixes][] | Disallow using `f` & `x` prefixes to define focused/skipped tests | ![recommended][] | ![fixable-green][] |
130+
| [no-test-return-statement][] | Disallow explicitly returning from tests | | |
131+
| [no-truthy-falsy][] | Disallow using `toBeTruthy()` & `toBeFalsy()` | | |
132+
| [no-try-expect][] | Prevent `catch` assertions in tests | | |
133+
| [prefer-called-with][] | Suggest using `toBeCalledWith()` OR `toHaveBeenCalledWith()` | | |
134+
| [prefer-expect-assertions][] | Suggest using `expect.assertions()` OR `expect.hasAssertions()` | | |
135+
| [prefer-inline-snapshots][] | Suggest using `toMatchInlineSnapshot()` | | ![fixable-green][] |
136+
| [prefer-spy-on][] | Suggest using `jest.spyOn()` | | ![fixable-green][] |
137+
| [prefer-strict-equal][] | Suggest using `toStrictEqual()` | | ![fixable-green][] |
138+
| [prefer-to-be-null][] | Suggest using `toBeNull()` | | ![fixable-green][] |
139+
| [prefer-to-be-undefined][] | Suggest using `toBeUndefined()` | | ![fixable-green][] |
140+
| [prefer-to-contain][] | Suggest using `toContain()` | | ![fixable-green][] |
141+
| [prefer-to-have-length][] | Suggest using `toHaveLength()` | | ![fixable-green][] |
142+
| [prefer-todo][] | Suggest using `test.todo()` | | ![fixable-green][] |
143+
| [require-top-level-describe][] | Require a top-level `describe` block | | |
144+
| [require-tothrow-message][] | Require that `toThrow()` and `toThrowError` includes a message | | |
145+
| [valid-describe][] | Enforce valid `describe()` callback | ![recommended][] | |
146+
| [valid-expect-in-promise][] | Enforce having return statement when testing with promises | ![recommended][] | |
147+
| [valid-expect][] | Enforce valid `expect()` usage | ![recommended][] | |
147148

148149
## Credit
149150

@@ -193,6 +194,7 @@ https://github.com/dangreenisrael/eslint-plugin-jest-formatting
193194
[prefer-to-contain]: docs/rules/prefer-to-contain.md
194195
[prefer-to-have-length]: docs/rules/prefer-to-have-length.md
195196
[prefer-todo]: docs/rules/prefer-todo.md
197+
[require-top-level-describe]: docs/rules/require-top-level-describe.md
196198
[require-tothrow-message]: docs/rules/require-tothrow-message.md
197199
[valid-describe]: docs/rules/valid-describe.md
198200
[valid-expect-in-promise]: docs/rules/valid-expect-in-promise.md
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Require top-level describe block (require-top-level-describe)
2+
3+
Jest allows you to organise your test files the way you want it. However, the
4+
more your codebase grows, the more it becomes hard to navigate in your test
5+
files. This rule makes sure that you provide at least a top-level describe block
6+
in your test file.
7+
8+
## Rule Details
9+
10+
This rule triggers a warning if a test case (`test` and `it`) or a hook
11+
(`beforeAll`, `beforeEach`, `afterEach`, `afterAll`) is not located in a
12+
top-level describe block.
13+
14+
The following patterns are considered warnings:
15+
16+
```js
17+
// Above a describe block
18+
test('my test', () => {});
19+
describe('test suite', () => {
20+
it('test', () => {});
21+
});
22+
23+
// Below a describe block
24+
describe('test suite', () => {});
25+
test('my test', () => {});
26+
27+
// Same for hooks
28+
beforeAll('my beforeAll', () => {});
29+
describe('test suite', () => {});
30+
afterEach('my afterEach', () => {});
31+
```
32+
33+
The following patterns are **not** considered warnings:
34+
35+
```js
36+
// In a describe block
37+
describe('test suite', () => {
38+
test('my test', () => {});
39+
});
40+
41+
// In a nested describe block
42+
describe('test suite', () => {
43+
test('my test', () => {});
44+
describe('another test suite', () => {
45+
test('my other test', () => {});
46+
});
47+
});
48+
```
49+
50+
## When Not To Use It
51+
52+
Don't use this rule on non-jest test files.

src/__tests__/rules.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { resolve } from 'path';
33
import plugin from '../';
44

55
const ruleNames = Object.keys(plugin.rules);
6-
const numberOfRules = 38;
6+
const numberOfRules = 39;
77

88
describe('rules', () => {
99
it('should have a corresponding doc for each rule', () => {
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { TSESLint } from '@typescript-eslint/experimental-utils';
2+
import rule from '../require-top-level-describe';
3+
4+
const ruleTester = new TSESLint.RuleTester({
5+
parserOptions: {
6+
ecmaVersion: 2015,
7+
},
8+
});
9+
10+
ruleTester.run('no-standalone-hook', rule, {
11+
valid: [
12+
'describe("test suite", () => { test("my test") });',
13+
'describe("test suite", () => { it("my test") });',
14+
`
15+
describe("test suite", () => {
16+
beforeEach("a", () => {});
17+
describe("b", () => {});
18+
test("c", () => {})
19+
});
20+
`,
21+
'describe("test suite", () => { beforeAll("my beforeAll") });',
22+
'describe("test suite", () => { afterEach("my afterEach") });',
23+
'describe("test suite", () => { afterAll("my afterAll") });',
24+
`
25+
describe("test suite", () => {
26+
it("my test", () => {})
27+
describe("another test suite", () => {
28+
});
29+
test("my other test", () => {})
30+
});
31+
`,
32+
'foo()',
33+
],
34+
invalid: [
35+
{
36+
code: 'beforeEach("my test", () => {})',
37+
errors: [{ messageId: 'unexpectedHook' }],
38+
},
39+
{
40+
code: `
41+
test("my test", () => {})
42+
describe("test suite", () => {});
43+
`,
44+
errors: [{ messageId: 'unexpectedTestCase' }],
45+
},
46+
{
47+
code: `
48+
test("my test", () => {})
49+
describe("test suite", () => {
50+
it("test", () => {})
51+
});
52+
`,
53+
errors: [{ messageId: 'unexpectedTestCase' }],
54+
},
55+
{
56+
code: `
57+
describe("test suite", () => {});
58+
afterAll("my test", () => {})
59+
`,
60+
errors: [{ messageId: 'unexpectedHook' }],
61+
},
62+
],
63+
});
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { TSESTree } from '@typescript-eslint/experimental-utils';
2+
3+
import { createRule, isDescribe, isHook, isTestCase } from './utils';
4+
5+
export default createRule({
6+
name: __filename,
7+
meta: {
8+
docs: {
9+
category: 'Best Practices',
10+
description:
11+
'Prevents test cases and hooks to be outside of a describe block',
12+
recommended: false,
13+
},
14+
messages: {
15+
unexpectedTestCase: 'All test cases must be wrapped in a describe block.',
16+
unexpectedHook: 'All hooks must be wrapped in a describe block.',
17+
},
18+
type: 'suggestion',
19+
schema: [],
20+
},
21+
defaultOptions: [],
22+
create(context) {
23+
let numberOfDescribeBlocks = 0;
24+
return {
25+
CallExpression(node) {
26+
if (isDescribe(node)) {
27+
numberOfDescribeBlocks++;
28+
return;
29+
}
30+
31+
if (numberOfDescribeBlocks === 0) {
32+
if (isTestCase(node)) {
33+
context.report({ node, messageId: 'unexpectedTestCase' });
34+
return;
35+
}
36+
37+
if (isHook(node)) {
38+
context.report({ node, messageId: 'unexpectedHook' });
39+
return;
40+
}
41+
}
42+
},
43+
'CallExpression:exit'(node: TSESTree.CallExpression) {
44+
if (isDescribe(node)) {
45+
numberOfDescribeBlocks--;
46+
}
47+
},
48+
};
49+
},
50+
});

0 commit comments

Comments
 (0)