Skip to content

Commit 2c4f8ca

Browse files
Kent C. DoddsKent C. Dodds
Kent C. Dodds
authored and
Kent C. Dodds
committed
fix(getBy*): throw an error if more than one element is found
Closes #202 BREAKING CHANGE: All `getBy` and `findBy` query variants now will throw an error if more than one element is returned. If this is expected, then use `getAllBy` (or `findAllBy`) instead.
1 parent 48b79d0 commit 2c4f8ca

File tree

3 files changed

+129
-18
lines changed

3 files changed

+129
-18
lines changed

src/__tests__/get-by-errors.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import cases from 'jest-in-case'
2+
import {render} from './helpers/test-utils'
3+
4+
cases(
5+
'getBy* queries throw an error when there are multiple elements returned',
6+
({name, query, html}) => {
7+
const utils = render(html)
8+
expect(() => utils[name](query)).toThrow(/multiple elements/i)
9+
},
10+
{
11+
getByLabelText: {
12+
query: /his/,
13+
html: `<div aria-label="his"></div><div aria-label="history"></div>`,
14+
},
15+
getByPlaceholderText: {
16+
query: /his/,
17+
html: `<input placeholder="his" /><input placeholder="history" />`,
18+
},
19+
getByText: {
20+
query: /his/,
21+
html: `<div>his</div><div>history</div>`,
22+
},
23+
getByAltText: {
24+
query: /his/,
25+
html: `<img alt="his" src="his.png" /><img alt="history" src="history.png" />`,
26+
},
27+
getByTitle: {
28+
query: /his/,
29+
html: `<div title="his"></div><div title="history"></div>`,
30+
},
31+
getByDisplayValue: {
32+
query: /his/,
33+
html: `<input value="his" /><select><option value="history">history</option></select>`,
34+
},
35+
getByRole: {
36+
query: /his/,
37+
html: `<div role="his"></div><div role="history"></div>`,
38+
},
39+
getByTestId: {
40+
query: /his/,
41+
html: `<div data-testid="his"></div><div data-testid="history"></div>`,
42+
},
43+
},
44+
)

src/queries.js

Lines changed: 77 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {fuzzyMatches, matches, makeNormalizer} from './matches'
22
import {getNodeText} from './get-node-text'
33
import {
44
getElementError,
5+
getGetByElementError,
56
firstResultOrNull,
67
queryAllByAttribute,
78
queryByAttribute,
@@ -136,7 +137,6 @@ function queryAllByTitle(
136137
function queryByTitle(...args) {
137138
return firstResultOrNull(queryAllByTitle, ...args)
138139
}
139-
140140
function getTestIdAttribute() {
141141
return getConfig().testIdAttribute
142142
}
@@ -209,8 +209,15 @@ function getAllByTestId(container, id, ...rest) {
209209
return els
210210
}
211211

212-
function getByTestId(...args) {
213-
return firstResultOrNull(getAllByTestId, ...args)
212+
function getByTestId(container, id, ...args) {
213+
const els = getAllByTestId(container, id, ...args)
214+
if (els.length > 1) {
215+
throw getGetByElementError(
216+
`Found multiple elements by: [${getTestIdAttribute()}="${id}"]`,
217+
container,
218+
)
219+
}
220+
return els[0]
214221
}
215222

216223
function getAllByTitle(container, title, ...rest) {
@@ -224,8 +231,15 @@ function getAllByTitle(container, title, ...rest) {
224231
return els
225232
}
226233

227-
function getByTitle(...args) {
228-
return firstResultOrNull(getAllByTitle, ...args)
234+
function getByTitle(container, title, ...args) {
235+
const els = getAllByTitle(container, title, ...args)
236+
if (els.length > 1) {
237+
throw getGetByElementError(
238+
`Found multiple elements with the title: ${title}.`,
239+
container,
240+
)
241+
}
242+
return els[0]
229243
}
230244

231245
function getAllByPlaceholderText(container, text, ...rest) {
@@ -239,8 +253,15 @@ function getAllByPlaceholderText(container, text, ...rest) {
239253
return els
240254
}
241255

242-
function getByPlaceholderText(...args) {
243-
return firstResultOrNull(getAllByPlaceholderText, ...args)
256+
function getByPlaceholderText(container, text, ...args) {
257+
const els = getAllByPlaceholderText(container, text, ...args)
258+
if (els.length > 1) {
259+
throw getGetByElementError(
260+
`Found multiple elements with the placeholder text of: ${text}`,
261+
container,
262+
)
263+
}
264+
return els[0]
244265
}
245266

246267
function getAllByLabelText(container, text, ...rest) {
@@ -262,8 +283,15 @@ function getAllByLabelText(container, text, ...rest) {
262283
return els
263284
}
264285

265-
function getByLabelText(...args) {
266-
return firstResultOrNull(getAllByLabelText, ...args)
286+
function getByLabelText(container, text, ...args) {
287+
const els = getAllByLabelText(container, text, ...args)
288+
if (els.length > 1) {
289+
throw getGetByElementError(
290+
`Found multiple elements with the text of: ${text}`,
291+
container,
292+
)
293+
}
294+
return els[0]
267295
}
268296

269297
function getAllByText(container, text, ...rest) {
@@ -277,8 +305,15 @@ function getAllByText(container, text, ...rest) {
277305
return els
278306
}
279307

280-
function getByText(...args) {
281-
return firstResultOrNull(getAllByText, ...args)
308+
function getByText(container, text, ...args) {
309+
const els = getAllByText(container, text, ...args)
310+
if (els.length > 1) {
311+
throw getGetByElementError(
312+
`Found multiple elements with the text: ${text}`,
313+
container,
314+
)
315+
}
316+
return els[0]
282317
}
283318

284319
function getAllByAltText(container, alt, ...rest) {
@@ -292,20 +327,37 @@ function getAllByAltText(container, alt, ...rest) {
292327
return els
293328
}
294329

295-
function getByAltText(...args) {
296-
return firstResultOrNull(getAllByAltText, ...args)
330+
function getByAltText(container, alt, ...args) {
331+
const els = getAllByAltText(container, alt, ...args)
332+
if (els.length > 1) {
333+
throw getGetByElementError(
334+
`Found multiple elements with the alt text: ${alt}`,
335+
container,
336+
)
337+
}
338+
return els[0]
297339
}
298340

299341
function getAllByRole(container, id, ...rest) {
300342
const els = queryAllByRole(container, id, ...rest)
301343
if (!els.length) {
302-
throw getElementError(`Unable to find an element by role=${id}`, container)
344+
throw getElementError(
345+
`Unable to find an element by [role=${id}]`,
346+
container,
347+
)
303348
}
304349
return els
305350
}
306351

307-
function getByRole(...args) {
308-
return firstResultOrNull(getAllByRole, ...args)
352+
function getByRole(container, id, ...args) {
353+
const els = getAllByRole(container, id, ...args)
354+
if (els.length > 1) {
355+
throw getGetByElementError(
356+
`Found multiple elements by [role=${id}]`,
357+
container,
358+
)
359+
}
360+
return els[0]
309361
}
310362

311363
function getAllByDisplayValue(container, value, ...rest) {
@@ -319,8 +371,15 @@ function getAllByDisplayValue(container, value, ...rest) {
319371
return els
320372
}
321373

322-
function getByDisplayValue(...args) {
323-
return firstResultOrNull(getAllByDisplayValue, ...args)
374+
function getByDisplayValue(container, value, ...args) {
375+
const els = getAllByDisplayValue(container, value, ...args)
376+
if (els.length > 1) {
377+
throw getGetByElementError(
378+
`Found multiple elements with the value: ${value}.`,
379+
container,
380+
)
381+
}
382+
return els[0]
324383
}
325384

326385
function makeFinder(getter) {

src/query-helpers.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@ function getElementError(message, container) {
3030
return new Error([message, debugDOM(container)].filter(Boolean).join('\n\n'))
3131
}
3232

33+
function getGetByElementError(message, container) {
34+
return getElementError(
35+
`${message}\n\n(If this is intentional, then use the \`*AllBy*\` variant of the query (like \`getAllByText\` or \`findAllByText\`)).`,
36+
container,
37+
)
38+
}
39+
3340
function firstResultOrNull(queryFunction, ...args) {
3441
const result = queryFunction(...args)
3542
if (result.length === 0) return null
@@ -56,6 +63,7 @@ function queryByAttribute(...args) {
5663
export {
5764
debugDOM,
5865
getElementError,
66+
getGetByElementError,
5967
firstResultOrNull,
6068
queryAllByAttribute,
6169
queryByAttribute,

0 commit comments

Comments
 (0)