Skip to content

fix: use exact match for data-testid #29

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

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,17 @@ function matches(textToMatch, node, matcher) {
}
}

export {matches}
function matchesExact(textToMatch, node, matcher) {
if (typeof textToMatch !== 'string') {
return false
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the only line not exercised by public api tests because the exact match is already pre-filtered with querySelectorAll in queryAllByAttribute. Added unit tests for the matchers instead to document match behavior.

}
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
Original file line number Diff line number Diff line change
@@ -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