Skip to content

Commit 7bab309

Browse files
alexkrolickKent C. Dodds
authored and
Kent C. Dodds
committed
fix: use exact match for data-testid (#29)
* fix: use exact match for data-testid [fixes #8] * add tests for exact matches * disallow substring match * fix: heading level for getByTestId * Add docs for ExactTextMatch * Update README.md
1 parent 6e0c752 commit 7bab309

File tree

5 files changed

+88
-10
lines changed

5 files changed

+88
-10
lines changed

README.md

+23-2
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,14 @@ when a real user uses it.
7676
* [`getByPlaceholderText(container: HTMLElement, text: TextMatch): HTMLElement`](#getbyplaceholdertextcontainer-htmlelement-text-textmatch-htmlelement)
7777
* [`getByText(container: HTMLElement, text: TextMatch): HTMLElement`](#getbytextcontainer-htmlelement-text-textmatch-htmlelement)
7878
* [`getByAltText(container: HTMLElement, text: TextMatch): HTMLElement`](#getbyalttextcontainer-htmlelement-text-textmatch-htmlelement)
79+
* [`getByTestId(container: HTMLElement, text: ExactTextMatch): HTMLElement`](#getbytestidcontainer-htmlelement-text-exacttextmatch-htmlelement)
7980
* [`wait`](#wait)
8081
* [`waitForElement`](#waitforelement)
8182
* [`fireEvent(node: HTMLElement, event: Event)`](#fireeventnode-htmlelement-event-event)
8283
* [Custom Jest Matchers](#custom-jest-matchers)
8384
* [Using other assertion libraries](#using-other-assertion-libraries)
8485
* [`TextMatch`](#textmatch)
86+
* [ExactTextMatch](#exacttextmatch)
8587
* [`query` APIs](#query-apis)
8688
* [`queryAll` and `getAll` APIs](#queryall-and-getall-apis)
8789
* [`bindElementToQueries`](#bindelementtoqueries)
@@ -248,10 +250,10 @@ and [`<area>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/area)
248250
const incrediblesPosterImg = getByAltText(container, /incredibles.*poster$/i)
249251
```
250252

251-
#### `getByTestId(container: HTMLElement, text: TextMatch): HTMLElement`
253+
### `getByTestId(container: HTMLElement, text: ExactTextMatch): HTMLElement`
252254

253255
A shortcut to `` container.querySelector(`[data-testid="${yourId}"]`) `` (and it
254-
also accepts a [`TextMatch`](#textmatch)).
256+
also accepts an [`ExactTextMatch`](#exacttextmatch)).
255257

256258
```javascript
257259
// <input data-testid="username-input" />
@@ -477,6 +479,25 @@ getByText(container, (content, element) => {
477479
})
478480
```
479481

482+
### ExactTextMatch
483+
484+
Some APIs use ExactTextMatch, which is the same as TextMatch but case-sensitive
485+
and does not match substrings; however, regexes and functions are also accepted
486+
for custom matching.
487+
488+
```js
489+
// <button data-testid="submit-button">Go</button>
490+
491+
// all of the following will find the button
492+
getByTestId(container, 'submit-button') // exact match
493+
getByTestId(container, /submit*/) // regex match
494+
getByTestId(container, content => content.startsWith('submit')) // function
495+
496+
// all of the following will NOT find the button
497+
getByTestId(container, 'submit-') // no substrings
498+
getByTestId(container, 'Submit-Button') // case-sensitive
499+
```
500+
480501
## `query` APIs
481502

482503
Each of the `get` APIs listed in [the 'Usage'](#usage) section above have a

src/__tests__/element-queries.js

+15
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,18 @@ test('get element by its alt text', () => {
117117
expect(getByAltText(/fin.*nem.*poster$/i).src).toBe('/finding-nemo.png')
118118
})
119119

120+
test('can get elements by data-testid attribute', () => {
121+
const {queryByTestId} = render(`<div data-testid="firstName"></div>`)
122+
expect(queryByTestId('firstName')).toBeInTheDOM()
123+
expect(queryByTestId(/first/)).toBeInTheDOM()
124+
expect(queryByTestId(testid => testid === 'firstName')).toBeInTheDOM()
125+
// match should be exact, case-sensitive
126+
expect(queryByTestId('firstname')).not.toBeInTheDOM()
127+
expect(queryByTestId('first')).not.toBeInTheDOM()
128+
expect(queryByTestId('firstNamePlusMore')).not.toBeInTheDOM()
129+
expect(queryByTestId('first-name')).not.toBeInTheDOM()
130+
})
131+
120132
test('getAll* matchers return an array', () => {
121133
const {
122134
getAllByAltText,
@@ -161,9 +173,12 @@ test('getAll* matchers throw for 0 matches', () => {
161173
} = render(`
162174
<div>
163175
<label>No Matches Please</label>
176+
<div data-testid="ABC"></div>
177+
<div data-testid="a-b-c"></div>
164178
</div>,
165179
`)
166180
expect(() => getAllByTestId('nope')).toThrow()
181+
expect(() => getAllByTestId('abc')).toThrow()
167182
expect(() => getAllByAltText('nope')).toThrow()
168183
expect(() => getAllByLabelText('nope')).toThrow()
169184
expect(() => getAllByLabelText('no matches please')).toThrow()

src/__tests__/matches.js

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import {matches, matchesExact} from '../'
2+
3+
// unit tests for text match utils
4+
5+
const node = null
6+
7+
test('matches should get fuzzy matches', () => {
8+
// should not match
9+
expect(matchesExact(null, node, 'abc')).toBe(false)
10+
expect(matchesExact('', node, 'abc')).toBe(false)
11+
// should match
12+
expect(matches('ABC', node, 'abc')).toBe(true)
13+
expect(matches('ABC', node, 'ABC')).toBe(true)
14+
})
15+
16+
test('matchesExact should only get exact matches', () => {
17+
// 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)
24+
// should match
25+
expect(matchesExact('ABC', node, 'ABC')).toBe(true)
26+
})

src/matches.js

+14-1
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,17 @@ function matches(textToMatch, node, matcher) {
1212
}
1313
}
1414

15-
export {matches}
15+
function matchesExact(textToMatch, node, matcher) {
16+
if (typeof textToMatch !== 'string') {
17+
return false
18+
}
19+
if (typeof matcher === 'string') {
20+
return textToMatch === matcher
21+
} else if (typeof matcher === 'function') {
22+
return matcher(textToMatch, node)
23+
} else {
24+
return matcher.test(textToMatch)
25+
}
26+
}
27+
28+
export {matches, matchesExact}

src/queries.js

+10-7
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {matches} from './matches'
1+
import {matches, matchesExact} from './matches'
22
import {getNodeText} from './get-node-text'
33
import {prettyDOM} from './pretty-dom'
44

@@ -70,22 +70,25 @@ function queryByText(container, text, opts) {
7070

7171
// this is just a utility and not an exposed query.
7272
// There are no plans to expose this.
73-
function queryAllByAttribute(attribute, container, text) {
73+
function queryAllByAttribute(attribute, container, text, {exact = false} = {}) {
74+
const matcher = exact ? matchesExact : matches
7475
return Array.from(container.querySelectorAll(`[${attribute}]`)).filter(node =>
75-
matches(node.getAttribute(attribute), node, text),
76+
matcher(node.getAttribute(attribute), node, text),
7677
)
7778
}
7879

7980
// this is just a utility and not an exposed query.
8081
// There are no plans to expose this.
81-
function queryByAttribute(attribute, container, text) {
82-
return firstResultOrNull(queryAllByAttribute, attribute, container, text)
82+
function queryByAttribute(...args) {
83+
return firstResultOrNull(queryAllByAttribute, ...args)
8384
}
8485

8586
const queryByPlaceholderText = queryByAttribute.bind(null, 'placeholder')
8687
const queryAllByPlaceholderText = queryAllByAttribute.bind(null, 'placeholder')
87-
const queryByTestId = queryByAttribute.bind(null, 'data-testid')
88-
const queryAllByTestId = queryAllByAttribute.bind(null, 'data-testid')
88+
const queryByTestId = (...args) =>
89+
queryByAttribute('data-testid', ...args, {exact: true})
90+
const queryAllByTestId = (...args) =>
91+
queryAllByAttribute('data-testid', ...args, {exact: true})
8992

9093
function queryAllByAltText(container, alt) {
9194
return Array.from(container.querySelectorAll('img,input,area')).filter(node =>

0 commit comments

Comments
 (0)