Skip to content

Commit 260f849

Browse files
emmenkoBelco90
authored andcommitted
feat(rule): prefer expect queryBy (#22)
* feat(rule): prefer expect queryBy * chore: regenerate lockfile * refactor: improve docs and tests, include also findBy * refactor: remove occurrences of findBy * refactor: generate test cases for all query methods * refactor: disable autofix implementation * refactor: remove fixable badge * refactor: fixable option to null
1 parent b0c7ace commit 260f849

File tree

7 files changed

+236
-8
lines changed

7 files changed

+236
-8
lines changed

README.md

+8-7
Original file line numberDiff line numberDiff line change
@@ -128,13 +128,14 @@ To enable this configuration use the `extends` property in your
128128

129129
## Supported Rules
130130

131-
| Rule | Description | Configurations | Fixable |
132-
| -------------------------------------------------------- | ---------------------------------------------- | ------------------------------------------------------------------------- | ------------------ |
133-
| [await-async-query](docs/rules/await-async-query.md) | Enforce async queries to have proper `await` | ![recommended-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | |
134-
| [await-fire-event](docs/rules/await-fire-event.md) | Enforce async fire event methods to be awaited | ![vue-badge][] | |
135-
| [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][] | |
136-
| [no-debug](docs/rules/no-debug.md) | Disallow the use of `debug` | ![angular-badge][] ![react-badge][] ![vue-badge][] | |
137-
| [no-dom-import](docs/rules/no-dom-import.md) | Disallow importing from DOM Testing Library | ![angular-badge][] ![react-badge][] ![vue-badge][] | ![fixable-badge][] |
131+
| Rule | Description | Configurations | Fixable |
132+
| -------------------------------------------------------------- | ---------------------------------------------- | ------------------------------------------------------------------------- | ------------------ |
133+
| [await-async-query](docs/rules/await-async-query.md) | Enforce async queries to have proper `await` | ![recommended-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | |
134+
| [await-fire-event](docs/rules/await-fire-event.md) | Enforce async fire event methods to be awaited | ![vue-badge][] | |
135+
| [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][] | |
136+
| [no-debug](docs/rules/no-debug.md) | Disallow the use of `debug` | ![angular-badge][] ![react-badge][] ![vue-badge][] | |
137+
| [no-dom-import](docs/rules/no-dom-import.md) | Disallow importing from DOM Testing Library | ![angular-badge][] ![react-badge][] ![vue-badge][] | ![fixable-badge][] |
138+
| [prefer-expect-query-by](docs/rules/prefer-expect-query-by.md) | Disallow the use of `expect(getBy*)` | ![recommended-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | |
138139

139140
[build-badge]: https://img.shields.io/travis/Belco90/eslint-plugin-testing-library?style=flat-square
140141
[build-url]: https://travis-ci.org/belco90/eslint-plugin-testing-library

docs/rules/prefer-expect-query-by.md

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Disallow the use of `expect(getBy*)` (prefer-expect-query-by)
2+
3+
The (DOM) Testing Library support three types of queries: `getBy*` and `queryBy*`. Using `getBy*` throws an error in case the element is not found. This is useful when using method like `waitForElement`, which are `async` functions that will wait for the element to be found until a certain timeout, after that the test will fail.
4+
However, when trying to assert if an element is not in the document, we can't use `getBy*` as the test will fail immediately. Instead it is recommended to use `queryBy*`, which does not throw and therefore we can assert that e.g. `expect(queryByText("Foo")).not.toBeInTheDocument()`.
5+
6+
> The same applies for the `getAll*` and `queryAll*` queries.
7+
8+
## Rule details
9+
10+
This rule gives a notification whenever `expect` is used with one of the query functions that throw an error if the element is not found.
11+
12+
This rule is enabled by default.
13+
14+
Examples of **incorrect** code for this rule:
15+
16+
```js
17+
test('some test', () => {
18+
const { getByText, getAllByText } = render(<App />);
19+
expect(getByText('Foo')).toBeInTheDocument();
20+
expect(getAllByText('Foo')[0]).toBeInTheDocument();
21+
expect(getByText('Foo')).not.toBeInTheDocument();
22+
expect(getAllByText('Foo')[0]).not.toBeInTheDocument();
23+
});
24+
```
25+
26+
```js
27+
test('some test', () => {
28+
const rendered = render(<App />);
29+
expect(rendered.getByText('Foo')).toBeInTheDocument();
30+
expect(rendered.getAllByText('Foo')[0]).toBeInTheDocument();
31+
expect(rendered.getByText('Foo')).not.toBeInTheDocument();
32+
expect(rendered.getAllByText('Foo')[0]).not.toBeInTheDocument();
33+
});
34+
```
35+
36+
Examples of **correct** code for this rule:
37+
38+
```js
39+
test('some test', () => {
40+
const { queryByText, queryAllByText } = render(<App />);
41+
expect(queryByText('Foo')).toBeInTheDocument();
42+
expect(queryAllByText('Foo')[0]).toBeInTheDocument();
43+
expect(queryByText('Foo')).not.toBeInTheDocument();
44+
expect(queryAllByText('Foo')[0]).not.toBeInTheDocument();
45+
});
46+
```
47+
48+
```js
49+
test('some test', () => {
50+
const rendered = render(<App />);
51+
expect(rendered.queryByText('Foo')).toBeInTheDocument();
52+
expect(rendered.queryAllByText('Foo')[0]).toBeInTheDocument();
53+
expect(rendered.queryByText('Foo')).not.toBeInTheDocument();
54+
expect(rendered.queryAllByText('Foo')[0]).not.toBeInTheDocument();
55+
});
56+
```
57+
58+
## Further Reading
59+
60+
- [Appearance and Disappearance](https://testing-library.com/docs/guide-disappearance#asserting-elements-are-not-present)
61+
- [Testing Library queries cheatsheet](https://testing-library.com/docs/dom-testing-library/cheatsheet#queries)

lib/index.js

+2
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@ const rules = {
66
'no-await-sync-query': require('./rules/no-await-sync-query'),
77
'no-debug': require('./rules/no-debug'),
88
'no-dom-import': require('./rules/no-dom-import'),
9+
'prefer-expect-query-by': require('./rules/prefer-expect-query-by'),
910
};
1011

1112
const recommendedRules = {
1213
'testing-library/await-async-query': 'error',
1314
'testing-library/no-await-sync-query': 'error',
15+
'testing-library/prefer-expect-query-by': 'error',
1416
};
1517

1618
module.exports = {

lib/rules/prefer-expect-query-by.js

+102
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
'use strict';
2+
3+
const { getDocsUrl } = require('../utils');
4+
5+
const AST_NODE_TYPES = {
6+
Identifier: 'Identifier',
7+
MemberExpression: 'MemberExpression',
8+
};
9+
10+
function isIdentifier(node) {
11+
return node.type === AST_NODE_TYPES.Identifier;
12+
}
13+
14+
function isMemberExpression(node) {
15+
return node.type === AST_NODE_TYPES.MemberExpression;
16+
}
17+
18+
function isUsingWrongQueries(node) {
19+
return node.name.startsWith('getBy') || node.name.startsWith('getAllBy');
20+
}
21+
22+
function isNotNullOrUndefined(input) {
23+
return input != null;
24+
}
25+
26+
function mapNodesForWrongGetByQuery(node) {
27+
const nodeArguments = node.arguments;
28+
return nodeArguments
29+
.map(arg => {
30+
if (!arg.callee) {
31+
return null;
32+
}
33+
// Example: `expect(rendered.getBy*)`
34+
if (isMemberExpression(arg.callee)) {
35+
const node = arg.callee.property;
36+
if (isIdentifier(node) && isUsingWrongQueries(node)) {
37+
return node;
38+
}
39+
return null;
40+
}
41+
42+
// Example: `expect(getBy*)`
43+
if (isIdentifier(arg.callee) && isUsingWrongQueries(arg.callee)) {
44+
return arg.callee;
45+
}
46+
47+
return null;
48+
})
49+
.filter(isNotNullOrUndefined);
50+
}
51+
52+
function hasExpectWithWrongGetByQuery(node) {
53+
if (
54+
node.callee &&
55+
node.callee.type === AST_NODE_TYPES.Identifier &&
56+
node.callee.name === 'expect' &&
57+
node.arguments
58+
) {
59+
const nodesGetBy = mapNodesForWrongGetByQuery(node);
60+
return nodesGetBy.length > 0;
61+
}
62+
return false;
63+
}
64+
65+
module.exports = {
66+
meta: {
67+
docs: {
68+
category: 'Best Practices',
69+
description: 'Disallow using getBy* queries in expect calls',
70+
recommended: 'error',
71+
url: getDocsUrl('prefer-expect-query-by'),
72+
},
73+
messages: {
74+
expectQueryBy:
75+
'Using `expect(getBy*)` is not recommended, use `expect(queryBy*)` instead.',
76+
},
77+
schema: [],
78+
type: 'suggestion',
79+
fixable: null,
80+
},
81+
82+
create: context => ({
83+
CallExpression(node) {
84+
if (hasExpectWithWrongGetByQuery(node)) {
85+
// const nodesGetBy = mapNodesForWrongGetByQuery(node);
86+
context.report({
87+
node: node.callee,
88+
messageId: 'expectQueryBy',
89+
// TODO: we keep the autofixing disabled for now, until we figure out
90+
// a better way to amend for the edge cases.
91+
// See also the related discussion: https://github.com/Belco90/eslint-plugin-testing-library/pull/22#discussion_r335394402
92+
// fix(fixer) {
93+
// return fixer.replaceText(
94+
// nodesGetBy[0],
95+
// nodesGetBy[0].name.replace(/^(get(All)?(.*))$/, 'query$2$3')
96+
// );
97+
// },
98+
});
99+
}
100+
},
101+
}),
102+
};

package-lock.json

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/__snapshots__/index.test.js.snap

+4
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Object {
1313
"error",
1414
"angular",
1515
],
16+
"testing-library/prefer-expect-query-by": "error",
1617
},
1718
}
1819
`;
@@ -30,6 +31,7 @@ Object {
3031
"error",
3132
"react",
3233
],
34+
"testing-library/prefer-expect-query-by": "error",
3335
},
3436
}
3537
`;
@@ -42,6 +44,7 @@ Object {
4244
"rules": Object {
4345
"testing-library/await-async-query": "error",
4446
"testing-library/no-await-sync-query": "error",
47+
"testing-library/prefer-expect-query-by": "error",
4548
},
4649
}
4750
`;
@@ -60,6 +63,7 @@ Object {
6063
"error",
6164
"vue",
6265
],
66+
"testing-library/prefer-expect-query-by": "error",
6367
},
6468
}
6569
`;
+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
'use strict';
2+
3+
const RuleTester = require('eslint').RuleTester;
4+
const rule = require('../../../lib/rules/prefer-expect-query-by');
5+
const { ALL_QUERIES_METHODS } = require('../../../lib/utils');
6+
7+
const ruleTester = new RuleTester({
8+
parserOptions: { ecmaVersion: 2015, sourceType: 'module' },
9+
});
10+
11+
const queryByVariants = ALL_QUERIES_METHODS.reduce(
12+
(variants, method) => [
13+
...variants,
14+
...[`query${method}`, `queryAll${method}`],
15+
],
16+
[]
17+
);
18+
const getByVariants = ALL_QUERIES_METHODS.reduce(
19+
(variants, method) => [...variants, ...[`get${method}`, `getAll${method}`]],
20+
[]
21+
);
22+
23+
ruleTester.run('prefer-expect-query-by', rule, {
24+
valid: queryByVariants.reduce(
25+
(validRules, queryName) => [
26+
...validRules,
27+
{ code: `expect(${queryName}('Hello')).toBeInTheDocument()` },
28+
{ code: `expect(rendered.${queryName}('Hello')).toBeInTheDocument()` },
29+
{ code: `expect(${queryName}('Hello')).not.toBeInTheDocument()` },
30+
{
31+
code: `expect(rendered.${queryName}('Hello')).not.toBeInTheDocument()`,
32+
},
33+
],
34+
[]
35+
),
36+
invalid: getByVariants.reduce(
37+
(invalidRules, queryName) => [
38+
...invalidRules,
39+
{
40+
code: `expect(${queryName}('Hello')).toBeInTheDocument()`,
41+
errors: [{ messageId: 'expectQueryBy' }],
42+
},
43+
{
44+
code: `expect(rendered.${queryName}('Hello')).toBeInTheDocument()`,
45+
errors: [{ messageId: 'expectQueryBy' }],
46+
},
47+
{
48+
code: `expect(${queryName}('Hello')).not.toBeInTheDocument()`,
49+
errors: [{ messageId: 'expectQueryBy' }],
50+
},
51+
{
52+
code: `expect(rendered.${queryName}('Hello')).not.toBeInTheDocument()`,
53+
errors: [{ messageId: 'expectQueryBy' }],
54+
},
55+
],
56+
[]
57+
),
58+
});

0 commit comments

Comments
 (0)