Skip to content

feat: add no-node-access rule #190

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

Merged
merged 24 commits into from
Jul 15, 2020
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
b0a1157
Merge remote-tracking branch 'upstream/v4' into no-node-access
thebinaryfelix Jun 28, 2020
9ebf44e
refactor(utils): add properties and methods
thebinaryfelix Jun 28, 2020
4d41edc
test(no-node-access): add first scenarios
thebinaryfelix Jun 28, 2020
9ebf3d2
feat(no-node-access): add rule
thebinaryfelix Jun 28, 2020
cba1f13
test(no-node-access): add scenarios
thebinaryfelix Jun 28, 2020
5092889
refactor(no-node-access): simplify conditions
thebinaryfelix Jun 29, 2020
f63f2b0
refactor(no-node-access): add scenario
thebinaryfelix Jun 29, 2020
c1ecd66
refactor(no-node-access): remove conditional
thebinaryfelix Jun 29, 2020
934bc1d
refactor(utils): add DOM properties
thebinaryfelix Jun 29, 2020
70a366e
refactor(no-node-access): add scenario
thebinaryfelix Jun 29, 2020
291eddf
docs(no-node-access): add readme
thebinaryfelix Jun 29, 2020
e3328ee
refactor(utils): export const
thebinaryfelix Jun 29, 2020
6cc5648
docs(no-node-access): fix file location
thebinaryfelix Jun 29, 2020
af0ed98
docs(readme): add no-node-access
thebinaryfelix Jun 29, 2020
8f545d1
refactor(no-node-access): change highlight location
thebinaryfelix Jun 29, 2020
145a89b
docs(no-node-access): fix typo
thebinaryfelix Jun 29, 2020
bd13e95
refactor(utils): add missing property
thebinaryfelix Jun 29, 2020
f5eacca
refactor(no-node-access): simplify checks
thebinaryfelix Jul 3, 2020
9415c3b
test(no-node-access): add more scenarios
thebinaryfelix Jul 3, 2020
0f1cd36
docs(no-node-access): update examples
thebinaryfelix Jul 3, 2020
ba40a64
Merge branch 'v4' of https://github.com/testing-library/eslint-plugin…
thebinaryfelix Jul 3, 2020
9d4f356
refactor(no-node-access): narrow error cases
thebinaryfelix Jul 5, 2020
ecb12fe
refactor(no-node-access): check imports
thebinaryfelix Jul 10, 2020
eb8d4a3
refactor(no-node-access): rename variable
thebinaryfelix Jul 10, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .all-contributorsrc
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,17 @@
"contributions": [
"bug"
]
},
{
"login": "thebinaryfelix",
"name": "Mateus Felix",
"avatar_url": "https://avatars2.githubusercontent.com/u/4968788?v=4",
"profile": "https://www.linkedin.com/in/mateusfelix/",
"contributions": [
"code",
"test",
"doc"
]
}
],
"contributorsPerLine": 7,
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->

[![All Contributors](https://img.shields.io/badge/all_contributors-26-orange.svg?style=flat-square)](#contributors-)
[![All Contributors](https://img.shields.io/badge/all_contributors-27-orange.svg?style=flat-square)](#contributors-)

<!-- ALL-CONTRIBUTORS-BADGE:END -->

Expand Down Expand Up @@ -136,6 +136,7 @@ To enable this configuration use the `extends` property in your
| [no-debug](docs/rules/no-debug.md) | Disallow the use of `debug` | ![angular-badge][] ![react-badge][] ![vue-badge][] | |
| [no-dom-import](docs/rules/no-dom-import.md) | Disallow importing from DOM Testing Library | ![angular-badge][] ![react-badge][] ![vue-badge][] | ![fixable-badge][] |
| [no-manual-cleanup](docs/rules/no-manual-cleanup.md) | Disallow the use of `cleanup` | | |
| [no-node-access](docs/rules/no-node-access.md) | Disallow direct Node access | ![angular-badge][] ![react-badge][] ![vue-badge][] | | |
| [no-promise-in-fire-event](docs/rules/no-promise-in-fire-event.md) | Disallow the use of promises passed to a `fireEvent` method | | |
| [no-wait-for-empty-callback](docs/rules/no-wait-for-empty-callback.md) | Disallow empty callbacks for `waitFor` and `waitForElementToBeRemoved` | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | |
| [prefer-explicit-assert](docs/rules/prefer-explicit-assert.md) | Suggest using explicit assertions rather than just `getBy*` queries | | |
Expand Down Expand Up @@ -205,6 +206,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<td align="center"><a href="https://github.com/jrparish"><img src="https://avatars3.githubusercontent.com/u/5173987?v=4" width="100px;" alt=""/><br /><sub><b>Jacob Parish</b></sub></a><br /><a href="https://github.com/testing-library/eslint-plugin-testing-library/issues?q=author%3Ajrparish" title="Bug reports">🐛</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=jrparish" title="Code">💻</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=jrparish" title="Tests">⚠️</a></td>
<td align="center"><a href="https://nickmccurdy.com/"><img src="https://avatars0.githubusercontent.com/u/927220?v=4" width="100px;" alt=""/><br /><sub><b>Nick McCurdy</b></sub></a><br /><a href="#ideas-nickmccurdy" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=nickmccurdy" title="Code">💻</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/pulls?q=is%3Apr+reviewed-by%3Anickmccurdy" title="Reviewed Pull Requests">👀</a></td>
<td align="center"><a href="https://stefancameron.com/"><img src="https://avatars3.githubusercontent.com/u/2855350?v=4" width="100px;" alt=""/><br /><sub><b>Stefan Cameron</b></sub></a><br /><a href="https://github.com/testing-library/eslint-plugin-testing-library/issues?q=author%3Astefcameron" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://www.linkedin.com/in/mateusfelix/"><img src="https://avatars2.githubusercontent.com/u/4968788?v=4" width="100px;" alt=""/><br /><sub><b>Mateus Felix</b></sub></a><br /><a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=thebinaryfelix" title="Code">💻</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=thebinaryfelix" title="Tests">⚠️</a> <a href="https://github.com/testing-library/eslint-plugin-testing-library/commits?author=thebinaryfelix" title="Documentation">📖</a></td>
</tr>
</table>

Expand Down
44 changes: 44 additions & 0 deletions docs/rules/no-node-access.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Disallow direct Node access (no-node-access)

The Testing Library already provides methods for querying DOM elements.

## Rule Details

This rule aims to disallow DOM traversal using native HTML methods and properties, such as `closest`, `lastChild` and all that returns another Node element from an HTML tree.

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

```js
screen.getByText('Submit').closest('button'); // chaining with Testing Library methods
```

```js
const buttons = screen.getAllByRole('button');
const buttonA = buttons[1]; // getting the element directly from the array

expect(buttonA.lastChild).toBeInTheDocument();
```

```js
const buttonText = screen.getByText('Submit');
const button = buttonText.closest('button');
```

```js
document.getElementById('submit-btn').closest('button');
```

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

```js
const buttonText = screen.getByRole('button');
expect(buttonText.textContent).toBe('Submit');
```

## Further Reading

### Properties / methods that return another Node

- [`Document`](https://developer.mozilla.org/en-US/docs/Web/API/Document)
- [`Element`](https://developer.mozilla.org/en-US/docs/Web/API/Element)
- [`Node`](https://developer.mozilla.org/en-US/docs/Web/API/Node)
5 changes: 5 additions & 0 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import noContainer from './rules/no-container';
import noDebug from './rules/no-debug';
import noDomImport from './rules/no-dom-import';
import noManualCleanup from './rules/no-manual-cleanup';
import noNodeAccess from './rules/no-node-access';
import noWaitForEmptyCallback from './rules/no-wait-for-empty-callback';
import noPromiseInFireEvent from './rules/no-promise-in-fire-event';
import preferExplicitAssert from './rules/prefer-explicit-assert';
Expand All @@ -25,6 +26,7 @@ const rules = {
'no-debug': noDebug,
'no-dom-import': noDomImport,
'no-manual-cleanup': noManualCleanup,
'no-node-access': noNodeAccess,
'no-promise-in-fire-event': noPromiseInFireEvent,
'no-wait-for-empty-callback': noWaitForEmptyCallback,
'prefer-explicit-assert': preferExplicitAssert,
Expand All @@ -49,13 +51,15 @@ const angularRules = {
'testing-library/no-container': 'error',
'testing-library/no-debug': 'warn',
'testing-library/no-dom-import': ['error', 'angular'],
'testing-library/no-node-access': 'error',
};

const reactRules = {
...domRules,
'testing-library/no-container': 'error',
'testing-library/no-debug': 'warn',
'testing-library/no-dom-import': ['error', 'react'],
'testing-library/no-node-access': 'error',
};

const vueRules = {
Expand All @@ -64,6 +68,7 @@ const vueRules = {
'testing-library/no-container': 'error',
'testing-library/no-debug': 'warn',
'testing-library/no-dom-import': ['error', 'vue'],
'testing-library/no-node-access': 'error',
};

export = {
Expand Down
87 changes: 87 additions & 0 deletions lib/rules/no-node-access.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { ESLintUtils, TSESTree } from '@typescript-eslint/experimental-utils';
import { isIdentifier, isMemberExpression, isLiteral } from '../node-utils';
import { getDocsUrl, ALL_QUERIES_METHODS, ALL_RETURNING_NODES } from '../utils';

export const RULE_NAME = 'no-node-access';

const ALL_QUERIES_AND_RETURNING_NODES = [
...ALL_QUERIES_METHODS,
...ALL_RETURNING_NODES,
];

export default ESLintUtils.RuleCreator(getDocsUrl)({
name: RULE_NAME,
meta: {
type: 'problem',
docs: {
description: 'Disallow direct Node access',
category: 'Best Practices',
recommended: 'error',
},
messages: {
noNodeAccess:
'Avoid direct Node access. Prefer using the methods from Testing Library.',
},
fixable: null,
schema: [],
},
defaultOptions: [],

create(context) {
const variablesWithNodes: string[] = [];

function showErrorForNodeAccess(node: TSESTree.MemberExpression) {
const isLiteralNumber =
isLiteral(node.property) && typeof node.property.value === 'number';
const hasForbiddenMethod =
isIdentifier(node.property) &&
ALL_RETURNING_NODES.includes(node.property.name);

(isLiteralNumber || hasForbiddenMethod) &&
context.report({
node: node,
loc: node.property.loc.start,
messageId: 'noNodeAccess',
});
}

function checkVariablesWithNodes(node: TSESTree.MemberExpression) {
const callExpression = node.parent as TSESTree.CallExpression;
const variableDeclarator = callExpression.parent as TSESTree.VariableDeclarator;
const methodsNames = ALL_QUERIES_AND_RETURNING_NODES.filter(
method =>
isIdentifier(node.property) && node.property.name.includes(method)
);

if (methodsNames.length) {
variablesWithNodes.push(
isIdentifier(variableDeclarator.id) && variableDeclarator.id.name
);
}
}

function checkDirectNodeAccess(node: TSESTree.Identifier) {
if (variablesWithNodes.includes(node.name)) {
isMemberExpression(node.parent) && showErrorForNodeAccess(node.parent);
}
}

function checkDirectMethodCall(node: TSESTree.CallExpression) {
const methodsNames = ALL_QUERIES_AND_RETURNING_NODES.filter(
method =>
isMemberExpression(node.callee) &&
isIdentifier(node.callee.property) &&
node.callee.property.name.includes(method)
);
if (methodsNames.length && isMemberExpression(node.parent)) {
showErrorForNodeAccess(node.parent);
}
}

return {
['VariableDeclarator > CallExpression > MemberExpression']: checkVariablesWithNodes,
['MemberExpression > Identifier']: checkDirectNodeAccess,
['CallExpression']: checkDirectMethodCall,
};
},
});
38 changes: 38 additions & 0 deletions lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,41 @@ const ASYNC_UTILS = [
'waitForDomChange',
];

const PROPERTIES_RETURNING_NODES = [
'activeElement',
'children',
'firstChild',
'firstElementChild',
'fullscreenElement',
'lastChild',
'lastElementChild',
'nextElementSibling',
'nextSibling',
'parentElement',
'parentNode',
'pointerLockElement',
'previousElementSibling',
'previousSibling',
'rootNode',
'scripts',
];

const METHODS_RETURNING_NODES = [
'closest',
'getElementById',
'getElementsByClassName',
'getElementsByName',
'getElementsByTagName',
'getElementsByTagNameNS',
'querySelector',
'querySelectorAll',
];

const ALL_RETURNING_NODES = [
...PROPERTIES_RETURNING_NODES,
...METHODS_RETURNING_NODES,
];

export {
getDocsUrl,
SYNC_QUERIES_VARIANTS,
Expand All @@ -74,4 +109,7 @@ export {
ALL_QUERIES_COMBINATIONS,
ASYNC_UTILS,
LIBRARY_MODULES,
PROPERTIES_RETURNING_NODES,
METHODS_RETURNING_NODES,
ALL_RETURNING_NODES,
};
3 changes: 3 additions & 0 deletions tests/__snapshots__/index.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Object {
"error",
"angular",
],
"testing-library/no-node-access": "error",
"testing-library/no-promise-in-fire-event": "error",
"testing-library/no-wait-for-empty-callback": "error",
"testing-library/prefer-find-by": "error",
Expand Down Expand Up @@ -55,6 +56,7 @@ Object {
"error",
"react",
],
"testing-library/no-node-access": "error",
"testing-library/no-promise-in-fire-event": "error",
"testing-library/no-wait-for-empty-callback": "error",
"testing-library/prefer-find-by": "error",
Expand All @@ -79,6 +81,7 @@ Object {
"error",
"vue",
],
"testing-library/no-node-access": "error",
"testing-library/no-promise-in-fire-event": "error",
"testing-library/no-wait-for-empty-callback": "error",
"testing-library/prefer-find-by": "error",
Expand Down
Loading