Skip to content

Commit 94758f1

Browse files
authored
feat(prefer-find-by): report presence assertions (#450)
* feat: report presence matchers * refactor: split two functions * refactor: split big if statement * refactor: split two functions * refactor: improve variable names * Add example in docs * refactor: remove redundant comments * Refactor else if -> if * test: add test case with an allowed assertion Co-authored-by: Michaël De Boey <[email protected]> Closes #420
1 parent 9b3261a commit 94758f1

File tree

3 files changed

+654
-115
lines changed

3 files changed

+654
-115
lines changed

docs/rules/prefer-find-by.md

+9
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,15 @@ const submitButton = await waitFor(() =>
2626
const submitButton = await waitFor(() =>
2727
queryAllByText('button', { name: /submit/i })
2828
);
29+
30+
// arrow functions with one statement, calling any sync query method with presence assertion
31+
const submitButton = await waitFor(() =>
32+
expect(queryByLabel('button', { name: /submit/i })).toBeInTheDocument()
33+
);
34+
35+
const submitButton = await waitFor(() =>
36+
expect(queryByLabel('button', { name: /submit/i })).not.toBeFalsy()
37+
);
2938
```
3039

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

lib/rules/prefer-find-by.ts

+252-19
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,230 @@ export default createTestingLibraryRule<Options, MessageIds>({
111111
});
112112
}
113113

114+
function getWrongQueryNameInAssertion(
115+
node: TSESTree.ArrowFunctionExpression
116+
) {
117+
if (
118+
!isCallExpression(node.body) ||
119+
!isMemberExpression(node.body.callee)
120+
) {
121+
return null;
122+
}
123+
124+
// expect(getByText).toBeInTheDocument() shape
125+
if (
126+
isCallExpression(node.body.callee.object) &&
127+
isCallExpression(node.body.callee.object.arguments[0]) &&
128+
ASTUtils.isIdentifier(node.body.callee.object.arguments[0].callee)
129+
) {
130+
return node.body.callee.object.arguments[0].callee.name;
131+
}
132+
133+
if (!ASTUtils.isIdentifier(node.body.callee.property)) {
134+
return null;
135+
}
136+
137+
// expect(screen.getByText).toBeInTheDocument() shape
138+
if (
139+
isCallExpression(node.body.callee.object) &&
140+
isCallExpression(node.body.callee.object.arguments[0]) &&
141+
isMemberExpression(node.body.callee.object.arguments[0].callee) &&
142+
ASTUtils.isIdentifier(
143+
node.body.callee.object.arguments[0].callee.property
144+
)
145+
) {
146+
return node.body.callee.object.arguments[0].callee.property.name;
147+
}
148+
149+
// expect(screen.getByText).not shape
150+
if (
151+
isMemberExpression(node.body.callee.object) &&
152+
isCallExpression(node.body.callee.object.object) &&
153+
isCallExpression(node.body.callee.object.object.arguments[0]) &&
154+
isMemberExpression(
155+
node.body.callee.object.object.arguments[0].callee
156+
) &&
157+
ASTUtils.isIdentifier(
158+
node.body.callee.object.object.arguments[0].callee.property
159+
)
160+
) {
161+
return node.body.callee.object.object.arguments[0].callee.property.name;
162+
}
163+
164+
// expect(getByText).not shape
165+
if (
166+
isMemberExpression(node.body.callee.object) &&
167+
isCallExpression(node.body.callee.object.object) &&
168+
isCallExpression(node.body.callee.object.object.arguments[0]) &&
169+
ASTUtils.isIdentifier(
170+
node.body.callee.object.object.arguments[0].callee
171+
)
172+
) {
173+
return node.body.callee.object.object.arguments[0].callee.name;
174+
}
175+
176+
return node.body.callee.property.name;
177+
}
178+
179+
function getWrongQueryName(node: TSESTree.ArrowFunctionExpression) {
180+
if (!isCallExpression(node.body)) {
181+
return null;
182+
}
183+
184+
// expect(() => getByText) and expect(() => screen.getByText) shape
185+
if (
186+
ASTUtils.isIdentifier(node.body.callee) &&
187+
helpers.isSyncQuery(node.body.callee)
188+
) {
189+
return node.body.callee.name;
190+
}
191+
192+
return getWrongQueryNameInAssertion(node);
193+
}
194+
195+
function getCaller(node: TSESTree.ArrowFunctionExpression) {
196+
if (
197+
!isCallExpression(node.body) ||
198+
!isMemberExpression(node.body.callee)
199+
) {
200+
return null;
201+
}
202+
203+
if (ASTUtils.isIdentifier(node.body.callee.object)) {
204+
// () => screen.getByText
205+
return node.body.callee.object.name;
206+
}
207+
208+
if (
209+
// expect()
210+
isCallExpression(node.body.callee.object) &&
211+
ASTUtils.isIdentifier(node.body.callee.object.callee) &&
212+
isCallExpression(node.body.callee.object.arguments[0]) &&
213+
isMemberExpression(node.body.callee.object.arguments[0].callee) &&
214+
ASTUtils.isIdentifier(
215+
node.body.callee.object.arguments[0].callee.object
216+
)
217+
) {
218+
return node.body.callee.object.arguments[0].callee.object.name;
219+
}
220+
221+
if (
222+
// expect().not
223+
isMemberExpression(node.body.callee.object) &&
224+
isCallExpression(node.body.callee.object.object) &&
225+
isCallExpression(node.body.callee.object.object.arguments[0]) &&
226+
isMemberExpression(
227+
node.body.callee.object.object.arguments[0].callee
228+
) &&
229+
ASTUtils.isIdentifier(
230+
node.body.callee.object.object.arguments[0].callee.object
231+
)
232+
) {
233+
return node.body.callee.object.object.arguments[0].callee.object.name;
234+
}
235+
236+
return null;
237+
}
238+
239+
function isSyncQuery(node: TSESTree.ArrowFunctionExpression) {
240+
if (!isCallExpression(node.body)) {
241+
return false;
242+
}
243+
244+
const isQuery =
245+
ASTUtils.isIdentifier(node.body.callee) &&
246+
helpers.isSyncQuery(node.body.callee);
247+
248+
const isWrappedInPresenceAssert =
249+
isMemberExpression(node.body.callee) &&
250+
isCallExpression(node.body.callee.object) &&
251+
isCallExpression(node.body.callee.object.arguments[0]) &&
252+
ASTUtils.isIdentifier(node.body.callee.object.arguments[0].callee) &&
253+
helpers.isSyncQuery(node.body.callee.object.arguments[0].callee) &&
254+
helpers.isPresenceAssert(node.body.callee);
255+
256+
const isWrappedInNegatedPresenceAssert =
257+
isMemberExpression(node.body.callee) &&
258+
isMemberExpression(node.body.callee.object) &&
259+
isCallExpression(node.body.callee.object.object) &&
260+
isCallExpression(node.body.callee.object.object.arguments[0]) &&
261+
ASTUtils.isIdentifier(
262+
node.body.callee.object.object.arguments[0].callee
263+
) &&
264+
helpers.isSyncQuery(
265+
node.body.callee.object.object.arguments[0].callee
266+
) &&
267+
helpers.isPresenceAssert(node.body.callee.object);
268+
269+
return (
270+
isQuery || isWrappedInPresenceAssert || isWrappedInNegatedPresenceAssert
271+
);
272+
}
273+
274+
function isScreenSyncQuery(node: TSESTree.ArrowFunctionExpression) {
275+
if (!isArrowFunctionExpression(node) || !isCallExpression(node.body)) {
276+
return false;
277+
}
278+
279+
if (
280+
!isMemberExpression(node.body.callee) ||
281+
!ASTUtils.isIdentifier(node.body.callee.property)
282+
) {
283+
return false;
284+
}
285+
286+
if (
287+
!ASTUtils.isIdentifier(node.body.callee.object) &&
288+
!isCallExpression(node.body.callee.object) &&
289+
!isMemberExpression(node.body.callee.object)
290+
) {
291+
return false;
292+
}
293+
294+
const isWrappedInPresenceAssert =
295+
helpers.isPresenceAssert(node.body.callee) &&
296+
isCallExpression(node.body.callee.object) &&
297+
isCallExpression(node.body.callee.object.arguments[0]) &&
298+
isMemberExpression(node.body.callee.object.arguments[0].callee) &&
299+
ASTUtils.isIdentifier(
300+
node.body.callee.object.arguments[0].callee.object
301+
);
302+
303+
const isWrappedInNegatedPresenceAssert =
304+
isMemberExpression(node.body.callee.object) &&
305+
helpers.isPresenceAssert(node.body.callee.object) &&
306+
isCallExpression(node.body.callee.object.object) &&
307+
isCallExpression(node.body.callee.object.object.arguments[0]) &&
308+
isMemberExpression(node.body.callee.object.object.arguments[0].callee);
309+
310+
return (
311+
helpers.isSyncQuery(node.body.callee.property) ||
312+
isWrappedInPresenceAssert ||
313+
isWrappedInNegatedPresenceAssert
314+
);
315+
}
316+
317+
function getQueryArguments(node: TSESTree.CallExpression) {
318+
if (
319+
isMemberExpression(node.callee) &&
320+
isCallExpression(node.callee.object) &&
321+
isCallExpression(node.callee.object.arguments[0])
322+
) {
323+
return node.callee.object.arguments[0].arguments;
324+
}
325+
326+
if (
327+
isMemberExpression(node.callee) &&
328+
isMemberExpression(node.callee.object) &&
329+
isCallExpression(node.callee.object.object) &&
330+
isCallExpression(node.callee.object.object.arguments[0])
331+
) {
332+
return node.callee.object.object.arguments[0].arguments;
333+
}
334+
335+
return node.arguments;
336+
}
337+
114338
return {
115339
'AwaitExpression > CallExpression'(node: TSESTree.CallExpression) {
116340
if (
@@ -122,27 +346,32 @@ export default createTestingLibraryRule<Options, MessageIds>({
122346
// ensure the only argument is an arrow function expression - if the arrow function is a block
123347
// we skip it
124348
const argument = node.arguments[0];
125-
if (!isArrowFunctionExpression(argument)) {
126-
return;
127-
}
128-
if (!isCallExpression(argument.body)) {
349+
if (
350+
!isArrowFunctionExpression(argument) ||
351+
!isCallExpression(argument.body)
352+
) {
129353
return;
130354
}
131355

132356
const waitForMethodName = node.callee.name;
133357

134358
// ensure here it's one of the sync methods that we are calling
135-
if (
136-
isMemberExpression(argument.body.callee) &&
137-
ASTUtils.isIdentifier(argument.body.callee.property) &&
138-
ASTUtils.isIdentifier(argument.body.callee.object) &&
139-
helpers.isSyncQuery(argument.body.callee.property)
140-
) {
359+
if (isScreenSyncQuery(argument)) {
360+
const caller = getCaller(argument);
361+
362+
if (!caller) {
363+
return;
364+
}
365+
141366
// shape of () => screen.getByText
142-
const fullQueryMethod = argument.body.callee.property.name;
143-
const caller = argument.body.callee.object.name;
367+
const fullQueryMethod = getWrongQueryName(argument);
368+
369+
if (!fullQueryMethod) {
370+
return;
371+
}
372+
144373
const queryVariant = getFindByQueryVariant(fullQueryMethod);
145-
const callArguments = argument.body.arguments;
374+
const callArguments = getQueryArguments(argument.body);
146375
const queryMethod = fullQueryMethod.split('By')[1];
147376

148377
reportInvalidUsage(node, {
@@ -166,17 +395,21 @@ export default createTestingLibraryRule<Options, MessageIds>({
166395
});
167396
return;
168397
}
169-
if (
170-
!ASTUtils.isIdentifier(argument.body.callee) ||
171-
!helpers.isSyncQuery(argument.body.callee)
172-
) {
398+
399+
if (!isSyncQuery(argument)) {
173400
return;
174401
}
402+
175403
// shape of () => getByText
176-
const fullQueryMethod = argument.body.callee.name;
404+
const fullQueryMethod = getWrongQueryName(argument);
405+
406+
if (!fullQueryMethod) {
407+
return;
408+
}
409+
177410
const queryMethod = fullQueryMethod.split('By')[1];
178411
const queryVariant = getFindByQueryVariant(fullQueryMethod);
179-
const callArguments = argument.body.arguments;
412+
const callArguments = getQueryArguments(argument.body);
180413

181414
reportInvalidUsage(node, {
182415
queryMethod,

0 commit comments

Comments
 (0)