Skip to content

Commit 839dca3

Browse files
benmonroBen Monrokentcdodds
authored
feat: suggestions for which query to use (#586)
* feat: suggestions for which query to use * coverage bumped * more tests, label text working * more cases for labelText * removed commented out code * added *ByDisplayValue * all queries supported now * cleanup * added types for suggestions * export suggestions from index * fixed a couple lint warnings * Update src/__tests__/suggestions.js Co-authored-by: Kent C. Dodds <[email protected]> * Update src/config.js haha Co-authored-by: Kent C. Dodds <[email protected]> * Update src/__tests__/suggestions.js Co-authored-by: Kent C. Dodds <[email protected]> * Update src/config.js Co-authored-by: Kent C. Dodds <[email protected]> * Update src/suggestions.js Co-authored-by: Kent C. Dodds <[email protected]> * Update src/query-helpers.js * PR feedback * refactor to getLabelFor * formatting * added support for suggest:false * case ignored regex * using full query name for get & getAll & query * suggest on labeltext * suggest on queryAllBy * more tests * rename showSuggs to throwSuggs * PR feedback * matches.d.ts * Update types/matches.d.ts Co-authored-by: Ben Monro <[email protected]> Co-authored-by: Kent C. Dodds <[email protected]>
1 parent 709044b commit 839dca3

17 files changed

+560
-42
lines changed

src/__tests__/suggestions.js

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
import {configure} from '../config'
2+
import {screen} from '..'
3+
import {renderIntoDocument} from './helpers/test-utils'
4+
5+
beforeAll(() => {
6+
configure({throwSuggestions: true})
7+
})
8+
9+
afterAll(() => {
10+
configure({throwSuggestions: false})
11+
})
12+
13+
test('does not suggest when using getByRole', () => {
14+
renderIntoDocument(`<button data-testid="foo">submit</button>`)
15+
16+
expect(() => screen.getByRole('button', {name: /submit/i})).not.toThrowError()
17+
})
18+
19+
test('should not suggest when nothing available', () => {
20+
renderIntoDocument(`<span data-testid="foo" />`)
21+
22+
expect(() => screen.queryByTestId('foo')).not.toThrowError()
23+
})
24+
25+
test(`should not suggest if the suggestion would give different results`, () => {
26+
renderIntoDocument(`
27+
<input type="text" data-testid="foo" /><span data-testid="foo" />
28+
`)
29+
30+
expect(() =>
31+
screen.getAllByTestId('foo', {suggest: false}),
32+
).not.toThrowError()
33+
})
34+
35+
test('should not suggest if there would be mixed suggestions', () => {
36+
renderIntoDocument(`
37+
<button data-testid="foo">submit</button>
38+
<label for="foo">Username</label><input data-testid="foo" id="foo" />`)
39+
40+
expect(() => screen.getAllByTestId('foo')).not.toThrowError()
41+
})
42+
43+
test('should not suggest when suggest is turned off for a query', () => {
44+
renderIntoDocument(`
45+
<button data-testid="foo">submit</button>
46+
<button data-testid="foot">another</button>`)
47+
48+
expect(() => screen.getByTestId('foo', {suggest: false})).not.toThrowError()
49+
expect(() =>
50+
screen.getAllByTestId(/foo/, {suggest: false}),
51+
).not.toThrowError()
52+
})
53+
54+
test('should suggest getByRole when used with getBy', () => {
55+
renderIntoDocument(`<button data-testid="foo">submit</button>`)
56+
57+
expect(() => screen.getByTestId('foo')).toThrowErrorMatchingInlineSnapshot(`
58+
"A better query is available, try this:
59+
getByRole("button", {name: /submit/i})
60+
61+
62+
<body>
63+
<button
64+
data-testid="foo"
65+
>
66+
submit
67+
</button>
68+
</body>"
69+
`)
70+
})
71+
72+
test('should suggest getAllByRole when used with getAllByTestId', () => {
73+
renderIntoDocument(`
74+
<button data-testid="foo">submit</button>
75+
<button data-testid="foo">submit</button>`)
76+
77+
expect(() => screen.getAllByTestId('foo'))
78+
.toThrowErrorMatchingInlineSnapshot(`
79+
"A better query is available, try this:
80+
getAllByRole("button", {name: /submit/i})
81+
82+
83+
<body>
84+
85+
86+
<button
87+
data-testid="foo"
88+
>
89+
submit
90+
</button>
91+
92+
93+
<button
94+
data-testid="foo"
95+
>
96+
submit
97+
</button>
98+
</body>"
99+
`)
100+
})
101+
test('should suggest findByRole when used with findByTestId', async () => {
102+
renderIntoDocument(`
103+
<button data-testid="foo">submit</button>
104+
<button data-testid="foot">submit</button>
105+
`)
106+
107+
await expect(screen.findByTestId('foo')).rejects.toThrowError(
108+
/findByRole\("button", \{name: \/submit\/i\}\)/,
109+
)
110+
await expect(screen.findAllByTestId(/foo/)).rejects.toThrowError(
111+
/findAllByRole\("button", \{name: \/submit\/i\}\)/,
112+
)
113+
})
114+
115+
test('should suggest img role w/ alt text', () => {
116+
renderIntoDocument(`<img data-testid="img" alt="Incredibles 2 Poster" />`)
117+
118+
expect(() => screen.getByAltText('Incredibles 2 Poster')).toThrowError(
119+
/getByRole\("img", \{name: \/incredibles 2 poster\/i\}\)/,
120+
)
121+
})
122+
123+
test('should suggest getByLabelText when no role available', () => {
124+
renderIntoDocument(
125+
`<label for="foo">Username</label><input data-testid="foo" id="foo" />`,
126+
)
127+
expect(() => screen.getByTestId('foo')).toThrowError(
128+
/getByLabelText\("Username"\)/,
129+
)
130+
})
131+
132+
test(`should suggest getByLabel on non form elements`, () => {
133+
renderIntoDocument(`
134+
<div data-testid="foo" aria-labelledby="section-one-header">
135+
<span id="section-one-header">Section One</span>
136+
<p>some content</p>
137+
</div>
138+
`)
139+
140+
expect(() => screen.getByTestId('foo')).toThrowError(
141+
/getByLabelText\("Section One"\)/,
142+
)
143+
})
144+
145+
test.each([
146+
`<label id="username-label">Username</label><input aria-labelledby="username-label" type="text" />`,
147+
`<label><span>Username</span><input type="text" /></label>`,
148+
`<label for="foo">Username</label><input id="foo" type="text" />`,
149+
])('%s\nshould suggest getByRole over', async html => {
150+
renderIntoDocument(html)
151+
152+
expect(() => screen.getByLabelText('Username')).toThrowError(
153+
/getByRole\("textbox", \{name: \/username\/i\}\)/,
154+
)
155+
expect(() => screen.getAllByLabelText('Username')).toThrowError(
156+
/getAllByRole\("textbox", \{name: \/username\/i\}\)/,
157+
)
158+
159+
expect(() => screen.queryByLabelText('Username')).toThrowError(
160+
/queryByRole\("textbox", \{name: \/username\/i\}\)/,
161+
)
162+
expect(() => screen.queryAllByLabelText('Username')).toThrowError(
163+
/queryAllByRole\("textbox", \{name: \/username\/i\}\)/,
164+
)
165+
166+
await expect(screen.findByLabelText('Username')).rejects.toThrowError(
167+
/findByRole\("textbox", \{name: \/username\/i\}\)/,
168+
)
169+
await expect(screen.findAllByLabelText(/Username/)).rejects.toThrowError(
170+
/findAllByRole\("textbox", \{name: \/username\/i\}\)/,
171+
)
172+
})
173+
174+
test(`should suggest label over placeholder text`, () => {
175+
renderIntoDocument(
176+
`<label for="foo">Username</label><input id="foo" data-testid="foo" placeholder="Username" />`,
177+
)
178+
179+
expect(() => screen.getByPlaceholderText('Username')).toThrowError(
180+
/getByLabelText\("Username"\)/,
181+
)
182+
})
183+
184+
test(`should suggest getByPlaceholderText`, () => {
185+
renderIntoDocument(`<input data-testid="foo" placeholder="Username" />`)
186+
187+
expect(() => screen.getByTestId('foo')).toThrowError(
188+
/getByPlaceholderText\("Username"\)/,
189+
)
190+
})
191+
192+
test(`should suggest getByText for simple elements`, () => {
193+
renderIntoDocument(`<div data-testid="foo">hello there</div>`)
194+
195+
expect(() => screen.getByTestId('foo')).toThrowError(
196+
/getByText\("hello there"\)/,
197+
)
198+
})
199+
200+
test(`should suggest getByDisplayValue`, () => {
201+
renderIntoDocument(`<input id="lastName" data-testid="lastName" />`)
202+
203+
document.getElementById('lastName').value = 'Prine' // RIP John Prine
204+
205+
expect(() => screen.getByTestId('lastName')).toThrowError(
206+
/getByDisplayValue\("Prine"\)/,
207+
)
208+
})
209+
210+
test(`should suggest getByAltText`, () => {
211+
renderIntoDocument(`
212+
<input data-testid="input" alt="last name" />
213+
<map name="workmap">
214+
<area data-testid="area" shape="rect" coords="34,44,270,350" alt="Computer">
215+
</map>
216+
`)
217+
218+
expect(() => screen.getByTestId('input')).toThrowError(
219+
/getByAltText\("last name"\)/,
220+
)
221+
expect(() => screen.getByTestId('area')).toThrowError(
222+
/getByAltText\("Computer"\)/,
223+
)
224+
})
225+
226+
test(`should suggest getByTitle`, () => {
227+
renderIntoDocument(`
228+
<span title="Delete" data-testid="delete"></span>
229+
<svg>
230+
<title data-testid="svg">Close</title>
231+
<g><path /></g>
232+
</svg>`)
233+
234+
expect(() => screen.getByTestId('delete')).toThrowError(
235+
/getByTitle\("Delete"\)/,
236+
)
237+
expect(() => screen.getAllByTestId('delete')).toThrowError(
238+
/getAllByTitle\("Delete"\)/,
239+
)
240+
expect(() => screen.queryByTestId('delete')).toThrowError(
241+
/queryByTitle\("Delete"\)/,
242+
)
243+
expect(() => screen.queryAllByTestId('delete')).toThrowError(
244+
/queryAllByTitle\("Delete"\)/,
245+
)
246+
expect(() => screen.queryAllByTestId('delete')).toThrowError(
247+
/queryAllByTitle\("Delete"\)/,
248+
)
249+
expect(() => screen.queryAllByTestId('delete')).toThrowError(
250+
/queryAllByTitle\("Delete"\)/,
251+
)
252+
253+
// Since `ByTitle` and `ByText` will both return the <title> element
254+
// `getByText` will always be the suggested query as it is higher up the list.
255+
expect(() => screen.getByTestId('svg')).toThrowError(/getByText\("Close"\)/)
256+
})

src/config.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,12 @@ let config = {
1616
asyncWrapper: cb => cb(),
1717
// default value for the `hidden` option in `ByRole` queries
1818
defaultHidden: false,
19-
//showOriginalStackTrace flag to show the full error stack traces for async errors
19+
// showOriginalStackTrace flag to show the full error stack traces for async errors
2020
showOriginalStackTrace: false,
2121

22+
// throw errors w/ suggestions for better queries. Opt in so off by default.
23+
throwSuggestions: false,
24+
2225
// called when getBy* queries fail. (message, container) => Error
2326
getElementError(message, container) {
2427
const error = new Error(

src/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export * from './query-helpers'
1616
export {getRoles, logRoles, isInaccessible} from './role-helpers'
1717
export * from './pretty-dom'
1818
export {configure} from './config'
19+
export * from './suggestions'
1920

2021
export {
2122
// "within" reads better in user-code

src/queries/alt-text.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {matches, fuzzyMatches, makeNormalizer, buildQueries} from './all-utils'
2+
import {wrapAllByQueryWithSuggestion} from '../query-helpers'
23

34
function queryAllByAltText(
45
container,
@@ -16,6 +17,12 @@ const getMultipleError = (c, alt) =>
1617
`Found multiple elements with the alt text: ${alt}`
1718
const getMissingError = (c, alt) =>
1819
`Unable to find an element with the alt text: ${alt}`
20+
21+
const queryAllByAltTextWithSuggestions = wrapAllByQueryWithSuggestion(
22+
queryAllByAltText,
23+
queryAllByAltText.name,
24+
'queryAll',
25+
)
1926
const [
2027
queryByAltText,
2128
getAllByAltText,
@@ -26,7 +33,7 @@ const [
2633

2734
export {
2835
queryByAltText,
29-
queryAllByAltText,
36+
queryAllByAltTextWithSuggestions as queryAllByAltText,
3037
getByAltText,
3138
getAllByAltText,
3239
findAllByAltText,

src/queries/display-value.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
makeNormalizer,
66
buildQueries,
77
} from './all-utils'
8+
import {wrapAllByQueryWithSuggestion} from '../query-helpers'
89

910
function queryAllByDisplayValue(
1011
container,
@@ -33,6 +34,13 @@ const getMultipleError = (c, value) =>
3334
`Found multiple elements with the display value: ${value}.`
3435
const getMissingError = (c, value) =>
3536
`Unable to find an element with the display value: ${value}.`
37+
38+
const queryAllByDisplayValueWithSuggestions = wrapAllByQueryWithSuggestion(
39+
queryAllByDisplayValue,
40+
queryAllByDisplayValue.name,
41+
'queryAll',
42+
)
43+
3644
const [
3745
queryByDisplayValue,
3846
getAllByDisplayValue,
@@ -43,7 +51,7 @@ const [
4351

4452
export {
4553
queryByDisplayValue,
46-
queryAllByDisplayValue,
54+
queryAllByDisplayValueWithSuggestions as queryAllByDisplayValue,
4755
getByDisplayValue,
4856
getAllByDisplayValue,
4957
findAllByDisplayValue,

0 commit comments

Comments
 (0)