diff --git a/.all-contributorsrc b/.all-contributorsrc
index 5bde84c2..90dd9791 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -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"
+ ]
}
]
}
diff --git a/README.md b/README.md
index 1fe420d9..bc9d6383 100644
--- a/README.md
+++ b/README.md
@@ -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]
@@ -86,10 +86,10 @@ test('Fetch makes an API call and displays the greeting when load-greeting is cl
}),
)
const url = '/greeting'
- const {queryByTestId, container} = render()
+ const {getByTestId, container} = render()
// Act
- Simulate.click(queryByTestId('load-greeting'))
+ Simulate.click(getByTestId('load-greeting'))
// let's wait for our mocked `get` request promise to resolve
await flushPromises()
@@ -97,7 +97,7 @@ test('Fetch makes an API call and displays the greeting when load-greeting is cl
// 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()
})
```
@@ -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.
@@ -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()
-expect(queryByTestId('number-display').textContent).toBe('1')
+const {container, getByTestId} = render()
+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(, {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)
@@ -219,14 +235,16 @@ jest.mock('react-transition-group', () => {
})
test('you can mock things with jest.mock', () => {
- const {queryByTestId} = render()
+ const {getByTestId, queryByTestId} = render(
+ ,
+ )
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
})
```
@@ -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`
@@ -286,18 +312,18 @@ Or you could include the index or an ID in your attribute:
{item.text}
```
-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
@@ -358,8 +384,8 @@ Thanks goes to these people ([emoji key][emojis]):
-| [
Kent C. Dodds](https://kentcdodds.com)
[💻](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") | [
Ryan Castner](http://audiolion.github.io)
[📖](https://github.com/kentcdodds/react-testing-library/commits?author=audiolion "Documentation") | [
Daniel Sandiego](https://www.dnlsandiego.com)
[💻](https://github.com/kentcdodds/react-testing-library/commits?author=dnlsandiego "Code") | [
Paweł Mikołajczyk](https://github.com/Miklet)
[💻](https://github.com/kentcdodds/react-testing-library/commits?author=Miklet "Code") | [
Alejandro Ñáñez Ortiz](http://co.linkedin.com/in/alejandronanez/)
[📖](https://github.com/kentcdodds/react-testing-library/commits?author=alejandronanez "Documentation") |
-| :---: | :---: | :---: | :---: | :---: |
+| [
Kent C. Dodds](https://kentcdodds.com)
[💻](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") | [
Ryan Castner](http://audiolion.github.io)
[📖](https://github.com/kentcdodds/react-testing-library/commits?author=audiolion "Documentation") | [
Daniel Sandiego](https://www.dnlsandiego.com)
[💻](https://github.com/kentcdodds/react-testing-library/commits?author=dnlsandiego "Code") | [
Paweł Mikołajczyk](https://github.com/Miklet)
[💻](https://github.com/kentcdodds/react-testing-library/commits?author=Miklet "Code") | [
Alejandro Ñáñez Ortiz](http://co.linkedin.com/in/alejandronanez/)
[📖](https://github.com/kentcdodds/react-testing-library/commits?author=alejandronanez "Documentation") | [
Matt Parrish](https://github.com/pbomb)
[🐛](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") |
+| :---: | :---: | :---: | :---: | :---: | :---: |
diff --git a/package.json b/package.json
index e662a13a..8794fdfe 100644
--- a/package.json
+++ b/package.json
@@ -61,4 +61,4 @@
"url": "https://github.com/kentcdodds/react-testing-library/issues"
},
"homepage": "https://github.com/kentcdodds/react-testing-library#readme"
-}
\ No newline at end of file
+}
diff --git a/src/__tests__/__snapshots__/element-queries.js.snap b/src/__tests__/__snapshots__/element-queries.js.snap
new file mode 100644
index 00000000..722e413e
--- /dev/null
+++ b/src/__tests__/__snapshots__/element-queries.js.snap
@@ -0,0 +1,15 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`getByTestId finds matching element 1`] = `
+
+`;
+
+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`] = `
+
+`;
diff --git a/src/__tests__/element-queries.js b/src/__tests__/element-queries.js
new file mode 100644
index 00000000..270865fe
--- /dev/null
+++ b/src/__tests__/element-queries.js
@@ -0,0 +1,26 @@
+import React from 'react'
+import {render} from '../'
+
+const TestComponent = () =>
+
+test('queryByTestId finds matching element', () => {
+ const {queryByTestId} = render()
+ expect(queryByTestId('test-component')).toMatchSnapshot()
+})
+
+test('queryByTestId returns null when no matching element exists', () => {
+ const {queryByTestId} = render()
+ expect(queryByTestId('unknown-data-testid')).toBeNull()
+})
+
+test('getByTestId finds matching element', () => {
+ const {getByTestId} = render()
+ expect(getByTestId('test-component')).toMatchSnapshot()
+})
+
+test('getByTestId throws error when no matching element exists', () => {
+ const {getByTestId} = render()
+ expect(() =>
+ getByTestId('unknown-data-testid'),
+ ).toThrowErrorMatchingSnapshot()
+})
diff --git a/src/__tests__/fetch.js b/src/__tests__/fetch.js
index 6b2c5f01..6cca6f44 100644
--- a/src/__tests__/fetch.js
+++ b/src/__tests__/fetch.js
@@ -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()
+ const {getByTestId, container} = render()
// 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()
})
diff --git a/src/__tests__/mock.react-transition-group.js b/src/__tests__/mock.react-transition-group.js
index 76d0b0ba..b46cf4b3 100644
--- a/src/__tests__/mock.react-transition-group.js
+++ b/src/__tests__/mock.react-transition-group.js
@@ -39,10 +39,12 @@ jest.mock('react-transition-group', () => {
})
test('you can mock things with jest.mock', () => {
- const {queryByTestId} = render()
+ const {getByTestId, queryByTestId} = render(
+ ,
+ )
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
diff --git a/src/__tests__/number-display.js b/src/__tests__/number-display.js
index f54843fd..60a9ea32 100644
--- a/src/__tests__/number-display.js
+++ b/src/__tests__/number-display.js
@@ -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()
- expect(queryByTestId('number-display').textContent).toBe('1')
+ const {container, getByTestId} = render()
+ 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(, {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')
})
diff --git a/src/__tests__/react-redux.js b/src/__tests__/react-redux.js
index eb9f527e..c811c515 100644
--- a/src/__tests__/react-redux.js
+++ b/src/__tests__/react-redux.js
@@ -82,27 +82,27 @@ function renderWithRedux(
}
test('can render with redux with defaults', () => {
- const {queryByTestId} = renderWithRedux()
- Simulate.click(queryByTestId('incrementer'))
- expect(queryByTestId('count-value').textContent).toBe('1')
+ const {getByTestId} = renderWithRedux()
+ Simulate.click(getByTestId('incrementer'))
+ expect(getByTestId('count-value').textContent).toBe('1')
})
test('can render with redux with custom initial state', () => {
- const {queryByTestId} = renderWithRedux(, {
+ const {getByTestId} = renderWithRedux(, {
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(, {
+ const {getByTestId} = renderWithRedux(, {
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')
})
diff --git a/src/__tests__/react-router.js b/src/__tests__/react-router.js
index 57c2fc83..74e75584 100644
--- a/src/__tests__/react-router.js
+++ b/src/__tests__/react-router.js
@@ -49,11 +49,11 @@ function renderWithRouter(
}
test('full app rendering/navigating', () => {
- const {container, queryByTestId} = renderWithRouter()
+ const {container, getByTestId} = renderWithRouter()
// 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')
})
@@ -68,6 +68,6 @@ test('landing on a bad page', () => {
test('rendering a component that uses withRouter', () => {
const route = '/some-route'
- const {queryByTestId} = renderWithRouter(, {route})
- expect(queryByTestId('location-display').textContent).toBe(route)
+ const {getByTestId} = renderWithRouter(, {route})
+ expect(getByTestId('location-display').textContent).toBe(route)
})
diff --git a/src/__tests__/shallow.react-transition-group.js b/src/__tests__/shallow.react-transition-group.js
index 0ff49dfb..b724eb70 100644
--- a/src/__tests__/shallow.react-transition-group.js
+++ b/src/__tests__/shallow.react-transition-group.js
@@ -35,7 +35,7 @@ jest.mock('react-transition-group', () => {
})
test('you can mock things with jest.mock', () => {
- const {queryByTestId} = render()
+ const {getByTestId} = render()
const context = expect.any(Object)
const children = expect.any(Object)
const defaultProps = {children, timeout: 1000, className: 'fade'}
@@ -43,7 +43,7 @@ test('you can mock things with jest.mock', () => {
{in: true, ...defaultProps},
context,
)
- Simulate.click(queryByTestId('toggle-message'))
+ Simulate.click(getByTestId('toggle-message'))
expect(CSSTransition).toHaveBeenCalledWith(
{in: true, ...defaultProps},
expect.any(Object),
diff --git a/src/__tests__/stopwatch.js b/src/__tests__/stopwatch.js
index e4b15c37..66dd2667 100644
--- a/src/__tests__/stopwatch.js
+++ b/src/__tests__/stopwatch.js
@@ -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()
- Simulate.click(queryByTestId('start-stop-button'))
+ const {unmount, getByTestId, container} = render()
+ 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.
diff --git a/src/index.js b/src/index.js
index da3ecba5..afa67298 100644
--- a/src/index.js
+++ b/src/index.js
@@ -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)}`)
+ }
+ 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),
}
}
diff --git a/typings/index.d.ts b/typings/index.d.ts
index ab239595..f3c8554c 100644
--- a/typings/index.d.ts
+++ b/typings/index.d.ts
@@ -4,6 +4,7 @@ interface RenderResult {
container: HTMLDivElement
unmount: VoidFunction
queryByTestId: (id: string) => HTMLElement | null
+ getByTestId: (id: string) => HTMLElement
}
export function render(