Skip to content

Commit f78720d

Browse files
authored
fix(prefer-explicit-assert): only enforce assertion when presence/absence matcher (#238)
Relates to #218
1 parent 6ce0140 commit f78720d

File tree

5 files changed

+82
-13
lines changed

5 files changed

+82
-13
lines changed

docs/rules/prefer-explicit-assert.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,11 @@ This rule has a few options:
5656
with `getBy*` queries. By default, any assertion is valid (`toBeTruthy`,
5757
`toBeDefined`, etc.). However, they all assert slightly different things.
5858
This option ensures all `getBy*` assertions are consistent and use the same
59-
assertion.
59+
assertion. This rule only allows defining a presence matcher
60+
(`toBeInTheDocument`, `toBeTruthy`, or `toBeDefined`), but checks for both
61+
presence and absence matchers (`not.toBeFalsy` and `not.toBeNull`). This means
62+
other assertions such as `toHaveValue` or `toBeDisabled` will not trigger this
63+
rule since these are valid uses with `getBy*`.
6064

6165
```js
6266
"testing-library/prefer-explicit-assert": ["error", {"assertion": "toBeInTheDocument"}],

lib/rules/prefer-explicit-assert.ts

+31-7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
import { ESLintUtils, TSESTree } from '@typescript-eslint/experimental-utils';
2-
import { getDocsUrl, ALL_QUERIES_METHODS } from '../utils';
3-
import { isMemberExpression } from '../node-utils';
2+
import {
3+
getDocsUrl,
4+
ALL_QUERIES_METHODS,
5+
PRESENCE_MATCHERS,
6+
ABSENCE_MATCHERS,
7+
} from '../utils';
8+
import {
9+
findClosestCallNode,
10+
isIdentifier,
11+
isMemberExpression,
12+
} from '../node-utils';
413

514
export const RULE_NAME = 'prefer-explicit-assert';
615
export type MessageIds =
@@ -48,6 +57,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)<Options, MessageIds>({
4857
properties: {
4958
assertion: {
5059
type: 'string',
60+
enum: PRESENCE_MATCHERS,
5161
},
5262
customQueryNames: {
5363
type: 'array',
@@ -84,15 +94,29 @@ export default ESLintUtils.RuleCreator(getDocsUrl)<Options, MessageIds>({
8494
messageId: 'preferExplicitAssert',
8595
});
8696
} else if (assertion) {
87-
const expectation = node.parent.parent.parent;
97+
const expectCallNode = findClosestCallNode(node, 'expect');
98+
99+
const expectStatement = expectCallNode.parent as TSESTree.MemberExpression;
100+
const property = expectStatement.property as TSESTree.Identifier;
101+
let matcher = property.name;
102+
let isNegatedMatcher = false;
88103

89104
if (
90-
expectation.type === 'MemberExpression' &&
91-
expectation.property.type === 'Identifier' &&
92-
expectation.property.name !== assertion
105+
matcher === 'not' &&
106+
isMemberExpression(expectStatement.parent) &&
107+
isIdentifier(expectStatement.parent.property)
93108
) {
109+
isNegatedMatcher = true;
110+
matcher = expectStatement.parent.property.name;
111+
}
112+
113+
const shouldEnforceAssertion =
114+
(!isNegatedMatcher && PRESENCE_MATCHERS.includes(matcher)) ||
115+
(isNegatedMatcher && ABSENCE_MATCHERS.includes(matcher));
116+
117+
if (shouldEnforceAssertion && matcher !== assertion) {
94118
context.report({
95-
node: expectation.property,
119+
node: property,
96120
messageId: 'preferExplicitAssertAssertion',
97121
data: {
98122
assertion,

lib/rules/prefer-presence-queries.ts

+1-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ESLintUtils, TSESTree } from '@typescript-eslint/experimental-utils';
2-
import { getDocsUrl, ALL_QUERIES_METHODS } from '../utils';
2+
import { getDocsUrl, ALL_QUERIES_METHODS, PRESENCE_MATCHERS, ABSENCE_MATCHERS } from '../utils';
33
import {
44
findClosestCallNode,
55
isMemberExpression,
@@ -13,8 +13,6 @@ type Options = [];
1313
const QUERIES_REGEXP = new RegExp(
1414
`^(get|query)(All)?(${ALL_QUERIES_METHODS.join('|')})$`
1515
);
16-
const PRESENCE_MATCHERS = ['toBeInTheDocument', 'toBeTruthy', 'toBeDefined'];
17-
const ABSENCE_MATCHERS = ['toBeNull', 'toBeFalsy'];
1816

1917
function isThrowingQuery(node: TSESTree.Identifier) {
2018
return node.name.startsWith('get');

lib/utils.ts

+5
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ const ASYNC_UTILS = [
6565

6666
const TESTING_FRAMEWORK_SETUP_HOOKS = ['beforeEach', 'beforeAll'];
6767

68+
const PRESENCE_MATCHERS = ['toBeInTheDocument', 'toBeTruthy', 'toBeDefined'];
69+
const ABSENCE_MATCHERS = ['toBeNull', 'toBeFalsy'];
70+
6871
export {
6972
getDocsUrl,
7073
SYNC_QUERIES_VARIANTS,
@@ -77,4 +80,6 @@ export {
7780
ASYNC_UTILS,
7881
TESTING_FRAMEWORK_SETUP_HOOKS,
7982
LIBRARY_MODULES,
83+
PRESENCE_MATCHERS,
84+
ABSENCE_MATCHERS
8085
};

tests/lib/rules/prefer-explicit-assert.test.ts

+40-2
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,14 @@ ruleTester.run(RULE_NAME, rule, {
7474
},
7575
],
7676
},
77+
{
78+
code: `expect(getByText('foo')).toBeEnabled()`,
79+
options: [
80+
{
81+
assertion: 'toBeInTheDocument',
82+
},
83+
],
84+
},
7785
],
7886

7987
invalid: [
@@ -144,14 +152,44 @@ ruleTester.run(RULE_NAME, rule, {
144152
code: `expect(getByText('foo')).toBeDefined()`,
145153
options: [
146154
{
147-
assertion: 'toBeInDocument',
155+
assertion: 'toBeInTheDocument',
156+
},
157+
],
158+
errors: [
159+
{
160+
messageId: 'preferExplicitAssertAssertion',
161+
column: 26,
162+
data: { assertion: 'toBeInTheDocument' },
163+
},
164+
],
165+
},
166+
{
167+
code: `expect(getByText('foo')).not.toBeNull()`,
168+
options: [
169+
{
170+
assertion: 'toBeInTheDocument',
171+
},
172+
],
173+
errors: [
174+
{
175+
messageId: 'preferExplicitAssertAssertion',
176+
column: 26,
177+
data: { assertion: 'toBeInTheDocument' },
178+
},
179+
],
180+
},
181+
{
182+
code: `expect(getByText('foo')).not.toBeFalsy()`,
183+
options: [
184+
{
185+
assertion: 'toBeInTheDocument',
148186
},
149187
],
150188
errors: [
151189
{
152190
messageId: 'preferExplicitAssertAssertion',
153191
column: 26,
154-
data: { assertion: 'toBeInDocument' },
192+
data: { assertion: 'toBeInTheDocument' },
155193
},
156194
],
157195
},

0 commit comments

Comments
 (0)