Skip to content

feat: Add getByTestId utility #10

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
merged 3 commits into from
Mar 21, 2018
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
12 changes: 12 additions & 0 deletions .all-contributorsrc
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,18 @@
"contributions": [
"doc"
]
},
{
"login": "pbomb",
"name": "Matt Parrish",
"avatar_url": "https://avatars0.githubusercontent.com/u/1402095?v=4",
"profile": "https://github.com/pbomb",
"contributions": [
"bug",
"code",
"doc",
"test"
]
}
]
}
66 changes: 46 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
[![downloads][downloads-badge]][npmtrends]
[![MIT License][license-badge]][license]

[![All Contributors](https://img.shields.io/badge/all_contributors-5-orange.svg?style=flat-square)](#contributors)
[![All Contributors](https://img.shields.io/badge/all_contributors-6-orange.svg?style=flat-square)](#contributors)
[![PRs Welcome][prs-badge]][prs]
[![Code of Conduct][coc-badge]][coc]

Expand Down Expand Up @@ -86,18 +86,18 @@ test('Fetch makes an API call and displays the greeting when load-greeting is cl
}),
)
const url = '/greeting'
const {queryByTestId, container} = render(<Fetch url={url} />)
const {getByTestId, container} = render(<Fetch url={url} />)

// Act
Simulate.click(queryByTestId('load-greeting'))
Simulate.click(getByTestId('load-greeting'))

// let's wait for our mocked `get` request promise to resolve
await flushPromises()

// Assert
expect(axiosMock.get).toHaveBeenCalledTimes(1)
expect(axiosMock.get).toHaveBeenCalledWith(url)
expect(queryByTestId('greeting-text').textContent).toBe('hello there')
expect(getByTestId('greeting-text').textContent).toBe('hello there')
expect(container.firstChild).toMatchSnapshot()
})
```
Expand Down Expand Up @@ -146,18 +146,34 @@ unmount()
// your component has been unmounted and now: container.innerHTML === ''
```

#### `getByTestId`

A shortcut to `` container.querySelector(`[data-testid="${yourId}"]`) `` except
that it will throw an Error if no matching element is found. Read more about
`data-testid`s below.

```javascript
const usernameInputElement = getByTestId('username-input')
usernameInputElement.value = 'new value'
Simulate.change(usernameInputElement)
```

#### `queryByTestId`

A shortcut to `` container.querySelector(`[data-testid="${yourId}"]`) ``. Read
more about `data-testid`s below.
A shortcut to `` container.querySelector(`[data-testid="${yourId}"]`) ``
(Note: just like `querySelector`, this could return null if no matching element
is found, which may lead to harder-to-understand error messages). Read more about
`data-testid`s below.

```javascript
const usernameInputElement = queryByTestId('username-input')
// assert something doesn't exist
// (you couldn't do this with `getByTestId`)
expect(queryByTestId('username-input')).toBeNull()
```

## More on `data-testid`s

The `queryByTestId` utility is referring to the practice of using `data-testid`
The `getByTestId` and `queryByTestId` utilities refer to the practice of using `data-testid`
attributes to identify individual elements in your rendered component. This is
one of the practices this library is intended to encourage.

Expand Down Expand Up @@ -186,14 +202,14 @@ prefer to update the props of a rendered component in your test, the easiest
way to do that is:

```javascript
const {container, queryByTestId} = render(<NumberDisplay number={1} />)
expect(queryByTestId('number-display').textContent).toBe('1')
const {container, getByTestId} = render(<NumberDisplay number={1} />)
expect(getByTestId('number-display').textContent).toBe('1')

// re-render the same component with different props
// but pass the same container in the options argument.
// which will cause a re-render of the same instance (normal React behavior).
render(<NumberDisplay number={2} />, {container})
expect(queryByTestId('number-display').textContent).toBe('2')
expect(getByTestId('number-display').textContent).toBe('2')
```

[Open the tests](https://github.com/kentcdodds/react-testing-library/blob/master/src/__tests__/number-display.js)
Expand All @@ -219,14 +235,16 @@ jest.mock('react-transition-group', () => {
})

test('you can mock things with jest.mock', () => {
const {queryByTestId} = render(<HiddenMessage initialShow={true} />)
const {getByTestId, queryByTestId} = render(
<HiddenMessage initialShow={true} />,
)
expect(queryByTestId('hidden-message')).toBeTruthy() // we just care it exists
// hide the message
Simulate.click(queryByTestId('toggle-message'))
Simulate.click(getByTestId('toggle-message'))
// in the real world, the CSSTransition component would take some time
// before finishing the animation which would actually hide the message.
// So we've mocked it out for our tests to make it happen instantly
expect(queryByTestId('hidden-message')).toBeFalsy() // we just care it doesn't exist
expect(queryByTestId('hidden-message')).toBeNull() // we just care it doesn't exist
})
```

Expand All @@ -247,6 +265,14 @@ something more
Learn more about how Jest mocks work from my blog post:
["But really, what is a JavaScript mock?"](https://blog.kentcdodds.com/but-really-what-is-a-javascript-mock-10d060966f7d)

**What if I want to verify that an element does NOT exist?**

You typically will get access to rendered elements using the `getByTestId` utility. However, that function will throw an error if the element isn't found. If you want to specifically test for the absence of an element, then you should use the `queryByTestId` utility which will return the element if found or `null` if not.

```javascript
expect(queryByTestId('thing-that-does-not-exist')).toBeNull()
```

**I don't want to use `data-testid` attributes for everything. Do I have to?**

Definitely not. That said, a common reason people don't like the `data-testid`
Expand Down Expand Up @@ -286,18 +312,18 @@ Or you could include the index or an ID in your attribute:
<li data-testid={`item-${item.id}`}>{item.text}</li>
```

And then you could use the `queryByTestId`:
And then you could use the `getByTestId` utility:

```javascript
const items = [
/* your items */
]
const {queryByTestId} = render(/* your component with the items */)
const thirdItem = queryByTestId(`item-${items[2].id}`)
const {getByTestId} = render(/* your component with the items */)
const thirdItem = getByTestId(`item-${items[2].id}`)
```

**What about enzyme is "bloated with complexity and features" and "encourage poor testing
practices"**
practices"?**

Most of the damaging features have to do with encouraging testing implementation
details. Primarily, these are
Expand Down Expand Up @@ -358,8 +384,8 @@ Thanks goes to these people ([emoji key][emojis]):
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->

<!-- prettier-ignore -->
| [<img src="https://avatars.githubusercontent.com/u/1500684?v=3" width="100px;"/><br /><sub><b>Kent C. Dodds</b></sub>](https://kentcdodds.com)<br />[💻](https://github.com/kentcdodds/react-testing-library/commits?author=kentcdodds "Code") [📖](https://github.com/kentcdodds/react-testing-library/commits?author=kentcdodds "Documentation") [🚇](#infra-kentcdodds "Infrastructure (Hosting, Build-Tools, etc)") [⚠️](https://github.com/kentcdodds/react-testing-library/commits?author=kentcdodds "Tests") | [<img src="https://avatars1.githubusercontent.com/u/2430381?v=4" width="100px;"/><br /><sub><b>Ryan Castner</b></sub>](http://audiolion.github.io)<br />[📖](https://github.com/kentcdodds/react-testing-library/commits?author=audiolion "Documentation") | [<img src="https://avatars0.githubusercontent.com/u/8008023?v=4" width="100px;"/><br /><sub><b>Daniel Sandiego</b></sub>](https://www.dnlsandiego.com)<br />[💻](https://github.com/kentcdodds/react-testing-library/commits?author=dnlsandiego "Code") | [<img src="https://avatars2.githubusercontent.com/u/12592677?v=4" width="100px;"/><br /><sub><b>Paweł Mikołajczyk</b></sub>](https://github.com/Miklet)<br />[💻](https://github.com/kentcdodds/react-testing-library/commits?author=Miklet "Code") | [<img src="https://avatars3.githubusercontent.com/u/464978?v=4" width="100px;"/><br /><sub><b>Alejandro Ñáñez Ortiz</b></sub>](http://co.linkedin.com/in/alejandronanez/)<br />[📖](https://github.com/kentcdodds/react-testing-library/commits?author=alejandronanez "Documentation") |
| :---: | :---: | :---: | :---: | :---: |
| [<img src="https://avatars.githubusercontent.com/u/1500684?v=3" width="100px;"/><br /><sub><b>Kent C. Dodds</b></sub>](https://kentcdodds.com)<br />[💻](https://github.com/kentcdodds/react-testing-library/commits?author=kentcdodds "Code") [📖](https://github.com/kentcdodds/react-testing-library/commits?author=kentcdodds "Documentation") [🚇](#infra-kentcdodds "Infrastructure (Hosting, Build-Tools, etc)") [⚠️](https://github.com/kentcdodds/react-testing-library/commits?author=kentcdodds "Tests") | [<img src="https://avatars1.githubusercontent.com/u/2430381?v=4" width="100px;"/><br /><sub><b>Ryan Castner</b></sub>](http://audiolion.github.io)<br />[📖](https://github.com/kentcdodds/react-testing-library/commits?author=audiolion "Documentation") | [<img src="https://avatars0.githubusercontent.com/u/8008023?v=4" width="100px;"/><br /><sub><b>Daniel Sandiego</b></sub>](https://www.dnlsandiego.com)<br />[💻](https://github.com/kentcdodds/react-testing-library/commits?author=dnlsandiego "Code") | [<img src="https://avatars2.githubusercontent.com/u/12592677?v=4" width="100px;"/><br /><sub><b>Paweł Mikołajczyk</b></sub>](https://github.com/Miklet)<br />[💻](https://github.com/kentcdodds/react-testing-library/commits?author=Miklet "Code") | [<img src="https://avatars3.githubusercontent.com/u/464978?v=4" width="100px;"/><br /><sub><b>Alejandro Ñáñez Ortiz</b></sub>](http://co.linkedin.com/in/alejandronanez/)<br />[📖](https://github.com/kentcdodds/react-testing-library/commits?author=alejandronanez "Documentation") | [<img src="https://avatars0.githubusercontent.com/u/1402095?v=4" width="100px;"/><br /><sub><b>Matt Parrish</b></sub>](https://github.com/pbomb)<br />[🐛](https://github.com/kentcdodds/react-testing-library/issues?q=author%3Apbomb "Bug reports") [💻](https://github.com/kentcdodds/react-testing-library/commits?author=pbomb "Code") [📖](https://github.com/kentcdodds/react-testing-library/commits?author=pbomb "Documentation") [⚠️](https://github.com/kentcdodds/react-testing-library/commits?author=pbomb "Tests") |
| :---: | :---: | :---: | :---: | :---: | :---: |

<!-- ALL-CONTRIBUTORS-LIST:END -->

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,4 @@
"url": "https://github.com/kentcdodds/react-testing-library/issues"
},
"homepage": "https://github.com/kentcdodds/react-testing-library#readme"
}
}
15 changes: 15 additions & 0 deletions src/__tests__/__snapshots__/element-queries.js.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`getByTestId finds matching element 1`] = `
<span
data-testid="test-component"
/>
`;

exports[`getByTestId throws error when no matching element exists 1`] = `"Unable to find element by [data-testid=\\"unknown-data-testid\\"]"`;

exports[`queryByTestId finds matching element 1`] = `
<span
data-testid="test-component"
/>
`;
26 changes: 26 additions & 0 deletions src/__tests__/element-queries.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React from 'react'
import {render} from '../'

const TestComponent = () => <span data-testid="test-component" />

test('queryByTestId finds matching element', () => {
const {queryByTestId} = render(<TestComponent />)
expect(queryByTestId('test-component')).toMatchSnapshot()
})

test('queryByTestId returns null when no matching element exists', () => {
const {queryByTestId} = render(<TestComponent />)
expect(queryByTestId('unknown-data-testid')).toBeNull()
})

test('getByTestId finds matching element', () => {
const {getByTestId} = render(<TestComponent />)
expect(getByTestId('test-component')).toMatchSnapshot()
})

test('getByTestId throws error when no matching element exists', () => {
const {getByTestId} = render(<TestComponent />)
expect(() =>
getByTestId('unknown-data-testid'),
).toThrowErrorMatchingSnapshot()
})
6 changes: 3 additions & 3 deletions src/__tests__/fetch.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,16 @@ test('Fetch makes an API call and displays the greeting when load-greeting is cl
}),
)
const url = '/greeting'
const {queryByTestId, container} = render(<Fetch url={url} />)
const {getByTestId, container} = render(<Fetch url={url} />)

// Act
Simulate.click(queryByTestId('load-greeting'))
Simulate.click(getByTestId('load-greeting'))

await flushPromises()

// Assert
expect(axiosMock.get).toHaveBeenCalledTimes(1)
expect(axiosMock.get).toHaveBeenCalledWith(url)
expect(queryByTestId('greeting-text').textContent).toBe('hello there')
expect(getByTestId('greeting-text').textContent).toBe('hello there')
expect(container.firstChild).toMatchSnapshot()
})
6 changes: 4 additions & 2 deletions src/__tests__/mock.react-transition-group.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,12 @@ jest.mock('react-transition-group', () => {
})

test('you can mock things with jest.mock', () => {
const {queryByTestId} = render(<HiddenMessage initialShow={true} />)
const {getByTestId, queryByTestId} = render(
<HiddenMessage initialShow={true} />,
)
expect(queryByTestId('hidden-message')).toBeTruthy() // we just care it exists
// hide the message
Simulate.click(queryByTestId('toggle-message'))
Simulate.click(getByTestId('toggle-message'))
// in the real world, the CSSTransition component would take some time
// before finishing the animation which would actually hide the message.
// So we've mocked it out for our tests to make it happen instantly
Expand Down
8 changes: 4 additions & 4 deletions src/__tests__/number-display.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@ class NumberDisplay extends React.Component {
}

test('calling render with the same component on the same container does not remount', () => {
const {container, queryByTestId} = render(<NumberDisplay number={1} />)
expect(queryByTestId('number-display').textContent).toBe('1')
const {container, getByTestId} = render(<NumberDisplay number={1} />)
expect(getByTestId('number-display').textContent).toBe('1')

// re-render the same component with different props
// but pass the same container in the options argument.
// which will cause a re-render of the same instance (normal React behavior).
render(<NumberDisplay number={2} />, {container})
expect(queryByTestId('number-display').textContent).toBe('2')
expect(getByTestId('number-display').textContent).toBe('2')

expect(queryByTestId('instance-id').textContent).toBe('1')
expect(getByTestId('instance-id').textContent).toBe('1')
})
22 changes: 11 additions & 11 deletions src/__tests__/react-redux.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,27 +82,27 @@ function renderWithRedux(
}

test('can render with redux with defaults', () => {
const {queryByTestId} = renderWithRedux(<ConnectedCounter />)
Simulate.click(queryByTestId('incrementer'))
expect(queryByTestId('count-value').textContent).toBe('1')
const {getByTestId} = renderWithRedux(<ConnectedCounter />)
Simulate.click(getByTestId('incrementer'))
expect(getByTestId('count-value').textContent).toBe('1')
})

test('can render with redux with custom initial state', () => {
const {queryByTestId} = renderWithRedux(<ConnectedCounter />, {
const {getByTestId} = renderWithRedux(<ConnectedCounter />, {
initialState: {count: 3},
})
Simulate.click(queryByTestId('decrementer'))
expect(queryByTestId('count-value').textContent).toBe('2')
Simulate.click(getByTestId('decrementer'))
expect(getByTestId('count-value').textContent).toBe('2')
})

test('can render with redux with custom store', () => {
// this is a silly store that can never be changed
const store = createStore(() => ({count: 1000}))
const {queryByTestId} = renderWithRedux(<ConnectedCounter />, {
const {getByTestId} = renderWithRedux(<ConnectedCounter />, {
store,
})
Simulate.click(queryByTestId('incrementer'))
expect(queryByTestId('count-value').textContent).toBe('1000')
Simulate.click(queryByTestId('decrementer'))
expect(queryByTestId('count-value').textContent).toBe('1000')
Simulate.click(getByTestId('incrementer'))
expect(getByTestId('count-value').textContent).toBe('1000')
Simulate.click(getByTestId('decrementer'))
expect(getByTestId('count-value').textContent).toBe('1000')
})
8 changes: 4 additions & 4 deletions src/__tests__/react-router.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,11 @@ function renderWithRouter(
}

test('full app rendering/navigating', () => {
const {container, queryByTestId} = renderWithRouter(<App />)
const {container, getByTestId} = renderWithRouter(<App />)
// normally I'd use a data-testid, but just wanted to show this is also possible
expect(container.innerHTML).toMatch('You are home')
const leftClick = {button: 0}
Simulate.click(queryByTestId('about-link'), leftClick)
Simulate.click(getByTestId('about-link'), leftClick)
// normally I'd use a data-testid, but just wanted to show this is also possible
expect(container.innerHTML).toMatch('You are on the about page')
})
Expand All @@ -68,6 +68,6 @@ test('landing on a bad page', () => {

test('rendering a component that uses withRouter', () => {
const route = '/some-route'
const {queryByTestId} = renderWithRouter(<LocationDisplay />, {route})
expect(queryByTestId('location-display').textContent).toBe(route)
const {getByTestId} = renderWithRouter(<LocationDisplay />, {route})
expect(getByTestId('location-display').textContent).toBe(route)
})
4 changes: 2 additions & 2 deletions src/__tests__/shallow.react-transition-group.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,15 @@ jest.mock('react-transition-group', () => {
})

test('you can mock things with jest.mock', () => {
const {queryByTestId} = render(<HiddenMessage initialShow={true} />)
const {getByTestId} = render(<HiddenMessage initialShow={true} />)
const context = expect.any(Object)
const children = expect.any(Object)
const defaultProps = {children, timeout: 1000, className: 'fade'}
expect(CSSTransition).toHaveBeenCalledWith(
{in: true, ...defaultProps},
context,
)
Simulate.click(queryByTestId('toggle-message'))
Simulate.click(getByTestId('toggle-message'))
expect(CSSTransition).toHaveBeenCalledWith(
{in: true, ...defaultProps},
expect.any(Object),
Expand Down
4 changes: 2 additions & 2 deletions src/__tests__/stopwatch.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ const wait = time => new Promise(resolve => setTimeout(resolve, time))

test('unmounts a component', async () => {
jest.spyOn(console, 'error').mockImplementation(() => {})
const {unmount, queryByTestId, container} = render(<StopWatch />)
Simulate.click(queryByTestId('start-stop-button'))
const {unmount, getByTestId, container} = render(<StopWatch />)
Simulate.click(getByTestId('start-stop-button'))
unmount()
// hey there reader! You don't need to have an assertion like this one
// this is just me making sure that the unmount function works.
Expand Down
14 changes: 12 additions & 2 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,26 @@ function select(id) {
}

// we may expose this eventually
function queryDivByTestId(div, id) {
function queryByTestId(div, id) {
return div.querySelector(select(id))
}

// we may expose this eventually
function getByTestId(div, id) {
const el = queryByTestId(div, id)
if (!el) {
throw new Error(`Unable to find element by ${select(id)}`)
Copy link
Collaborator

@antsmartian antsmartian Mar 22, 2018

Choose a reason for hiding this comment

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

@kentcdodds Oops! If now getByTestId throws error, then not of toBeInTheDOM (which I'm working on it) wouldn't make sense right? Because people will use toThrowError for their assertion instead of not. So toBeInTheDOM is only for +ve case.

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, I'd thought of that. It still works for queryByTestId though.

}
return el
}

function render(ui, {container = document.createElement('div')} = {}) {
ReactDOM.render(ui, container)
return {
container,
unmount: () => ReactDOM.unmountComponentAtNode(container),
queryByTestId: queryDivByTestId.bind(null, container),
queryByTestId: queryByTestId.bind(null, container),
getByTestId: getByTestId.bind(null, container),
}
}

Expand Down
Loading