Skip to content

Commit

Permalink
fix: use exact match for data-testid (#29)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
alexkrolick authored and Kent C. Dodds committed May 4, 2018
1 parent 6e0c752 commit 7bab309
Show file tree
Hide file tree
Showing 5 changed files with 88 additions and 10 deletions.
25 changes: 23 additions & 2 deletions README.md
Expand Up @@ -76,12 +76,14 @@ when a real user uses it.
* [`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)
* [`getByTestId(container: HTMLElement, text: ExactTextMatch): HTMLElement`](#getbytestidcontainer-htmlelement-text-exacttextmatch-htmlelement)
* [`wait`](#wait)
* [`waitForElement`](#waitforelement)
* [`fireEvent(node: HTMLElement, event: Event)`](#fireeventnode-htmlelement-event-event)
* [Custom Jest Matchers](#custom-jest-matchers)
* [Using other assertion libraries](#using-other-assertion-libraries)
* [`TextMatch`](#textmatch)
* [ExactTextMatch](#exacttextmatch)
* [`query` APIs](#query-apis)
* [`queryAll` and `getAll` APIs](#queryall-and-getall-apis)
* [`bindElementToQueries`](#bindelementtoqueries)
Expand Down Expand Up @@ -248,10 +250,10 @@ and [`<area>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/area)
const incrediblesPosterImg = getByAltText(container, /incredibles.*poster$/i)
```

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

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

```javascript
// <input data-testid="username-input" />
Expand Down Expand Up @@ -477,6 +479,25 @@ getByText(container, (content, element) => {
})
```

### ExactTextMatch

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.

```js
// <button data-testid="submit-button">Go</button>
// 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
// all of the following will NOT find the button
getByTestId(container, 'submit-') // no substrings
getByTestId(container, 'Submit-Button') // case-sensitive
```

## `query` APIs

Each of the `get` APIs listed in [the 'Usage'](#usage) section above have a
Expand Down
15 changes: 15 additions & 0 deletions src/__tests__/element-queries.js
Expand Up @@ -117,6 +117,18 @@ test('get element by its alt text', () => {
expect(getByAltText(/fin.*nem.*poster$/i).src).toBe('/finding-nemo.png')
})

test('can get elements by data-testid attribute', () => {
const {queryByTestId} = render(`<div data-testid="firstName"></div>`)
expect(queryByTestId('firstName')).toBeInTheDOM()
expect(queryByTestId(/first/)).toBeInTheDOM()
expect(queryByTestId(testid => testid === 'firstName')).toBeInTheDOM()
// match should be exact, case-sensitive
expect(queryByTestId('firstname')).not.toBeInTheDOM()
expect(queryByTestId('first')).not.toBeInTheDOM()
expect(queryByTestId('firstNamePlusMore')).not.toBeInTheDOM()
expect(queryByTestId('first-name')).not.toBeInTheDOM()
})

test('getAll* matchers return an array', () => {
const {
getAllByAltText,
Expand Down Expand Up @@ -161,9 +173,12 @@ 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()
expect(() => getAllByTestId('abc')).toThrow()
expect(() => getAllByAltText('nope')).toThrow()
expect(() => getAllByLabelText('nope')).toThrow()
expect(() => getAllByLabelText('no matches please')).toThrow()
Expand Down
26 changes: 26 additions & 0 deletions src/__tests__/matches.js
@@ -0,0 +1,26 @@
import {matches, matchesExact} from '../'

// unit tests for text match utils

const node = null

test('matches should get fuzzy matches', () => {
// should not match
expect(matchesExact(null, node, 'abc')).toBe(false)
expect(matchesExact('', node, 'abc')).toBe(false)
// should match
expect(matches('ABC', node, 'abc')).toBe(true)
expect(matches('ABC', node, 'ABC')).toBe(true)
})

test('matchesExact should only get exact matches', () => {
// should not match
expect(matchesExact(null, node, null)).toBe(false)
expect(matchesExact(null, node, 'abc')).toBe(false)
expect(matchesExact('', node, 'abc')).toBe(false)
expect(matchesExact('ABC', node, 'abc')).toBe(false)
expect(matchesExact('ABC', node, 'A')).toBe(false)
expect(matchesExact('ABC', node, 'ABCD')).toBe(false)
// should match
expect(matchesExact('ABC', node, 'ABC')).toBe(true)
})
15 changes: 14 additions & 1 deletion src/matches.js
Expand Up @@ -12,4 +12,17 @@ function matches(textToMatch, node, matcher) {
}
}

export {matches}
function matchesExact(textToMatch, node, matcher) {
if (typeof textToMatch !== 'string') {
return false
}
if (typeof matcher === 'string') {
return textToMatch === matcher
} else if (typeof matcher === 'function') {
return matcher(textToMatch, node)
} else {
return matcher.test(textToMatch)
}
}

export {matches, matchesExact}
17 changes: 10 additions & 7 deletions src/queries.js
@@ -1,4 +1,4 @@
import {matches} from './matches'
import {matches, matchesExact} from './matches'
import {getNodeText} from './get-node-text'
import {prettyDOM} from './pretty-dom'

Expand Down Expand Up @@ -70,22 +70,25 @@ function queryByText(container, text, opts) {

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

// this is just a utility and not an exposed query.
// There are no plans to expose this.
function queryByAttribute(attribute, container, text) {
return firstResultOrNull(queryAllByAttribute, attribute, container, text)
function queryByAttribute(...args) {
return firstResultOrNull(queryAllByAttribute, ...args)
}

const queryByPlaceholderText = queryByAttribute.bind(null, 'placeholder')
const queryAllByPlaceholderText = queryAllByAttribute.bind(null, 'placeholder')
const queryByTestId = queryByAttribute.bind(null, 'data-testid')
const queryAllByTestId = queryAllByAttribute.bind(null, 'data-testid')
const queryByTestId = (...args) =>
queryByAttribute('data-testid', ...args, {exact: true})
const queryAllByTestId = (...args) =>
queryAllByAttribute('data-testid', ...args, {exact: true})

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

0 comments on commit 7bab309

Please sign in to comment.