Skip to content

Commit 5b0a717

Browse files
committed
breaking: opt-in to fuzzy matching
- Changes queries to default to exact string matching - Can opt-in to fuzzy matches by passing { exact: true } as the last arg - Queries that search inner text collapse whitespace (queryByText, queryByLabelText) This is a breaking change!
1 parent 5fe849f commit 5b0a717

File tree

6 files changed

+199
-52
lines changed

6 files changed

+199
-52
lines changed

src/__tests__/__snapshots__/element-queries.js.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ exports[`get throws a useful error message 6`] = `
4949
`;
5050

5151
exports[`label with no form control 1`] = `
52-
"Found a label with the text of: alone, however no form control was found associated to that label. Make sure you're using the \\"for\\" attribute or \\"aria-labelledby\\" attribute correctly.
52+
"Found a label with the text of: /alone/, however no form control was found associated to that label. Make sure you're using the \\"for\\" attribute or \\"aria-labelledby\\" attribute correctly.
5353
5454
<div>
5555
<label>

src/__tests__/element-queries.js

Lines changed: 113 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,8 @@ test('get can get form controls by placeholder', () => {
9393

9494
test('label with no form control', () => {
9595
const {getByLabelText, queryByLabelText} = render(`<label>All alone</label>`)
96-
expect(queryByLabelText('alone')).toBeNull()
97-
expect(() => getByLabelText('alone')).toThrowErrorMatchingSnapshot()
96+
expect(queryByLabelText(/alone/)).toBeNull()
97+
expect(() => getByLabelText(/alone/)).toThrowErrorMatchingSnapshot()
9898
})
9999

100100
test('totally empty label', () => {
@@ -106,7 +106,7 @@ test('totally empty label', () => {
106106
test('getByLabelText with aria-label', () => {
107107
// not recommended normally, but supported for completeness
108108
const {queryByLabelText} = render(`<input aria-label="batman" />`)
109-
expect(queryByLabelText('bat')).toBeInTheDOM()
109+
expect(queryByLabelText(/bat/)).toBeInTheDOM()
110110
})
111111

112112
test('get element by its alt text', () => {
@@ -144,6 +144,114 @@ test('can get elements by data-testid attribute', () => {
144144
expect(queryByTestId('first-name')).not.toBeInTheDOM()
145145
})
146146

147+
test('queries passed strings match case-sensitive exact strings', () => {
148+
const {
149+
queryAllByTestId,
150+
queryAllByAltText,
151+
queryAllByText,
152+
queryAllByLabelText,
153+
queryAllByPlaceholderText,
154+
} = render(`
155+
<div>
156+
<img
157+
data-testid="poster"
158+
alt="Finding Nemo poster"
159+
src="/finding-nemo.png" />
160+
<img
161+
data-testid="poster"
162+
alt="Finding Dory poster"
163+
src="/finding-dory.png" />
164+
<img
165+
data-testid="poster"
166+
alt="jumanji poster"
167+
src="/jumanji.png" />
168+
<p>Where to next?</p>
169+
<p>
170+
content
171+
with
172+
linebreaks
173+
is
174+
ok
175+
</p>
176+
<label for="username">User Name</label>
177+
<input id="username" placeholder="Dwayne 'The Rock' Johnson" />
178+
</div>,
179+
`)
180+
expect(queryAllByAltText('Finding Nemo poster')).toHaveLength(1)
181+
expect(queryAllByAltText('Finding')).toHaveLength(0)
182+
expect(queryAllByAltText('finding nemo poster')).toHaveLength(0)
183+
expect(queryAllByTestId('poster')).toHaveLength(3)
184+
expect(queryAllByTestId('Poster')).toHaveLength(0)
185+
expect(queryAllByTestId('post')).toHaveLength(0)
186+
expect(queryAllByPlaceholderText("Dwayne 'The Rock' Johnson")).toHaveLength(1)
187+
expect(queryAllByPlaceholderText('The Rock')).toHaveLength(0)
188+
expect(queryAllByPlaceholderText("dwayne 'the rock' johnson")).toHaveLength(0)
189+
expect(queryAllByLabelText('User Name')).toHaveLength(1)
190+
expect(queryAllByLabelText('user name')).toHaveLength(0)
191+
expect(queryAllByLabelText('User')).toHaveLength(0)
192+
expect(queryAllByText('Where to next?')).toHaveLength(1)
193+
expect(queryAllByText('Where to next')).toHaveLength(0)
194+
expect(queryAllByText('Where')).toHaveLength(0)
195+
expect(queryAllByText('where to next?')).toHaveLength(0)
196+
expect(queryAllByText('content with linebreaks is ok')).toHaveLength(1)
197+
})
198+
199+
test('passing {exact: false} uses fuzzy matches', () => {
200+
const fuzzy = Object.freeze({exact: false})
201+
const {
202+
queryAllByTestId,
203+
queryAllByAltText,
204+
queryAllByText,
205+
queryAllByLabelText,
206+
queryAllByPlaceholderText,
207+
} = render(`
208+
<div>
209+
<img
210+
data-testid="poster"
211+
alt="Finding Nemo poster"
212+
src="/finding-nemo.png" />
213+
<img
214+
data-testid="poster"
215+
alt="Finding Dory poster"
216+
src="/finding-dory.png" />
217+
<img
218+
data-testid="poster"
219+
alt="jumanji poster"
220+
src="/jumanji.png" />
221+
<p>Where to next?</p>
222+
<p>
223+
content
224+
with
225+
linebreaks
226+
</p>
227+
<label for="username">User Name</label>
228+
<input id="username" placeholder="Dwayne 'The Rock' Johnson" />
229+
</div>,
230+
`)
231+
expect(queryAllByAltText('Finding Nemo poster', fuzzy)).toHaveLength(1)
232+
expect(queryAllByAltText('Finding', fuzzy)).toHaveLength(2)
233+
expect(queryAllByAltText('finding nemo poster', fuzzy)).toHaveLength(1)
234+
expect(queryAllByTestId('poster', fuzzy)).toHaveLength(3)
235+
expect(queryAllByTestId('Poster', fuzzy)).toHaveLength(3)
236+
expect(queryAllByTestId('post', fuzzy)).toHaveLength(3)
237+
expect(
238+
queryAllByPlaceholderText("Dwayne 'The Rock' Johnson", fuzzy),
239+
).toHaveLength(1)
240+
expect(queryAllByPlaceholderText('The Rock', fuzzy)).toHaveLength(1)
241+
expect(
242+
queryAllByPlaceholderText("dwayne 'the rock' johnson", fuzzy),
243+
).toHaveLength(1)
244+
expect(queryAllByLabelText('User Name', fuzzy)).toHaveLength(1)
245+
expect(queryAllByLabelText('user name', fuzzy)).toHaveLength(1)
246+
expect(queryAllByLabelText('user', fuzzy)).toHaveLength(1)
247+
expect(queryAllByLabelText('User', fuzzy)).toHaveLength(1)
248+
expect(queryAllByText('Where to next?', fuzzy)).toHaveLength(1)
249+
expect(queryAllByText('Where to next', fuzzy)).toHaveLength(1)
250+
expect(queryAllByText('Where', fuzzy)).toHaveLength(1)
251+
expect(queryAllByText('where to next?', fuzzy)).toHaveLength(1)
252+
expect(queryAllByText('content with linebreaks', fuzzy)).toHaveLength(1)
253+
})
254+
147255
test('getAll* matchers return an array', () => {
148256
const {
149257
getAllByAltText,
@@ -171,11 +279,11 @@ test('getAll* matchers return an array', () => {
171279
</div>,
172280
`)
173281
expect(getAllByAltText(/finding.*poster$/i)).toHaveLength(2)
174-
expect(getAllByAltText('jumanji')).toHaveLength(1)
282+
expect(getAllByAltText(/jumanji/)).toHaveLength(1)
175283
expect(getAllByTestId('poster')).toHaveLength(3)
176284
expect(getAllByPlaceholderText(/The Rock/)).toHaveLength(1)
177285
expect(getAllByLabelText('User Name')).toHaveLength(1)
178-
expect(getAllByText('where')).toHaveLength(1)
286+
expect(getAllByText(/^where/i)).toHaveLength(1)
179287
})
180288

181289
test('getAll* matchers throw for 0 matches', () => {
@@ -188,8 +296,6 @@ test('getAll* matchers throw for 0 matches', () => {
188296
} = render(`
189297
<div>
190298
<label>No Matches Please</label>
191-
<div data-testid="ABC"></div>
192-
<div data-testid="a-b-c"></div>
193299
</div>,
194300
`)
195301
expect(() => getAllByTestId('nope')).toThrow()

src/__tests__/matches.js

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,36 @@
1-
import {matches, matchesExact} from '../'
1+
import {fuzzyMatches, matches} from '../'
22

33
// unit tests for text match utils
44

55
const node = null
66

77
test('matches should get fuzzy matches', () => {
88
// should not match
9-
expect(matchesExact(null, node, 'abc')).toBe(false)
10-
expect(matchesExact('', node, 'abc')).toBe(false)
9+
expect(matches(null, node, 'abc')).toBe(false)
10+
expect(matches('', node, 'abc')).toBe(false)
1111
// should match
12-
expect(matches('ABC', node, 'abc')).toBe(true)
13-
expect(matches('ABC', node, 'ABC')).toBe(true)
12+
expect(fuzzyMatches('ABC', node, 'abc')).toBe(true)
13+
expect(fuzzyMatches('ABC', node, 'ABC')).toBe(true)
1414
})
1515

1616
test('matchesExact should only get exact matches', () => {
1717
// should not match
18-
expect(matchesExact(null, node, null)).toBe(false)
19-
expect(matchesExact(null, node, 'abc')).toBe(false)
20-
expect(matchesExact('', node, 'abc')).toBe(false)
21-
expect(matchesExact('ABC', node, 'abc')).toBe(false)
22-
expect(matchesExact('ABC', node, 'A')).toBe(false)
23-
expect(matchesExact('ABC', node, 'ABCD')).toBe(false)
18+
expect(matches(null, node, null)).toBe(false)
19+
expect(matches(null, node, 'abc')).toBe(false)
20+
expect(matches('', node, 'abc')).toBe(false)
21+
expect(matches('ABC', node, 'abc')).toBe(false)
22+
expect(matches('ABC', node, 'A')).toBe(false)
23+
expect(matches('ABC', node, 'ABCD')).toBe(false)
24+
// should match
25+
expect(matches('ABC', node, 'ABC')).toBe(true)
26+
})
27+
28+
test('matchers should collapse whitespace if requested', () => {
2429
// should match
25-
expect(matchesExact('ABC', node, 'ABC')).toBe(true)
30+
expect(matches('ABC\n \t', node, 'ABC', true)).toBe(true)
31+
expect(matches('ABC\n \t', node, 'ABC', false)).toBe(false)
32+
expect(fuzzyMatches('ABC\n \t', node, 'ABC', true)).toBe(true)
33+
expect(fuzzyMatches(' ABC\n \t ', node, 'ABC', false)).toBe(true)
34+
expect(fuzzyMatches(' ABC\n \t ', node, /^ABC/, true)).toBe(true)
35+
expect(fuzzyMatches(' ABC\n \t ', node, /^ABC/, false)).toBe(false)
2636
})

src/__tests__/text-matchers.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,25 @@ cases(
99
`)
1010
expect(getByText(opts.textMatch).id).toBe('anchor')
1111
},
12+
[
13+
{name: 'string match', textMatch: 'About'},
14+
{name: 'regex', textMatch: /^about$/i},
15+
{
16+
name: 'function',
17+
textMatch: (text, element) =>
18+
element.tagName === 'A' && text.includes('out'),
19+
},
20+
],
21+
)
22+
23+
cases(
24+
'fuzzy text matchers',
25+
opts => {
26+
const {getByText} = render(`
27+
<a href="/about" id="anchor">About</a>
28+
`)
29+
expect(getByText(opts.textMatch, {exact: false}).id).toBe('anchor')
30+
},
1231
[
1332
{name: 'string match', textMatch: 'About'},
1433
{name: 'case insensitive', textMatch: 'about'},

src/matches.js

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
function matches(textToMatch, node, matcher) {
1+
function fuzzyMatches(textToMatch, node, matcher, collapseWhitespace = true) {
22
if (typeof textToMatch !== 'string') {
33
return false
44
}
5-
const normalizedText = textToMatch.trim().replace(/\s+/g, ' ')
5+
const normalizedText = collapseWhitespace
6+
? textToMatch.trim().replace(/\s+/g, ' ')
7+
: textToMatch
68
if (typeof matcher === 'string') {
79
return normalizedText.toLowerCase().includes(matcher.toLowerCase())
810
} else if (typeof matcher === 'function') {
@@ -12,17 +14,20 @@ function matches(textToMatch, node, matcher) {
1214
}
1315
}
1416

15-
function matchesExact(textToMatch, node, matcher) {
17+
function matches(textToMatch, node, matcher, collapseWhitespace = false) {
1618
if (typeof textToMatch !== 'string') {
1719
return false
1820
}
21+
const normalizedText = collapseWhitespace
22+
? textToMatch.trim().replace(/\s+/g, ' ')
23+
: textToMatch
1924
if (typeof matcher === 'string') {
20-
return textToMatch === matcher
25+
return normalizedText === matcher
2126
} else if (typeof matcher === 'function') {
22-
return matcher(textToMatch, node)
27+
return matcher(normalizedText, node)
2328
} else {
24-
return matcher.test(textToMatch)
29+
return matcher.test(normalizedText)
2530
}
2631
}
2732

28-
export {matches, matchesExact}
33+
export {fuzzyMatches, matches}

0 commit comments

Comments
 (0)