Skip to content

Commit 099ad7b

Browse files
committed
fix(prefer-in-documment): make toHaveLength(1) with AllBy queries valid
1 parent efae7d0 commit 099ad7b

File tree

2 files changed

+63
-56
lines changed

2 files changed

+63
-56
lines changed

src/__tests__/lib/rules/prefer-in-document.js

+17-51
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,23 @@ const valid = [
9797
`
9898
const element = getByText('value')
9999
expect(element).toBeInTheDocument`,
100+
101+
// *AllBy queries with `.toHaveLength(1)` is valid
102+
// see conclusion at https://github.com/testing-library/eslint-plugin-jest-dom/issues/171#issuecomment-895074086
103+
`expect(screen.getAllByRole('foo')).toHaveLength(1)`,
104+
`expect(await screen.findAllByRole('foo')).toHaveLength(1)`,
105+
`expect(getAllByRole('foo')).toHaveLength(1)`,
106+
`expect(wrapper.getAllByRole('foo')).toHaveLength(1)`,
107+
`const foo = screen.getAllByRole('foo');
108+
expect(foo).toHaveLength(1);`,
109+
`const foo = getAllByRole('foo');
110+
expect(foo).toHaveLength(1);`,
111+
`let foo;
112+
foo = getAllByRole('foo');
113+
expect(foo).toHaveLength(1);`,
114+
`let foo;
115+
foo = screen.getAllByRole('foo');
116+
expect(foo).toHaveLength(1);`,
100117
];
101118
const invalid = [
102119
invalidCase(
@@ -150,51 +167,6 @@ const invalid = [
150167
foo = screen.getByText('foo');
151168
expect(foo).toBeInTheDocument();`
152169
),
153-
invalidCase(
154-
`expect(screen.getAllByRole('foo')).toHaveLength(1)`,
155-
`expect(screen.getByRole('foo')).toBeInTheDocument()`
156-
),
157-
invalidCase(
158-
`expect(await screen.findAllByRole('foo')).toHaveLength(1)`,
159-
`expect(await screen.findByRole('foo')).toBeInTheDocument()`
160-
),
161-
invalidCase(
162-
`expect(getAllByRole('foo')).toHaveLength(1)`,
163-
`expect(getByRole('foo')).toBeInTheDocument()`
164-
),
165-
invalidCase(
166-
`expect(wrapper.getAllByRole('foo')).toHaveLength(1)`,
167-
`expect(wrapper.getByRole('foo')).toBeInTheDocument()`
168-
),
169-
invalidCase(
170-
`const foo = screen.getAllByRole('foo');
171-
expect(foo).toHaveLength(1);`,
172-
`const foo = screen.getByRole('foo');
173-
expect(foo).toBeInTheDocument();`
174-
),
175-
invalidCase(
176-
`const foo = getAllByRole('foo');
177-
expect(foo).toHaveLength(1);`,
178-
`const foo = getByRole('foo');
179-
expect(foo).toBeInTheDocument();`
180-
),
181-
invalidCase(
182-
`let foo;
183-
foo = getAllByRole('foo');
184-
expect(foo).toHaveLength(1);`,
185-
`let foo;
186-
foo = getByRole('foo');
187-
expect(foo).toBeInTheDocument();`
188-
),
189-
invalidCase(
190-
`let foo;
191-
foo = screen.getAllByRole('foo');
192-
expect(foo).toHaveLength(1);`,
193-
`let foo;
194-
foo = screen.getByRole('foo');
195-
expect(foo).toBeInTheDocument();`
196-
),
197-
198170
invalidCase(
199171
`expect(screen.getByText('foo')).toHaveLength(1)`,
200172
`expect(screen.getByText('foo')).toBeInTheDocument()`
@@ -461,12 +433,6 @@ const invalid = [
461433
span = getByText('foo') as HTMLSpanElement
462434
expect(span).toBeInTheDocument()`
463435
),
464-
invalidCase(
465-
`const things = screen.getAllByText("foo");
466-
expect(things).toHaveLength(1);`,
467-
`const things = screen.getByText("foo");
468-
expect(things).toBeInTheDocument();`
469-
),
470436
];
471437

472438
const ruleTester = new RuleTester({

src/rules/prefer-in-document.js

+46-5
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,24 @@ function usesToHaveLengthZero(matcherNode, matcherArguments) {
4343
return matcherNode.name === "toHaveLength" && matcherArguments[0].value === 0;
4444
}
4545

46+
/**
47+
* Extract the DTL query identifier from a call expression
48+
*
49+
* <query>() -> <query>
50+
* screen.<query>() -> <query>
51+
*/
52+
function getDTLQueryIdentifierNode(callExpressionNode) {
53+
if (!callExpressionNode || callExpressionNode.type !== "CallExpression") {
54+
return;
55+
}
56+
57+
if (callExpressionNode.callee.type === "Identifier") {
58+
return callExpressionNode.callee;
59+
}
60+
61+
return callExpressionNode.callee.property;
62+
}
63+
4664
export const create = (context) => {
4765
const alternativeMatchers =
4866
/^(toHaveLength|toBeDefined|toBeNull|toBe|toEqual|toBeTruthy|toBeFalsy)$/;
@@ -84,7 +102,12 @@ export const create = (context) => {
84102
// isNotToHaveLengthZero represents .not.toHaveLength(0) which is a valid use of toHaveLength
85103
const isNotToHaveLengthZero =
86104
usesToHaveLengthZero(matcherNode, matcherArguments) && negatedMatcher;
105+
87106
const isValidUseOfToHaveLength =
107+
// .toHaveLength(1) is valid when used with *AllBy* queries
108+
// meaning checking for exactly one match
109+
// see discussion https://github.com/testing-library/eslint-plugin-jest-dom/issues/171
110+
(lengthValue === 1 && /AllBy/.test(queryNode.name)) ||
88111
isNotToHaveLengthZero ||
89112
!["Literal", "Identifier"].includes(matcherArguments[0].type) ||
90113
lengthValue === undefined ||
@@ -184,6 +207,12 @@ export const create = (context) => {
184207
context,
185208
node.object.object.arguments[0].name
186209
);
210+
211+
// Not an RTL query
212+
if (!queryNode || queryNode.type !== "CallExpression") {
213+
return;
214+
}
215+
187216
const matcherNode = node.property;
188217

189218
const matcherArguments = node.parent.arguments;
@@ -201,16 +230,25 @@ export const create = (context) => {
201230
[`MemberExpression[object.callee.name=expect][property.name=${alternativeMatchers}][object.arguments.0.type=Identifier]`](
202231
node
203232
) {
204-
const queryNode = getAssignmentForIdentifier(
233+
// Value expression being assigned to the left-hand value
234+
const rightValueNode = getAssignmentForIdentifier(
205235
context,
206236
node.object.arguments[0].name
207237
);
238+
239+
// Not a DTL query
240+
if (!rightValueNode || rightValueNode.type !== "CallExpression") {
241+
return;
242+
}
243+
244+
const queryIdentifierNode = getDTLQueryIdentifierNode(rightValueNode);
245+
208246
const matcherNode = node.property;
209247

210248
const matcherArguments = node.parent.arguments;
211249
check({
212250
negatedMatcher: false,
213-
queryNode: (queryNode && queryNode.callee) || queryNode,
251+
queryNode: queryIdentifierNode,
214252
matcherNode,
215253
matcherArguments,
216254
});
@@ -226,14 +264,17 @@ export const create = (context) => {
226264
return;
227265
}
228266

229-
const queryNode =
230-
arg.type === "AwaitExpression" ? arg.argument.callee : arg.callee;
267+
const queryIdentifierNode =
268+
arg.type === "AwaitExpression"
269+
? getDTLQueryIdentifierNode(arg.argument)
270+
: getDTLQueryIdentifierNode(arg);
271+
231272
const matcherNode = node.callee.property;
232273
const matcherArguments = node.arguments;
233274

234275
check({
235276
negatedMatcher: false,
236-
queryNode,
277+
queryNode: queryIdentifierNode,
237278
matcherNode,
238279
matcherArguments,
239280
});

0 commit comments

Comments
 (0)