Skip to content

Commit 3fb5364

Browse files
committed
feat(no-render-in-setup): adds no-render-in-setup rule, closes #207
1 parent 6005415 commit 3fb5364

9 files changed

+218
-11
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@
2323
[![Tweet][tweet-badge]][tweet-url]
2424

2525
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
26+
2627
[![All Contributors](https://img.shields.io/badge/all_contributors-29-orange.svg?style=flat-square)](#contributors-)
28+
2729
<!-- ALL-CONTRIBUTORS-BADGE:END -->
2830

2931
## Installation
@@ -141,6 +143,7 @@ To enable this configuration use the `extends` property in your
141143
| [no-debug](docs/rules/no-debug.md) | Disallow the use of `debug` | ![angular-badge][] ![react-badge][] ![vue-badge][] | |
142144
| [no-dom-import](docs/rules/no-dom-import.md) | Disallow importing from DOM Testing Library | ![angular-badge][] ![react-badge][] ![vue-badge][] | ![fixable-badge][] |
143145
| [no-manual-cleanup](docs/rules/no-manual-cleanup.md) | Disallow the use of `cleanup` | | |
146+
| [no-render-in-setup](docs/rules/no-render-in-setup.md) | Disallow the use of `render` in setup functions | | |
144147
| [no-wait-for-empty-callback](docs/rules/no-wait-for-empty-callback.md) | Disallow empty callbacks for `waitFor` and `waitForElementToBeRemoved` | | |
145148
| [prefer-explicit-assert](docs/rules/prefer-explicit-assert.md) | Suggest using explicit assertions rather than just `getBy*` queries | | |
146149
| [prefer-find-by](docs/rules/prefer-find-by.md) | Suggest using `findBy*` methods instead of the `waitFor` + `getBy` queries | ![recommended-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | ![fixable-badge][] |
@@ -219,6 +222,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
219222

220223
<!-- markdownlint-enable -->
221224
<!-- prettier-ignore-end -->
225+
222226
<!-- ALL-CONTRIBUTORS-LIST:END -->
223227

224228
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!

docs/rules/no-render-in-setup.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Disallow the use of `render` in setup functions (no-render-in-setup)
2+
3+
## Rule Details
4+
5+
This rule disallows the usage of `render` in setup functions (`beforeEach` or `beforeAll`) in favor of a single test with multiple assertions.
6+
7+
Examples of **incorrect** code for this rule:
8+
9+
```js
10+
beforeEach(() => {
11+
render(<MyComponent />);
12+
});
13+
14+
it('Should have foo', () => {
15+
expect(screen.getByText('foo')).toBeInTheDocument();
16+
});
17+
18+
it('Should have bar', () => {
19+
expect(screen.getByText('bar')).toBeInTheDocument();
20+
});
21+
22+
it('Should have baz', () => {
23+
expect(screen.getByText('baz')).toBeInTheDocument();
24+
});
25+
```
26+
27+
Examples of **correct** code for this rule:
28+
29+
```js
30+
it('Should have foo, bar and baz', () => {
31+
render(<MyComponent />);
32+
expect(screen.getByText('foo')).toBeInTheDocument();
33+
expect(screen.getByText('bar')).toBeInTheDocument();
34+
expect(screen.getByText('baz')).toBeInTheDocument();
35+
});
36+
```
37+
38+
If you use [custom render functions](https://testing-library.com/docs/example-react-redux) then you can set a config option in your `.eslintrc` to look for these.
39+
40+
```
41+
"testing-library/no-render-in-setup": ["error", {"renderFunctions":["renderWithRedux", "renderWithRouter"]}],
42+
```

lib/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import noAwaitSyncQuery from './rules/no-await-sync-query';
66
import noDebug from './rules/no-debug';
77
import noDomImport from './rules/no-dom-import';
88
import noManualCleanup from './rules/no-manual-cleanup';
9+
import noRenderInSetup from './rules/no-render-in-setup';
910
import noWaitForEmptyCallback from './rules/no-wait-for-empty-callback';
1011
import preferExplicitAssert from './rules/prefer-explicit-assert';
1112
import preferPresenceQueries from './rules/prefer-presence-queries';
@@ -22,6 +23,7 @@ const rules = {
2223
'no-debug': noDebug,
2324
'no-dom-import': noDomImport,
2425
'no-manual-cleanup': noManualCleanup,
26+
'no-render-in-setup': noRenderInSetup,
2527
'no-wait-for-empty-callback': noWaitForEmptyCallback,
2628
'prefer-explicit-assert': preferExplicitAssert,
2729
'prefer-find-by': preferFindBy,

lib/node-utils.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,15 @@ export function isVariableDeclarator(
5050
return node && node.type === AST_NODE_TYPES.VariableDeclarator;
5151
}
5252

53+
export function isRenderFunction(
54+
callNode: TSESTree.CallExpression,
55+
renderFunctions: string[]
56+
) {
57+
return ['render', ...renderFunctions].some(
58+
name => isIdentifier(callNode.callee) && name === callNode.callee.name
59+
);
60+
}
61+
5362
export function isObjectPattern(
5463
node: TSESTree.Node
5564
): node is TSESTree.ObjectPattern {

lib/rules/no-debug.ts

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,11 @@ import {
99
isAwaitExpression,
1010
isMemberExpression,
1111
isImportSpecifier,
12+
isRenderFunction,
1213
} from '../node-utils';
1314

1415
export const RULE_NAME = 'no-debug';
1516

16-
function isRenderFunction(
17-
callNode: TSESTree.CallExpression,
18-
renderFunctions: string[]
19-
) {
20-
return ['render', ...renderFunctions].some(
21-
name => isIdentifier(callNode.callee) && name === callNode.callee.name
22-
);
23-
}
24-
2517
function isRenderVariableDeclarator(
2618
node: TSESTree.VariableDeclarator,
2719
renderFunctions: string[]

lib/rules/no-manual-cleanup.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)<Options, MessageIds>({
2121
meta: {
2222
type: 'problem',
2323
docs: {
24-
description: ' Disallow the use of `cleanup`',
24+
description: 'Disallow the use of `cleanup`',
2525
category: 'Best Practices',
2626
recommended: false,
2727
},
@@ -121,7 +121,6 @@ export default ESLintUtils.RuleCreator(getDocsUrl)<Options, MessageIds>({
121121
messageId: 'noManualCleanup',
122122
});
123123
}
124-
125124
} else {
126125
defaultRequireFromTestingLibrary = declaratorNode.id;
127126
}

lib/rules/no-render-in-setup.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { ESLintUtils, TSESTree } from '@typescript-eslint/experimental-utils';
2+
import { getDocsUrl, BEFORE_HOOKS } from '../utils';
3+
import {
4+
isIdentifier,
5+
isCallExpression,
6+
isRenderFunction,
7+
} from '../node-utils';
8+
9+
export const RULE_NAME = 'no-render-in-setup';
10+
export type MessageIds = 'noRenderInSetup';
11+
12+
export function findClosestBeforeHook(
13+
node: TSESTree.Node
14+
): TSESTree.Identifier | null {
15+
if (node === null) return null;
16+
if (
17+
isCallExpression(node) &&
18+
isIdentifier(node.callee) &&
19+
BEFORE_HOOKS.includes(node.callee.name)
20+
) {
21+
return node.callee;
22+
}
23+
24+
return findClosestBeforeHook(node.parent);
25+
}
26+
27+
export default ESLintUtils.RuleCreator(getDocsUrl)({
28+
name: RULE_NAME,
29+
meta: {
30+
type: 'problem',
31+
docs: {
32+
description: 'Disallow the use of `render` in setup functions',
33+
category: 'Best Practices',
34+
recommended: false,
35+
},
36+
messages: {
37+
noRenderInSetup:
38+
'Combine assertions into a single test instead of re-rendering the component.',
39+
},
40+
fixable: null,
41+
schema: [
42+
{
43+
type: 'object',
44+
properties: {
45+
renderFunctions: {
46+
type: 'array',
47+
},
48+
},
49+
},
50+
],
51+
},
52+
defaultOptions: [
53+
{
54+
renderFunctions: [],
55+
},
56+
],
57+
58+
create(context, [{ renderFunctions }]) {
59+
return {
60+
CallExpression(node) {
61+
const beforeHook = findClosestBeforeHook(node);
62+
if (isRenderFunction(node, renderFunctions) && beforeHook) {
63+
context.report({
64+
node,
65+
messageId: 'noRenderInSetup',
66+
data: {
67+
name: beforeHook.name,
68+
},
69+
});
70+
}
71+
},
72+
};
73+
},
74+
});

lib/utils.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ const ASYNC_UTILS = [
6363
'waitForDomChange',
6464
];
6565

66+
const BEFORE_HOOKS = ['beforeEach', 'beforeAll'];
67+
6668
export {
6769
getDocsUrl,
6870
SYNC_QUERIES_VARIANTS,
@@ -73,5 +75,6 @@ export {
7375
ASYNC_QUERIES_COMBINATIONS,
7476
ALL_QUERIES_COMBINATIONS,
7577
ASYNC_UTILS,
78+
BEFORE_HOOKS,
7679
LIBRARY_MODULES,
7780
};
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { createRuleTester } from '../test-utils';
2+
import { BEFORE_HOOKS } from '../../../lib/utils';
3+
import rule, { RULE_NAME } from '../../../lib/rules/no-render-in-setup';
4+
5+
const ruleTester = createRuleTester({
6+
ecmaFeatures: {
7+
jsx: true,
8+
},
9+
});
10+
11+
ruleTester.run(RULE_NAME, rule, {
12+
valid: [
13+
{
14+
code: `
15+
it('Test', () => {
16+
render(<Component/>)
17+
})
18+
`,
19+
},
20+
],
21+
22+
invalid: [
23+
...BEFORE_HOOKS.map(beforeHook => ({
24+
code: `
25+
${beforeHook}(() => {
26+
render(<Component/>)
27+
})
28+
`,
29+
errors: [
30+
{
31+
messageId: 'noRenderInSetup',
32+
},
33+
],
34+
})),
35+
...BEFORE_HOOKS.map(beforeHook => ({
36+
code: `
37+
${beforeHook}(function() {
38+
render(<Component/>)
39+
})
40+
`,
41+
errors: [
42+
{
43+
messageId: 'noRenderInSetup',
44+
},
45+
],
46+
})),
47+
// custom render function
48+
...BEFORE_HOOKS.map(beforeHook => ({
49+
code: `
50+
${beforeHook}(() => {
51+
renderWithRedux(<Component/>)
52+
})
53+
`,
54+
options: [
55+
{
56+
renderFunctions: ['renderWithRedux'],
57+
},
58+
],
59+
errors: [
60+
{
61+
messageId: 'noRenderInSetup',
62+
},
63+
],
64+
})),
65+
// call render within a wrapper function
66+
...BEFORE_HOOKS.map(beforeHook => ({
67+
code: `
68+
${beforeHook}(() => {
69+
const wrapper = () => {
70+
render(<Component/>)
71+
}
72+
wrapper();
73+
})
74+
`,
75+
errors: [
76+
{
77+
messageId: 'noRenderInSetup',
78+
},
79+
],
80+
})),
81+
],
82+
});

0 commit comments

Comments
 (0)