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 21 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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ To enable this configuration use the `extends` property in your
| [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-multiple-assertions-wait-for](docs/rules/no-multiple-assertions-wait-for.md) | Disallow the use of multiple expect inside `waitFor` | | |
| [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
10 changes: 5 additions & 5 deletions docs/rules/no-multiple-assertions-wait-for.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ const foo = async () => {

// or
await waitFor(function() {
expect(a).toEqual('a')
expect(a).toEqual('a');
expect(b).toEqual('b');
})
});
};
```

Expand All @@ -30,11 +30,11 @@ Examples of **correct** code for this rule:
const foo = async () => {
await waitFor(() => expect(a).toEqual('a'));
expect(b).toEqual('b');

// or
await waitFor(function() {
expect(a).toEqual('a')
})
expect(a).toEqual('a');
});
expect(b).toEqual('b');

// it only detects expect
Expand Down
42 changes: 42 additions & 0 deletions docs/rules/no-node-access.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# 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');
expect(buttons[1].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 button = screen.getByRole('button');
expect(button).toHaveTextContent('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 @@ -27,6 +28,7 @@ const rules = {
'no-dom-import': noDomImport,
'no-manual-cleanup': noManualCleanup,
'no-multiple-assertions-wait-for': noMultipleAssertionsWaitFor,
'no-node-access': noNodeAccess,
'no-promise-in-fire-event': noPromiseInFireEvent,
'no-wait-for-empty-callback': noWaitForEmptyCallback,
'prefer-explicit-assert': preferExplicitAssert,
Expand All @@ -51,13 +53,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 @@ -66,6 +70,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
41 changes: 41 additions & 0 deletions lib/rules/no-node-access.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { ESLintUtils, TSESTree } from '@typescript-eslint/experimental-utils';
import { getDocsUrl, ALL_RETURNING_NODES } from '../utils';
import { isIdentifier } from '../node-utils';

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

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) {
function showErrorForNodeAccess(node: TSESTree.Identifier) {
isIdentifier(node) &&
ALL_RETURNING_NODES.includes(node.name) &&
context.report({
node: node,
loc: node.loc.start,
messageId: 'noNodeAccess',
});
}

return {
['ExpressionStatement MemberExpression Identifier']: showErrorForNodeAccess,
['VariableDeclarator Identifier']: showErrorForNodeAccess,
};
},
});
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