Skip to content

Commit 020f181

Browse files
committed
feat(prefer-explicit-assert): adding new rule
1 parent 8b0b9cc commit 020f181

File tree

5 files changed

+727
-1
lines changed

5 files changed

+727
-1
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,7 @@ module.exports = {
228228
| [no-wait-for-snapshot](docs/rules/no-wait-for-snapshot.md) | Ensures no snapshot is generated inside of a `waitFor` call | ![badge-angular][] ![badge-dom][] ![badge-marko][] ![badge-react][] ![badge-vue][] | | |
229229
| [prefer-explicit-assert](docs/rules/prefer-explicit-assert.md) | Suggest using explicit assertions rather than standalone queries | | | |
230230
| [prefer-find-by](docs/rules/prefer-find-by.md) | Suggest using `find(All)By*` query instead of `waitFor` + `get(All)By*` to wait for elements | ![badge-angular][] ![badge-dom][] ![badge-marko][] ![badge-react][] ![badge-vue][] | | 🔧 |
231+
| [prefer-implicit-assert](docs/rules/prefer-implicit-assert.md) | Suggest using implicit assertions for getBy* & findBy* queries | | | |
231232
| [prefer-presence-queries](docs/rules/prefer-presence-queries.md) | Ensure appropriate `get*`/`query*` queries are used with their respective matchers | ![badge-angular][] ![badge-dom][] ![badge-marko][] ![badge-react][] ![badge-vue][] | | |
232233
| [prefer-query-by-disappearance](docs/rules/prefer-query-by-disappearance.md) | Suggest using `queryBy*` queries when waiting for disappearance | ![badge-angular][] ![badge-dom][] ![badge-marko][] ![badge-react][] ![badge-vue][] | | |
233234
| [prefer-query-matchers](docs/rules/prefer-query-matchers.md) | Ensure the configured `get*`/`query*` query is used with the corresponding matchers | | | |

docs/rules/prefer-explicit-assert.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ This is how you can use these options in eslint configuration:
7272

7373
## When Not To Use It
7474

75-
If you prefer to use `getBy*` queries implicitly as an assert-like method itself, then this rule is not recommended.
75+
If you prefer to use `getBy*` queries implicitly as an assert-like method itself, then this rule is not recommended. Instead check out this rule [prefer-implicit-assert](https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/prefer-implicit-assert.md)
7676

7777
## Further Reading
7878

docs/rules/prefer-implicit-assert.md

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Suggest using implicit assertions for getBy* & findBy* queries (`testing-library/prefer-implicit-assert`)
2+
3+
<!-- end auto-generated rule header -->
4+
5+
Testing Library `getBy*` & `findBy*` queries throw an error if the element is not
6+
found. Therefore it is not necessary to also assert existance with things like `expect(getBy*.toBeInTheDocument()` or `expect(awaint findBy*).not.toBeNull()`
7+
8+
## Rule Details
9+
10+
This rule aims to reuduce uncecessary assertion's for presense of an element,
11+
when using queries that implicitly fail when said element is not found.
12+
13+
Examples of **incorrect** code for this rule with the default configuration:
14+
15+
```js
16+
// wrapping the getBy or findBy queries within a `expect` and using existence matchers for
17+
// making the assertion is not necessary
18+
expect(getByText('foo')).toBeInTheDocument();
19+
expect(await findByText('foo')).toBeInTheDocument();
20+
21+
expect(getByText('foo')).toBeDefined();
22+
expect(await findByText('foo')).toBeDefined();
23+
24+
const utils = render(<Component />);
25+
expect(utils.getByText('foo')).toBeInTheDocument();
26+
expect(await utils.findByText('foo')).toBeInTheDocument();
27+
28+
expect(await findByText('foo')).not.toBeNull();
29+
expect(await findByText('foo')).not.toBeUndified();
30+
```
31+
32+
Examples of **correct** code for this rule with the default configuration:
33+
34+
```js
35+
getByText('foo');
36+
await findByText('foo');
37+
38+
const utils = render(<Component />);
39+
utils.getByText('foo');
40+
await utils.findByText('foo');
41+
42+
// When using queryBy* queries thees do not implicitly fial therefore you should explicitly check if your elements eixst or not
43+
expect(queryByText('foo')).toBeInTheDocument();
44+
expect(queryByText('foo')).not.toBeInTheDocument();
45+
```
46+
47+
## When Not To Use It
48+
49+
If you prefer to use `getBy*` & `findBy*` queries with explicitly asserting existance of elements, then this rule is not recommended. Instead check out this rule [prefer-explicit-assert](https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/prefer-explicit-assert.md)
50+
51+
## Further Reading
52+
53+
- [getBy query](https://testing-library.com/docs/dom-testing-library/api-queries#getby)

lib/rules/prefer-implicit-assert.ts

+189
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
import { TSESTree, ASTUtils, AST_NODE_TYPES } from '@typescript-eslint/utils';
2+
3+
import { createTestingLibraryRule } from '../create-testing-library-rule';
4+
import { isCallExpression, isMemberExpression } from '../node-utils';
5+
import { PRESENCE_MATCHERS, ABSENCE_MATCHERS } from '../utils';
6+
7+
export const RULE_NAME = 'prefer-implicit-assert';
8+
export type MessageIds = 'preferImplicitAssert';
9+
type Options = [];
10+
11+
const isCalledUsingSomeObject = (node: TSESTree.Identifier) =>
12+
isMemberExpression(node.parent) &&
13+
node.parent.object.type === AST_NODE_TYPES.Identifier;
14+
15+
const isCalledInExpect = (
16+
node: TSESTree.Identifier | TSESTree.Node,
17+
isAsyncQuery: boolean
18+
) => {
19+
if (isAsyncQuery) {
20+
return (
21+
isCallExpression(node.parent) &&
22+
ASTUtils.isAwaitExpression(node.parent.parent) &&
23+
isCallExpression(node.parent.parent.parent) &&
24+
ASTUtils.isIdentifier(node.parent.parent.parent.callee) &&
25+
node.parent.parent.parent.callee.name === 'expect'
26+
);
27+
}
28+
return (
29+
isCallExpression(node.parent) &&
30+
isCallExpression(node.parent.parent) &&
31+
ASTUtils.isIdentifier(node.parent.parent.callee) &&
32+
node.parent.parent.callee.name === 'expect'
33+
);
34+
};
35+
36+
const usesPresenceAssertion = (
37+
node: TSESTree.Identifier | TSESTree.Node,
38+
isAsyncQuery: boolean
39+
) => {
40+
if (isAsyncQuery) {
41+
return (
42+
isMemberExpression(node.parent?.parent?.parent?.parent) &&
43+
node.parent?.parent?.parent?.parent.property.type ===
44+
AST_NODE_TYPES.Identifier &&
45+
PRESENCE_MATCHERS.includes(node.parent.parent.parent.parent.property.name)
46+
);
47+
}
48+
return (
49+
isMemberExpression(node.parent?.parent?.parent) &&
50+
node.parent?.parent?.parent.property.type === AST_NODE_TYPES.Identifier &&
51+
PRESENCE_MATCHERS.includes(node.parent.parent.parent.property.name)
52+
);
53+
};
54+
55+
const usesNotPresenceAssertion = (
56+
node: TSESTree.Identifier | TSESTree.Node,
57+
isAsyncQuery: boolean
58+
) => {
59+
if (isAsyncQuery) {
60+
return (
61+
isMemberExpression(node.parent?.parent?.parent?.parent) &&
62+
node.parent?.parent?.parent?.parent.property.type ===
63+
AST_NODE_TYPES.Identifier &&
64+
node.parent.parent.parent.parent.property.name === 'not' &&
65+
isMemberExpression(node.parent.parent.parent.parent.parent) &&
66+
node.parent.parent.parent.parent.parent.property.type ===
67+
AST_NODE_TYPES.Identifier &&
68+
ABSENCE_MATCHERS.includes(
69+
node.parent.parent.parent.parent.parent.property.name
70+
)
71+
);
72+
}
73+
return (
74+
isMemberExpression(node.parent?.parent?.parent) &&
75+
node.parent?.parent?.parent.property.type === AST_NODE_TYPES.Identifier &&
76+
node.parent.parent.parent.property.name === 'not' &&
77+
isMemberExpression(node.parent.parent.parent.parent) &&
78+
node.parent.parent.parent.parent.property.type ===
79+
AST_NODE_TYPES.Identifier &&
80+
ABSENCE_MATCHERS.includes(node.parent.parent.parent.parent.property.name)
81+
);
82+
};
83+
84+
export default createTestingLibraryRule<Options, MessageIds>({
85+
name: RULE_NAME,
86+
meta: {
87+
type: 'suggestion',
88+
docs: {
89+
description:
90+
'Suggest using implicit assertions for getBy* & findBy* queries',
91+
recommendedConfig: {
92+
dom: false,
93+
angular: false,
94+
react: false,
95+
vue: false,
96+
marko: false,
97+
},
98+
},
99+
messages: {
100+
preferImplicitAssert:
101+
"Don't wrap `{{queryType}}` query with `expect` & presence matchers like `toBeInTheDocument` or `not.toBeNull` as `{{queryType}}` queries fail implicitly when element is not found",
102+
},
103+
schema: [],
104+
},
105+
defaultOptions: [],
106+
create(context, _, helpers) {
107+
const findQueryCalls: TSESTree.Identifier[] = [];
108+
const getQueryCalls: TSESTree.Identifier[] = [];
109+
110+
return {
111+
'CallExpression Identifier'(node: TSESTree.Identifier) {
112+
if (helpers.isFindQueryVariant(node)) {
113+
findQueryCalls.push(node);
114+
}
115+
if (helpers.isGetQueryVariant(node)) {
116+
getQueryCalls.push(node);
117+
}
118+
},
119+
'Program:exit'() {
120+
let isAsyncQuery = true;
121+
findQueryCalls.forEach((queryCall) => {
122+
const node: TSESTree.Identifier | TSESTree.Node | undefined =
123+
isCalledUsingSomeObject(queryCall) ? queryCall.parent : queryCall;
124+
125+
if (node) {
126+
if (
127+
isCalledInExpect(node, isAsyncQuery) &&
128+
usesPresenceAssertion(node, isAsyncQuery)
129+
) {
130+
return context.report({
131+
node: queryCall,
132+
messageId: 'preferImplicitAssert',
133+
data: {
134+
queryType: 'findBy*',
135+
},
136+
});
137+
}
138+
139+
if (
140+
isCalledInExpect(node, isAsyncQuery) &&
141+
usesNotPresenceAssertion(node, isAsyncQuery)
142+
) {
143+
return context.report({
144+
node: queryCall,
145+
messageId: 'preferImplicitAssert',
146+
data: {
147+
queryType: 'findBy*',
148+
},
149+
});
150+
}
151+
}
152+
});
153+
154+
getQueryCalls.forEach((queryCall) => {
155+
isAsyncQuery = false;
156+
const node: TSESTree.Identifier | TSESTree.Node | undefined =
157+
isCalledUsingSomeObject(queryCall) ? queryCall.parent : queryCall;
158+
if (node) {
159+
if (
160+
isCalledInExpect(node, isAsyncQuery) &&
161+
usesPresenceAssertion(node, isAsyncQuery)
162+
) {
163+
return context.report({
164+
node: queryCall,
165+
messageId: 'preferImplicitAssert',
166+
data: {
167+
queryType: 'getBy*',
168+
},
169+
});
170+
}
171+
172+
if (
173+
isCalledInExpect(node, isAsyncQuery) &&
174+
usesNotPresenceAssertion(node, isAsyncQuery)
175+
) {
176+
return context.report({
177+
node: queryCall,
178+
messageId: 'preferImplicitAssert',
179+
data: {
180+
queryType: 'getBy*',
181+
},
182+
});
183+
}
184+
}
185+
});
186+
},
187+
};
188+
},
189+
});

0 commit comments

Comments
 (0)