Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make fuzzy matching opt-in instead of default #31

Merged
merged 10 commits into from
May 6, 2018
194 changes: 140 additions & 54 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,19 +72,20 @@ when a real user uses it.

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

## Usage

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.
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.
* See [TextMatch](#textmatch) for details on the `exact`, `trim`, and `collapseWhitespace` options.

```javascript
// src/__tests__/example.js
Expand Down Expand Up @@ -179,7 +183,19 @@ test('examples of some things', async () => {
})
```

### `getByLabelText(container: HTMLElement, text: TextMatch, options: {selector: string = '*'}): HTMLElement`
### `getByLabelText`

```typescript
getByLabelText(
container: HTMLElement,
text: TextMatch,
options?: {
selector?: string = '*',
exact?: boolean = true,
collapseWhitespace?: boolean = true,
trim?: boolean = true,
}): HTMLElement
```

This will search for the label that matches the given [`TextMatch`](#textmatch),
then find the element associated with that label.
Expand Down Expand Up @@ -214,7 +230,18 @@ const inputNode = getByLabelText(container, 'username', {selector: 'input'})
> want this behavior (for example you wish to assert that it doesn't exist),
> then use `queryByLabelText` instead.

### `getByPlaceholderText(container: HTMLElement, text: TextMatch): HTMLElement`
### `getByPlaceholderText`

```typescript
getByPlaceholderText(
container: HTMLElement,
text: TextMatch,
options?: {
exact?: boolean = true,
collapseWhitespace?: boolean = false,
trim?: boolean = true,
}): HTMLElement
```

This will search for all elements with a placeholder attribute and find one
that matches the given [`TextMatch`](#textmatch).
Expand All @@ -227,7 +254,18 @@ const inputNode = getByPlaceholderText(container, 'Username')
> NOTE: a placeholder is not a good substitute for a label so you should
> generally use `getByLabelText` instead.

### `getByText(container: HTMLElement, text: TextMatch): HTMLElement`
### `getByText`

```typescript
getByText(
container: HTMLElement,
text: TextMatch,
options?: {
exact?: boolean = true,
collapseWhitespace?: boolean = true,
trim?: boolean = true,
}): HTMLElement
```

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

### `getByAltText(container: HTMLElement, text: TextMatch): HTMLElement`
### `getByAltText`

```typescript
getByAltText(
container: HTMLElement,
text: TextMatch,
options?: {
exact?: boolean = true,
collapseWhitespace?: boolean = false,
trim?: boolean = true,
}): HTMLElement
```

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

### `getByTitle(container: HTMLElement, title: ExactTextMatch): HTMLElement`
### `getByTitle`

This will return the element that has the matching `title` attribute.
```typescript
getByTitle(
container: HTMLElement,
title: TextMatch,
options?: {
exact?: boolean = true,
collapseWhitespace?: boolean = false,
trim?: boolean = true,
}): HTMLElement
```

Returns the element that has the matching `title` attribute.

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

### `getByTestId(container: HTMLElement, text: ExactTextMatch): HTMLElement`
### `getByTestId`

```typescript
getByTestId(
container: HTMLElement,
text: TextMatch,
options?: {
exact?: boolean = true,
collapseWhitespace?: boolean = false,
trim?: boolean = true,
}): HTMLElement`
```

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

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

### `wait`

Defined as:

```typescript
function wait(
callback?: () => void,
Expand Down Expand Up @@ -323,8 +392,6 @@ intervals.

### `waitForElement`

Defined as:

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

### `fireEvent(node: HTMLElement, event: Event)`
### `fireEvent`

```typescript
fireEvent(node: HTMLElement, event: Event)
```

Fire DOM events.

Expand All @@ -398,7 +469,11 @@ fireEvent(
)
```

#### `fireEvent[eventName](node: HTMLElement, eventProperties: Object)`
#### `fireEvent[eventName]`

```typescript
fireEvent[eventName](node: HTMLElement, eventProperties: Object)
```

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

#### `getNodeText(node: HTMLElement)`
#### `getNodeText`

```typescript
getNodeText(node: HTMLElement)
```

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

Here's an example
### Precision

Some APIs accept an object as the final argument that can contain options that
affect the precision of string matching:

* `exact`: Defaults to `true`; matches full strings, case-sensitive. When false,
matches substrings and is not case-sensitive.
* `exact` has no effect on `regex` or `function` arguments.
* In most cases using a regex instead of a string gives you more control over
fuzzy matching and should be preferred over `{ exact: false }`.
* `trim`: Defaults to `true`; trim leading and trailing whitespace.
* `collapseWhitespace`: Defaults to `true`. Collapses inner whitespace (newlines, tabs, repeated spaces) into a single space.

### TextMatch Examples

```javascript
// <div>Hello World</div>
// all of the following will find the div
getByText(container, 'Hello World') // full match
getByText(container, 'llo worl') // substring match
getByText(container, 'hello world') // strings ignore case
getByText(container, /Hello W?oRlD/i) // regex
getByText(container, (content, element) => content.startsWith('Hello')) // function

// all of the following will NOT find the div
getByText(container, 'Goodbye World') // non-string match
getByText(container, /hello world/) // case-sensitive regex with different case
// function looking for a span when it's actually a div
getByText(container, (content, element) => {
return element.tagName.toLowerCase() === 'span' && content.startsWith('Hello')
})
```
// <div>
// Hello World
// </div>

// WILL find the div:

### ExactTextMatch
// Matching a string:
getByText(container, 'Hello World') // full string match
getByText(container, 'llo Worl'), {exact: false} // substring match
getByText(container, 'hello world', {exact: false}) // ignore case

Some APIs use ExactTextMatch, which is the same as TextMatch but case-sensitive
and does not match substrings; however, regexes and functions are also accepted
for custom matching.
// Matching a regex:
getByText(container, /World/) // substring match
getByText(container, /world/i) // substring match, ignore case
getByText(container, /^hello world$/i) // full string match, ignore case
getByText(container, /Hello W?oRlD/i) // advanced regex

```js
// <button data-testid="submit-button">Go</button>
// Matching with a custom function:
getByText(container, (content, element) => content.startsWith('Hello'))

// all of the following will find the button
getByTestId(container, 'submit-button') // exact match
getByTestId(container, /submit*/) // regex match
getByTestId(container, content => content.startsWith('submit')) // function
// WILL NOT find the div:

// all of the following will NOT find the button
getByTestId(container, 'submit-') // no substrings
getByTestId(container, 'Submit-Button') // case-sensitive
getByText(container, 'Goodbye World') // full string does not match
getByText(container, /hello world/) // case-sensitive regex with different case
// function looking for a span when it's actually a div:
getByText(container, (content, element) => {
return element.tagName.toLowerCase() === 'span' && content.startsWith('Hello')
})
```

## `query` APIs
Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/__snapshots__/element-queries.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ exports[`get throws a useful error message 6`] = `
`;

exports[`label with no form control 1`] = `
"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.
"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.

<div>
<label>
Expand Down
12 changes: 5 additions & 7 deletions src/__tests__/element-queries.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,8 @@ test('get can get form controls by placeholder', () => {

test('label with no form control', () => {
const {getByLabelText, queryByLabelText} = render(`<label>All alone</label>`)
expect(queryByLabelText('alone')).toBeNull()
expect(() => getByLabelText('alone')).toThrowErrorMatchingSnapshot()
expect(queryByLabelText(/alone/)).toBeNull()
expect(() => getByLabelText(/alone/)).toThrowErrorMatchingSnapshot()
})

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

test('get element by its alt text', () => {
Expand Down Expand Up @@ -171,11 +171,11 @@ test('getAll* matchers return an array', () => {
</div>,
`)
expect(getAllByAltText(/finding.*poster$/i)).toHaveLength(2)
expect(getAllByAltText('jumanji')).toHaveLength(1)
expect(getAllByAltText(/jumanji/)).toHaveLength(1)
expect(getAllByTestId('poster')).toHaveLength(3)
expect(getAllByPlaceholderText(/The Rock/)).toHaveLength(1)
expect(getAllByLabelText('User Name')).toHaveLength(1)
expect(getAllByText('where')).toHaveLength(1)
expect(getAllByText(/^where/i)).toHaveLength(1)
})

test('getAll* matchers throw for 0 matches', () => {
Expand All @@ -188,8 +188,6 @@ test('getAll* matchers throw for 0 matches', () => {
} = render(`
<div>
<label>No Matches Please</label>
<div data-testid="ABC"></div>
<div data-testid="a-b-c"></div>
</div>,
`)
expect(() => getAllByTestId('nope')).toThrow()
Expand Down