Skip to content

Commit cde0cdf

Browse files
alexkrolickKent C. Dodds
authored and
Kent C. Dodds
committed
feat(TextMatch): make fuzzy matching opt-in instead of default (testing-library#31)
- 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) BREAKING CHANGE: Strings are considered to be an exact match now. You can opt-into fuzzy matching, but it's recommended to use a regex instead.
1 parent 5fe849f commit cde0cdf

File tree

8 files changed

+423
-152
lines changed

8 files changed

+423
-152
lines changed

README.md

Lines changed: 140 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -72,19 +72,20 @@ when a real user uses it.
7272

7373
* [Installation](#installation)
7474
* [Usage](#usage)
75-
* [`getByLabelText(container: HTMLElement, text: TextMatch, options: {selector: string = '*'}): HTMLElement`](#getbylabeltextcontainer-htmlelement-text-textmatch-options-selector-string---htmlelement)
76-
* [`getByPlaceholderText(container: HTMLElement, text: TextMatch): HTMLElement`](#getbyplaceholdertextcontainer-htmlelement-text-textmatch-htmlelement)
77-
* [`getByText(container: HTMLElement, text: TextMatch): HTMLElement`](#getbytextcontainer-htmlelement-text-textmatch-htmlelement)
78-
* [`getByAltText(container: HTMLElement, text: TextMatch): HTMLElement`](#getbyalttextcontainer-htmlelement-text-textmatch-htmlelement)
79-
* [`getByTitle(container: HTMLElement, title: ExactTextMatch): HTMLElement`](#getbytitlecontainer-htmlelement-title-exacttextmatch-htmlelement)
80-
* [`getByTestId(container: HTMLElement, text: ExactTextMatch): HTMLElement`](#getbytestidcontainer-htmlelement-text-exacttextmatch-htmlelement)
75+
* [`getByLabelText`](#getbylabeltext)
76+
* [`getByPlaceholderText`](#getbyplaceholdertext)
77+
* [`getByText`](#getbytext)
78+
* [`getByAltText`](#getbyalttext)
79+
* [`getByTitle`](#getbytitle)
80+
* [`getByTestId`](#getbytestid)
8181
* [`wait`](#wait)
8282
* [`waitForElement`](#waitforelement)
83-
* [`fireEvent(node: HTMLElement, event: Event)`](#fireeventnode-htmlelement-event-event)
83+
* [`fireEvent`](#fireevent)
8484
* [Custom Jest Matchers](#custom-jest-matchers)
8585
* [Using other assertion libraries](#using-other-assertion-libraries)
8686
* [`TextMatch`](#textmatch)
87-
* [ExactTextMatch](#exacttextmatch)
87+
* [Precision](#precision)
88+
* [TextMatch Examples](#textmatch-examples)
8889
* [`query` APIs](#query-apis)
8990
* [`queryAll` and `getAll` APIs](#queryall-and-getall-apis)
9091
* [`bindElementToQueries`](#bindelementtoqueries)
@@ -110,7 +111,10 @@ npm install --save-dev dom-testing-library
110111

111112
## Usage
112113

113-
Note: each of the `get` APIs below have a matching [`getAll`](#queryall-and-getall-apis) API that returns all elements instead of just the first one, and [`query`](#query-apis)/[`getAll`](#queryall-and-getall-apis) that return `null`/`[]` instead of throwing an error.
114+
Note:
115+
116+
* Each of the `get` APIs below have a matching [`getAll`](#queryall-and-getall-apis) API that returns all elements instead of just the first one, and [`query`](#query-apis)/[`getAll`](#queryall-and-getall-apis) that return `null`/`[]` instead of throwing an error.
117+
* See [TextMatch](#textmatch) for details on the `exact`, `trim`, and `collapseWhitespace` options.
114118

115119
```javascript
116120
// src/__tests__/example.js
@@ -179,7 +183,19 @@ test('examples of some things', async () => {
179183
})
180184
```
181185

182-
### `getByLabelText(container: HTMLElement, text: TextMatch, options: {selector: string = '*'}): HTMLElement`
186+
### `getByLabelText`
187+
188+
```typescript
189+
getByLabelText(
190+
container: HTMLElement,
191+
text: TextMatch,
192+
options?: {
193+
selector?: string = '*',
194+
exact?: boolean = true,
195+
collapseWhitespace?: boolean = true,
196+
trim?: boolean = true,
197+
}): HTMLElement
198+
```
183199

184200
This will search for the label that matches the given [`TextMatch`](#textmatch),
185201
then find the element associated with that label.
@@ -214,7 +230,18 @@ const inputNode = getByLabelText(container, 'username', {selector: 'input'})
214230
> want this behavior (for example you wish to assert that it doesn't exist),
215231
> then use `queryByLabelText` instead.
216232
217-
### `getByPlaceholderText(container: HTMLElement, text: TextMatch): HTMLElement`
233+
### `getByPlaceholderText`
234+
235+
```typescript
236+
getByPlaceholderText(
237+
container: HTMLElement,
238+
text: TextMatch,
239+
options?: {
240+
exact?: boolean = true,
241+
collapseWhitespace?: boolean = false,
242+
trim?: boolean = true,
243+
}): HTMLElement
244+
```
218245

219246
This will search for all elements with a placeholder attribute and find one
220247
that matches the given [`TextMatch`](#textmatch).
@@ -227,7 +254,18 @@ const inputNode = getByPlaceholderText(container, 'Username')
227254
> NOTE: a placeholder is not a good substitute for a label so you should
228255
> generally use `getByLabelText` instead.
229256
230-
### `getByText(container: HTMLElement, text: TextMatch): HTMLElement`
257+
### `getByText`
258+
259+
```typescript
260+
getByText(
261+
container: HTMLElement,
262+
text: TextMatch,
263+
options?: {
264+
exact?: boolean = true,
265+
collapseWhitespace?: boolean = true,
266+
trim?: boolean = true,
267+
}): HTMLElement
268+
```
231269

232270
This will search for all elements that have a text node with `textContent`
233271
matching the given [`TextMatch`](#textmatch).
@@ -237,7 +275,18 @@ matching the given [`TextMatch`](#textmatch).
237275
const aboutAnchorNode = getByText(container, 'about')
238276
```
239277

240-
### `getByAltText(container: HTMLElement, text: TextMatch): HTMLElement`
278+
### `getByAltText`
279+
280+
```typescript
281+
getByAltText(
282+
container: HTMLElement,
283+
text: TextMatch,
284+
options?: {
285+
exact?: boolean = true,
286+
collapseWhitespace?: boolean = false,
287+
trim?: boolean = true,
288+
}): HTMLElement
289+
```
241290

242291
This will return the element (normally an `<img>`) that has the given `alt`
243292
text. Note that it only supports elements which accept an `alt` attribute:
@@ -251,19 +300,41 @@ and [`<area>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/area)
251300
const incrediblesPosterImg = getByAltText(container, /incredibles.*poster$/i)
252301
```
253302

254-
### `getByTitle(container: HTMLElement, title: ExactTextMatch): HTMLElement`
303+
### `getByTitle`
255304

256-
This will return the element that has the matching `title` attribute.
305+
```typescript
306+
getByTitle(
307+
container: HTMLElement,
308+
title: TextMatch,
309+
options?: {
310+
exact?: boolean = true,
311+
collapseWhitespace?: boolean = false,
312+
trim?: boolean = true,
313+
}): HTMLElement
314+
```
315+
316+
Returns the element that has the matching `title` attribute.
257317

258318
```javascript
259319
// <span title="Delete" id="2" />
260320
const deleteElement = getByTitle(container, 'Delete')
261321
```
262322

263-
### `getByTestId(container: HTMLElement, text: ExactTextMatch): HTMLElement`
323+
### `getByTestId`
324+
325+
```typescript
326+
getByTestId(
327+
container: HTMLElement,
328+
text: TextMatch,
329+
options?: {
330+
exact?: boolean = true,
331+
collapseWhitespace?: boolean = false,
332+
trim?: boolean = true,
333+
}): HTMLElement`
334+
```
264335

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

268339
```javascript
269340
// <input data-testid="username-input" />
@@ -280,8 +351,6 @@ const usernameInputElement = getByTestId(container, 'username-input')
280351

281352
### `wait`
282353

283-
Defined as:
284-
285354
```typescript
286355
function wait(
287356
callback?: () => void,
@@ -323,8 +392,6 @@ intervals.
323392

324393
### `waitForElement`
325394

326-
Defined as:
327-
328395
```typescript
329396
function waitForElement<T>(
330397
callback?: () => T | null | undefined,
@@ -383,7 +450,11 @@ The default `timeout` is `4500ms` which will keep you under
383450
additions and removals of child elements (including text nodes) in the `container` and any of its descendants.
384451
It won't detect attribute changes unless you add `attributes: true` to the options.
385452

386-
### `fireEvent(node: HTMLElement, event: Event)`
453+
### `fireEvent`
454+
455+
```typescript
456+
fireEvent(node: HTMLElement, event: Event)
457+
```
387458

388459
Fire DOM events.
389460

@@ -398,7 +469,11 @@ fireEvent(
398469
)
399470
```
400471

401-
#### `fireEvent[eventName](node: HTMLElement, eventProperties: Object)`
472+
#### `fireEvent[eventName]`
473+
474+
```typescript
475+
fireEvent[eventName](node: HTMLElement, eventProperties: Object)
476+
```
402477

403478
Convenience methods for firing DOM events. Check out
404479
[src/events.js](https://github.com/kentcdodds/dom-testing-library/blob/master/src/events.js)
@@ -411,7 +486,11 @@ fireEvent.click(getElementByText('Submit'), rightClick)
411486
// default `button` property for click events is set to `0` which is a left click.
412487
```
413488

414-
#### `getNodeText(node: HTMLElement)`
489+
#### `getNodeText`
490+
491+
```typescript
492+
getNodeText(node: HTMLElement)
493+
```
415494

416495
Returns the complete text content of a html element, removing any extra
417496
whitespace. The intention is to treat text in nodes exactly as how it is
@@ -469,43 +548,50 @@ and add it here!
469548
Several APIs accept a `TextMatch` which can be a `string`, `regex` or a
470549
`function` which returns `true` for a match and `false` for a mismatch.
471550

472-
Here's an example
551+
### Precision
552+
553+
Some APIs accept an object as the final argument that can contain options that
554+
affect the precision of string matching:
555+
556+
* `exact`: Defaults to `true`; matches full strings, case-sensitive. When false,
557+
matches substrings and is not case-sensitive.
558+
* `exact` has no effect on `regex` or `function` arguments.
559+
* In most cases using a regex instead of a string gives you more control over
560+
fuzzy matching and should be preferred over `{ exact: false }`.
561+
* `trim`: Defaults to `true`; trim leading and trailing whitespace.
562+
* `collapseWhitespace`: Defaults to `true`. Collapses inner whitespace (newlines, tabs, repeated spaces) into a single space.
563+
564+
### TextMatch Examples
473565

474566
```javascript
475-
// <div>Hello World</div>
476-
// all of the following will find the div
477-
getByText(container, 'Hello World') // full match
478-
getByText(container, 'llo worl') // substring match
479-
getByText(container, 'hello world') // strings ignore case
480-
getByText(container, /Hello W?oRlD/i) // regex
481-
getByText(container, (content, element) => content.startsWith('Hello')) // function
482-
483-
// all of the following will NOT find the div
484-
getByText(container, 'Goodbye World') // non-string match
485-
getByText(container, /hello world/) // case-sensitive regex with different case
486-
// function looking for a span when it's actually a div
487-
getByText(container, (content, element) => {
488-
return element.tagName.toLowerCase() === 'span' && content.startsWith('Hello')
489-
})
490-
```
567+
// <div>
568+
// Hello World
569+
// </div>
570+
571+
// WILL find the div:
491572
492-
### ExactTextMatch
573+
// Matching a string:
574+
getByText(container, 'Hello World') // full string match
575+
getByText(container, 'llo Worl'), {exact: false} // substring match
576+
getByText(container, 'hello world', {exact: false}) // ignore case
493577
494-
Some APIs use ExactTextMatch, which is the same as TextMatch but case-sensitive
495-
and does not match substrings; however, regexes and functions are also accepted
496-
for custom matching.
578+
// Matching a regex:
579+
getByText(container, /World/) // substring match
580+
getByText(container, /world/i) // substring match, ignore case
581+
getByText(container, /^hello world$/i) // full string match, ignore case
582+
getByText(container, /Hello W?oRlD/i) // advanced regex
497583
498-
```js
499-
// <button data-testid="submit-button">Go</button>
584+
// Matching with a custom function:
585+
getByText(container, (content, element) => content.startsWith('Hello'))
500586
501-
// all of the following will find the button
502-
getByTestId(container, 'submit-button') // exact match
503-
getByTestId(container, /submit*/) // regex match
504-
getByTestId(container, content => content.startsWith('submit')) // function
587+
// WILL NOT find the div:
505588
506-
// all of the following will NOT find the button
507-
getByTestId(container, 'submit-') // no substrings
508-
getByTestId(container, 'Submit-Button') // case-sensitive
589+
getByText(container, 'Goodbye World') // full string does not match
590+
getByText(container, /hello world/) // case-sensitive regex with different case
591+
// function looking for a span when it's actually a div:
592+
getByText(container, (content, element) => {
593+
return element.tagName.toLowerCase() === 'span' && content.startsWith('Hello')
594+
})
509595
```
510596

511597
## `query` APIs

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: 5 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', () => {
@@ -171,11 +171,11 @@ test('getAll* matchers return an array', () => {
171171
</div>,
172172
`)
173173
expect(getAllByAltText(/finding.*poster$/i)).toHaveLength(2)
174-
expect(getAllByAltText('jumanji')).toHaveLength(1)
174+
expect(getAllByAltText(/jumanji/)).toHaveLength(1)
175175
expect(getAllByTestId('poster')).toHaveLength(3)
176176
expect(getAllByPlaceholderText(/The Rock/)).toHaveLength(1)
177177
expect(getAllByLabelText('User Name')).toHaveLength(1)
178-
expect(getAllByText('where')).toHaveLength(1)
178+
expect(getAllByText(/^where/i)).toHaveLength(1)
179179
})
180180

181181
test('getAll* matchers throw for 0 matches', () => {
@@ -188,8 +188,6 @@ test('getAll* matchers throw for 0 matches', () => {
188188
} = render(`
189189
<div>
190190
<label>No Matches Please</label>
191-
<div data-testid="ABC"></div>
192-
<div data-testid="a-b-c"></div>
193191
</div>,
194192
`)
195193
expect(() => getAllByTestId('nope')).toThrow()

0 commit comments

Comments
 (0)