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

feat: Add getByTestId utility #10

Merged
merged 3 commits into from
Mar 21, 2018
Merged
Show file tree
Hide file tree
Changes from 1 commit
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"
]
}
]
}
43 changes: 28 additions & 15 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 @@ -148,16 +148,27 @@ unmount()

#### `queryByTestId`

A shortcut to `` container.querySelector(`[data-testid="${yourId}"]`) ``. Read
A shortcut to `` container.querySelector(`[data-testid="${yourId}"]`) ``. This could return null if no matching element is found. Read
more about `data-testid`s below.

```javascript
const usernameInputElement = queryByTestId('username-input')
const hiddenItemElement = queryByTestId('item-hidden')
expect(hiddenItemElement).toBeFalsy() // we just care it doesn't exist
```

#### `getByTestId`
Copy link
Member

Choose a reason for hiding this comment

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

Let's move this above queryByTestId to indicate that it's recommended.


A shortcut to `` container.querySelector(`[data-testid="${yourId}"]`) `` except that it will throw an Error if no matching element is found. Use this instead of `queryByTestId` if you don't want to handle whether the return value could be null. Read more about `data-testid`s below.

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

## More on `data-testid`s

The `queryByTestId` utility is referring to the practice of using `data-testid`
The `queryByTestId` and `getByTestId` utilities refer to the practice of using `data-testid`
Copy link
Member

Choose a reason for hiding this comment

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

Swap the order here to give preferential treatment to getByTestId

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 +197,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,10 +230,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 Expand Up @@ -286,7 +299,7 @@ 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 `queryByTestId` utility:
Copy link
Member

Choose a reason for hiding this comment

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

Could you switch this to getByTestId?

Also, could you add a FAQ for: What if I want to verify that an element does NOT exist? Where we show how to use queryByTestId as well as container.querySelector as possible solutions.


```javascript
const items = [
Expand Down Expand Up @@ -358,8 +371,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
1 change: 1 addition & 0 deletions typings/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ interface RenderResult {
container: HTMLDivElement
unmount: VoidFunction
queryByTestId: (id: string) => HTMLElement | null
getByTestId: (id: string) => HTMLElement
}

export function render(
Expand Down