Skip to content

Commit d16d284

Browse files
authoredJul 30, 2022
feat(no-node-access): add allowContainerFirstChild option (#611)
1 parent bface3a commit d16d284

File tree

3 files changed

+102
-33
lines changed

3 files changed

+102
-33
lines changed
 

‎docs/rules/no-node-access.md

+19
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,25 @@ within(signinModal).getByPlaceholderText('Username');
5151
document.getElementById('submit-btn').closest('button');
5252
```
5353

54+
## Options
55+
56+
This rule has one option:
57+
58+
- `allowContainerFirstChild`: **disabled by default**. When we have container
59+
with rendered content then the easiest way to access content itself is [by using
60+
`firstChild` property](https://testing-library.com/docs/react-testing-library/api/#container-1). Use this option in cases when this is hardly avoidable.
61+
62+
```js
63+
"testing-library/no-node-access": ["error", {"allowContainerFirstChild": true}]
64+
```
65+
66+
Correct:
67+
68+
```jsx
69+
const { container } = render(<MyComponent />);
70+
expect(container.firstChild).toMatchSnapshot();
71+
```
72+
5473
## Further Reading
5574

5675
### Properties / methods that return another Node

‎lib/rules/no-node-access.ts

+21-4
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { ALL_RETURNING_NODES } from '../utils';
55

66
export const RULE_NAME = 'no-node-access';
77
export type MessageIds = 'noNodeAccess';
8-
type Options = [];
8+
export type Options = [{ allowContainerFirstChild: boolean }];
99

1010
export default createTestingLibraryRule<Options, MessageIds>({
1111
name: RULE_NAME,
@@ -25,11 +25,24 @@ export default createTestingLibraryRule<Options, MessageIds>({
2525
noNodeAccess:
2626
'Avoid direct Node access. Prefer using the methods from Testing Library.',
2727
},
28-
schema: [],
28+
schema: [
29+
{
30+
type: 'object',
31+
properties: {
32+
allowContainerFirstChild: {
33+
type: 'boolean',
34+
},
35+
},
36+
},
37+
],
2938
},
30-
defaultOptions: [],
39+
defaultOptions: [
40+
{
41+
allowContainerFirstChild: false,
42+
},
43+
],
3144

32-
create(context, _, helpers) {
45+
create(context, [{ allowContainerFirstChild = false }], helpers) {
3346
function showErrorForNodeAccess(node: TSESTree.MemberExpression) {
3447
// This rule is so aggressive that can cause tons of false positives outside test files when Aggressive Reporting
3548
// is enabled. Because of that, this rule will skip this mechanism and report only if some Testing Library package
@@ -42,6 +55,10 @@ export default createTestingLibraryRule<Options, MessageIds>({
4255
ASTUtils.isIdentifier(node.property) &&
4356
ALL_RETURNING_NODES.includes(node.property.name)
4457
) {
58+
if (allowContainerFirstChild && node.property.name === 'firstChild') {
59+
return;
60+
}
61+
4562
context.report({
4663
node,
4764
loc: node.property.loc.start,

‎tests/lib/rules/no-node-access.test.ts

+62-29
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1-
import rule, { RULE_NAME } from '../../../lib/rules/no-node-access';
1+
import type { TSESLint } from '@typescript-eslint/utils';
2+
3+
import rule, { RULE_NAME, Options } from '../../../lib/rules/no-node-access';
24
import { createRuleTester } from '../test-utils';
35

46
const ruleTester = createRuleTester();
57

8+
type ValidTestCase = TSESLint.ValidTestCase<Options>;
9+
610
const SUPPORTED_TESTING_FRAMEWORKS = [
711
'@testing-library/angular',
812
'@testing-library/react',
@@ -11,51 +15,52 @@ const SUPPORTED_TESTING_FRAMEWORKS = [
1115
];
1216

1317
ruleTester.run(RULE_NAME, rule, {
14-
valid: SUPPORTED_TESTING_FRAMEWORKS.flatMap((testingFramework) => [
15-
{
16-
code: `
18+
valid: SUPPORTED_TESTING_FRAMEWORKS.flatMap<ValidTestCase>(
19+
(testingFramework) => [
20+
{
21+
code: `
1722
import { screen } from '${testingFramework}';
1823
1924
const buttonText = screen.getByText('submit');
2025
`,
21-
},
22-
{
23-
code: `
26+
},
27+
{
28+
code: `
2429
import { screen } from '${testingFramework}';
2530
2631
const { getByText } = screen
2732
const firstChild = getByText('submit');
2833
expect(firstChild).toBeInTheDocument()
2934
`,
30-
},
31-
{
32-
code: `
35+
},
36+
{
37+
code: `
3338
import { screen } from '${testingFramework}';
3439
3540
const firstChild = screen.getByText('submit');
3641
expect(firstChild).toBeInTheDocument()
3742
`,
38-
},
39-
{
40-
code: `
43+
},
44+
{
45+
code: `
4146
import { screen } from '${testingFramework}';
4247
4348
const { getByText } = screen;
4449
const button = getByRole('button');
4550
expect(button).toHaveTextContent('submit');
4651
`,
47-
},
48-
{
49-
code: `
52+
},
53+
{
54+
code: `
5055
import { render, within } from '${testingFramework}';
5156
5257
const { getByLabelText } = render(<MyComponent />);
5358
const signInModal = getByLabelText('Sign In');
5459
within(signInModal).getByPlaceholderText('Username');
5560
`,
56-
},
57-
{
58-
code: `
61+
},
62+
{
63+
code: `
5964
// case: code not related to testing library at all
6065
ReactDOM.render(
6166
<CommProvider useDsa={false}>
@@ -70,25 +75,36 @@ ruleTester.run(RULE_NAME, rule, {
7075
document.getElementById('root')
7176
);
7277
`,
73-
},
74-
{
75-
settings: {
76-
'testing-library/utils-module': 'test-utils',
7778
},
78-
code: `
79+
{
80+
settings: {
81+
'testing-library/utils-module': 'test-utils',
82+
},
83+
code: `
7984
// case: custom module set but not imported (aggressive reporting limited)
8085
const closestButton = document.getElementById('submit-btn').closest('button');
8186
expect(closestButton).toBeInTheDocument();
8287
`,
83-
},
84-
{
85-
code: `
88+
},
89+
{
90+
code: `
8691
// case: without importing TL (aggressive reporting skipped)
8792
const closestButton = document.getElementById('submit-btn')
8893
expect(closestButton).toBeInTheDocument();
8994
`,
90-
},
91-
]),
95+
},
96+
{
97+
options: [{ allowContainerFirstChild: true }],
98+
code: `
99+
import { render } from '${testingFramework}';
100+
101+
const { container } = render(<MyComponent />)
102+
103+
expect(container.firstChild).toMatchSnapshot()
104+
`,
105+
},
106+
]
107+
),
92108
invalid: SUPPORTED_TESTING_FRAMEWORKS.flatMap((testingFramework) => [
93109
{
94110
settings: {
@@ -291,5 +307,22 @@ ruleTester.run(RULE_NAME, rule, {
291307
},
292308
],
293309
},
310+
{
311+
code: `
312+
import { render } from '${testingFramework}';
313+
314+
const { container } = render(<MyComponent />)
315+
316+
expect(container.firstChild).toMatchSnapshot()
317+
`,
318+
errors: [
319+
{
320+
// error points to `firstChild`
321+
line: 6,
322+
column: 26,
323+
messageId: 'noNodeAccess',
324+
},
325+
],
326+
},
294327
]),
295328
});

0 commit comments

Comments
 (0)