Skip to content

Commit 86f9c84

Browse files
authored
feat: add no-container rule #177
2 parents a4cc8d8 + 9d44911 commit 86f9c84

File tree

8 files changed

+336
-34
lines changed

8 files changed

+336
-34
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ To enable this configuration use the `extends` property in your
140140
| [await-fire-event](docs/rules/await-fire-event.md) | Enforce async fire event methods to be awaited | ![vue-badge][] | |
141141
| [consistent-data-testid](docs/rules/consistent-data-testid.md) | Ensure `data-testid` values match a provided regex. | | |
142142
| [no-await-sync-query](docs/rules/no-await-sync-query.md) | Disallow unnecessary `await` for sync queries | ![recommended-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | |
143+
| [no-container](docs/rules/no-container.md) | Disallow the use of `container` methods | ![angular-badge][] ![react-badge][] ![vue-badge][] | |
143144
| [no-debug](docs/rules/no-debug.md) | Disallow the use of `debug` | ![angular-badge][] ![react-badge][] ![vue-badge][] | |
144145
| [no-dom-import](docs/rules/no-dom-import.md) | Disallow importing from DOM Testing Library | ![angular-badge][] ![react-badge][] ![vue-badge][] | ![fixable-badge][] |
145146
| [no-manual-cleanup](docs/rules/no-manual-cleanup.md) | Disallow the use of `cleanup` | | |

docs/rules/no-container.md

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Disallow the use of `container` methods (no-container)
2+
3+
By using `container` methods like `.querySelector` you may lose a lot of the confidence that the user can really interact with your UI. Also, the test becomes harder to read, and it will break more frequently.
4+
5+
This applies to Testing Library frameworks built on top of **DOM Testing Library**
6+
7+
## Rule Details
8+
9+
This rule aims to disallow the use of `container` methods in your tests.
10+
11+
Examples of **incorrect** code for this rule:
12+
13+
```js
14+
const { container } = render(<Example />);
15+
const button = container.querySelector('.btn-primary');
16+
```
17+
18+
```js
19+
const { container: alias } = render(<Example />);
20+
const button = alias.querySelector('.btn-primary');
21+
```
22+
23+
```js
24+
const view = render(<Example />);
25+
const button = view.container.getElementsByClassName('.btn-primary');
26+
```
27+
28+
Examples of **correct** code for this rule:
29+
30+
```js
31+
render(<Example />);
32+
screen.getByRole('button', { name: /click me/i });
33+
```
34+
35+
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.
36+
37+
```
38+
"testing-library/no-container": ["error", {"renderFunctions":["renderWithRedux", "renderWithRouter"]}],
39+
```
40+
41+
## Further Reading
42+
43+
- [about the `container` element](https://testing-library.com/docs/react-testing-library/api#container-1)
44+
- [querying with `screen`](https://testing-library.com/docs/dom-testing-library/api-queries#screen)

lib/index.ts

+5
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import awaitAsyncUtils from './rules/await-async-utils';
33
import awaitFireEvent from './rules/await-fire-event';
44
import consistentDataTestid from './rules/consistent-data-testid';
55
import noAwaitSyncQuery from './rules/no-await-sync-query';
6+
import noContainer from './rules/no-container';
67
import noDebug from './rules/no-debug';
78
import noDomImport from './rules/no-dom-import';
89
import noManualCleanup from './rules/no-manual-cleanup';
@@ -20,6 +21,7 @@ const rules = {
2021
'await-fire-event': awaitFireEvent,
2122
'consistent-data-testid': consistentDataTestid,
2223
'no-await-sync-query': noAwaitSyncQuery,
24+
'no-container': noContainer,
2325
'no-debug': noDebug,
2426
'no-dom-import': noDomImport,
2527
'no-manual-cleanup': noManualCleanup,
@@ -53,6 +55,7 @@ export = {
5355
plugins: ['testing-library'],
5456
rules: {
5557
...recommendedRules,
58+
'testing-library/no-container': 'error',
5659
'testing-library/no-debug': 'warn',
5760
'testing-library/no-dom-import': ['error', 'angular'],
5861
},
@@ -61,6 +64,7 @@ export = {
6164
plugins: ['testing-library'],
6265
rules: {
6366
...recommendedRules,
67+
'testing-library/no-container': 'error',
6468
'testing-library/no-debug': 'warn',
6569
'testing-library/no-dom-import': ['error', 'react'],
6670
},
@@ -70,6 +74,7 @@ export = {
7074
rules: {
7175
...recommendedRules,
7276
'testing-library/await-fire-event': 'error',
77+
'testing-library/no-container': 'error',
7378
'testing-library/no-debug': 'warn',
7479
'testing-library/no-dom-import': ['error', 'vue'],
7580
},

lib/node-utils.ts

+33
Original file line numberDiff line numberDiff line change
@@ -114,3 +114,36 @@ export function isArrowFunctionExpression(
114114
): node is TSESTree.ArrowFunctionExpression {
115115
return node && node.type === 'ArrowFunctionExpression';
116116
}
117+
118+
function isRenderFunction(
119+
callNode: TSESTree.CallExpression,
120+
renderFunctions: string[]
121+
) {
122+
return ['render', ...renderFunctions].some(
123+
name => isIdentifier(callNode.callee) && name === callNode.callee.name
124+
);
125+
}
126+
127+
export function isRenderVariableDeclarator(
128+
node: TSESTree.VariableDeclarator,
129+
renderFunctions: string[]
130+
) {
131+
if (node.init) {
132+
if (isAwaitExpression(node.init)) {
133+
return (
134+
node.init.argument &&
135+
isRenderFunction(
136+
node.init.argument as TSESTree.CallExpression,
137+
renderFunctions
138+
)
139+
);
140+
} else {
141+
return (
142+
isCallExpression(node.init) &&
143+
isRenderFunction(node.init, renderFunctions)
144+
);
145+
}
146+
}
147+
148+
return false;
149+
}

lib/rules/no-container.ts

+120
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import { ESLintUtils, TSESTree } from '@typescript-eslint/experimental-utils';
2+
import { getDocsUrl } from '../utils';
3+
import {
4+
isIdentifier,
5+
isMemberExpression,
6+
isObjectPattern,
7+
isProperty,
8+
isRenderVariableDeclarator,
9+
} from '../node-utils';
10+
11+
export const RULE_NAME = 'no-container';
12+
13+
export default ESLintUtils.RuleCreator(getDocsUrl)({
14+
name: RULE_NAME,
15+
meta: {
16+
type: 'problem',
17+
docs: {
18+
description: 'Disallow the use of container methods',
19+
category: 'Best Practices',
20+
recommended: 'error',
21+
},
22+
messages: {
23+
noContainer:
24+
'Avoid using container methods. Prefer using the methods from Testing Library, such as "getByRole()"',
25+
},
26+
fixable: null,
27+
schema: [
28+
{
29+
type: 'object',
30+
properties: {
31+
renderFunctions: {
32+
type: 'array',
33+
},
34+
},
35+
},
36+
],
37+
},
38+
defaultOptions: [
39+
{
40+
renderFunctions: [],
41+
},
42+
],
43+
44+
create(context, [options]) {
45+
const { renderFunctions } = options;
46+
const destructuredContainerPropNames: string[] = [];
47+
let renderWrapperName: string = null;
48+
let containerName: string = null;
49+
let containerCallsMethod = false;
50+
51+
function showErrorIfChainedContainerMethod(
52+
innerNode: TSESTree.MemberExpression
53+
) {
54+
if (isMemberExpression(innerNode)) {
55+
if (isIdentifier(innerNode.object)) {
56+
const isContainerName = innerNode.object.name === containerName;
57+
const isRenderWrapper = innerNode.object.name === renderWrapperName;
58+
59+
containerCallsMethod =
60+
isIdentifier(innerNode.property) &&
61+
innerNode.property.name === 'container' &&
62+
isRenderWrapper;
63+
64+
if (isContainerName || containerCallsMethod) {
65+
context.report({
66+
node: innerNode,
67+
messageId: 'noContainer',
68+
});
69+
}
70+
}
71+
showErrorIfChainedContainerMethod(
72+
innerNode.object as TSESTree.MemberExpression
73+
);
74+
}
75+
}
76+
77+
return {
78+
VariableDeclarator(node) {
79+
if (isRenderVariableDeclarator(node, renderFunctions)) {
80+
if (isObjectPattern(node.id)) {
81+
const containerIndex = node.id.properties.findIndex(
82+
property =>
83+
isProperty(property) &&
84+
isIdentifier(property.key) &&
85+
property.key.name === 'container'
86+
);
87+
const nodeValue =
88+
containerIndex !== -1 && node.id.properties[containerIndex].value;
89+
if (isIdentifier(nodeValue)) {
90+
containerName = nodeValue.name;
91+
} else {
92+
isObjectPattern(nodeValue) &&
93+
nodeValue.properties.forEach(
94+
property =>
95+
isProperty(property) &&
96+
isIdentifier(property.key) &&
97+
destructuredContainerPropNames.push(property.key.name)
98+
);
99+
}
100+
} else {
101+
renderWrapperName = isIdentifier(node.id) && node.id.name;
102+
}
103+
}
104+
},
105+
106+
CallExpression(node: TSESTree.CallExpression) {
107+
if (isMemberExpression(node.callee)) {
108+
showErrorIfChainedContainerMethod(node.callee);
109+
} else {
110+
isIdentifier(node.callee) &&
111+
destructuredContainerPropNames.includes(node.callee.name) &&
112+
context.report({
113+
node,
114+
messageId: 'noContainer',
115+
});
116+
}
117+
},
118+
};
119+
},
120+
});

lib/rules/no-debug.ts

+1-34
Original file line numberDiff line numberDiff line change
@@ -6,46 +6,13 @@ import {
66
isIdentifier,
77
isCallExpression,
88
isLiteral,
9-
isAwaitExpression,
109
isMemberExpression,
1110
isImportSpecifier,
11+
isRenderVariableDeclarator,
1212
} from '../node-utils';
1313

1414
export const RULE_NAME = 'no-debug';
1515

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-
25-
function isRenderVariableDeclarator(
26-
node: TSESTree.VariableDeclarator,
27-
renderFunctions: string[]
28-
) {
29-
if (node.init) {
30-
if (isAwaitExpression(node.init)) {
31-
return (
32-
node.init.argument &&
33-
isRenderFunction(
34-
node.init.argument as TSESTree.CallExpression,
35-
renderFunctions
36-
)
37-
);
38-
} else {
39-
return (
40-
isCallExpression(node.init) &&
41-
isRenderFunction(node.init, renderFunctions)
42-
);
43-
}
44-
}
45-
46-
return false;
47-
}
48-
4916
function hasTestingLibraryImportModule(
5017
importDeclarationNode: TSESTree.ImportDeclaration
5118
) {

tests/__snapshots__/index.test.ts.snap

+3
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Object {
99
"testing-library/await-async-query": "error",
1010
"testing-library/await-async-utils": "error",
1111
"testing-library/no-await-sync-query": "error",
12+
"testing-library/no-container": "error",
1213
"testing-library/no-debug": "warn",
1314
"testing-library/no-dom-import": Array [
1415
"error",
@@ -31,6 +32,7 @@ Object {
3132
"testing-library/await-async-query": "error",
3233
"testing-library/await-async-utils": "error",
3334
"testing-library/no-await-sync-query": "error",
35+
"testing-library/no-container": "error",
3436
"testing-library/no-debug": "warn",
3537
"testing-library/no-dom-import": Array [
3638
"error",
@@ -71,6 +73,7 @@ Object {
7173
"testing-library/await-async-utils": "error",
7274
"testing-library/await-fire-event": "error",
7375
"testing-library/no-await-sync-query": "error",
76+
"testing-library/no-container": "error",
7477
"testing-library/no-debug": "warn",
7578
"testing-library/no-dom-import": Array [
7679
"error",

0 commit comments

Comments
 (0)