diff --git a/.all-contributorsrc b/.all-contributorsrc
index 5d154811..ac1c1f6b 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -1,6 +1,7 @@
{
"projectName": "react-testing-library",
"projectOwner": "kentcdodds",
+ "repoType": "github",
"files": [
"README.md"
],
@@ -18,6 +19,15 @@
"infra",
"test"
]
+ },
+ {
+ "login": "audiolion",
+ "name": "Ryan Castner",
+ "avatar_url": "https://avatars1.githubusercontent.com/u/2430381?v=4",
+ "profile": "http://audiolion.github.io",
+ "contributions": [
+ "doc"
+ ]
}
]
}
diff --git a/.prettierrc b/.prettierrc
index 68f30bd7..fb31ee19 100644
--- a/.prettierrc
+++ b/.prettierrc
@@ -4,7 +4,7 @@
"useTabs": false,
"semi": false,
"singleQuote": true,
- "trailingComma": 'all',
+ "trailingComma": "all",
"bracketSpacing": false,
"jsxBracketSameLine": false
}
diff --git a/README.md b/README.md
index a87391f6..1854178f 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,7 @@
[![downloads][downloads-badge]][npmtrends]
[![MIT License][license-badge]][license]
-[](#contributors)
+[](#contributors)
[![PRs Welcome][prs-badge]][prs]
[![Code of Conduct][coc-badge]][coc]
@@ -45,6 +45,7 @@ components. It provides light utility functions on top of `react-dom` and
* [`flushPromises`](#flushpromises)
* [`render`](#render)
* [More on `data-testid`s](#more-on-data-testids)
+* [Examples](#examples)
* [FAQ](#faq)
* [Other Solutions](#other-solutions)
* [Guiding Principles](#guiding-principles)
@@ -126,6 +127,21 @@ The containing DOM node of your rendered React Element (rendered using
> Tip: To get the root element of your rendered element, use `container.firstChild`.
+#### `unmount`
+
+This will cause the rendered component to be unmounted. This is useful for
+testing what happens when your component is removed from the page (like testing
+that you don't leave event handlers hanging around causing memory leaks).
+
+> This method is a pretty small abstraction over
+> `ReactDOM.unmountComponentAtNode`
+
+```javascript
+const {container, unmount} = render()
+unmount()
+// your component has been unmounted and now: container.innerHTML === ''
+```
+
#### `queryByTestId`
A shortcut to `` container.querySelector(`[data-testid="${yourId}"]`) ``. Read
@@ -144,6 +160,17 @@ one of the practices this library is intended to encourage.
Learn more about this practice in the blog post:
["Making your UI tests resilient to change"](https://blog.kentcdodds.com/making-your-ui-tests-resilient-to-change-d37a6ee37269)
+## Examples
+
+You'll find examples of testing with different libraries in
+[the test directory](https://github.com/kentcdodds/react-testing-library/blob/master/src/__tests__).
+Some included are:
+
+* [`react-redux`](https://github.com/kentcdodds/react-testing-library/blob/master/src/__tests__/react-redux.js)
+* [`react-router`](https://github.com/kentcdodds/react-testing-library/blob/master/src/__tests__/react-router.js)
+
+Feel free to contribute more!
+
## FAQ
**How do I update the props of a rendered component?**
@@ -241,6 +268,30 @@ const allLisInDiv = container.querySelectorAll('div li')
const rootElement = container.firstChild
```
+**What if Iām iterating over a list of items that I want to put the data-testid="item" attribute on. How do I distinguish them from each other?**
+
+You can make your selector just choose the one you want by including :nth-child in the selector.
+
+```javascript
+const thirdLiInUl = container.querySelector('ul > li:nth-child(3)')
+```
+
+Or you could include the index or an ID in your attribute:
+
+```javascript
+
{item.text}
+```
+
+And then you could use the `queryByTestId`:
+
+```javascript
+const items = [
+ /* your items */
+]
+const {queryByTestId} = render(/* your component with the items */)
+const thirdItem = queryByTestId(`item-${items[2].id}`)
+```
+
**What about enzyme is "bloated with complexity and features" and "encourage poor testing
practices"**
@@ -303,8 +354,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") |
-| :---: |
+| [ 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") |
+| :---: | :---: |
diff --git a/package.json b/package.json
index 204da157..58881938 100644
--- a/package.json
+++ b/package.json
@@ -26,10 +26,15 @@
"dependencies": {},
"devDependencies": {
"axios": "^0.18.0",
+ "history": "^4.7.2",
"kcd-scripts": "^0.36.1",
"react": "^16.2.0",
"react-dom": "^16.2.0",
- "react-transition-group": "^2.2.1"
+ "react-redux": "^5.0.7",
+ "react-router": "^4.2.0",
+ "react-router-dom": "^4.2.2",
+ "react-transition-group": "^2.2.1",
+ "redux": "^3.7.2"
},
"peerDependencies": {
"react-dom": "*"
diff --git a/src/__tests__/react-redux.js b/src/__tests__/react-redux.js
new file mode 100644
index 00000000..eb9f527e
--- /dev/null
+++ b/src/__tests__/react-redux.js
@@ -0,0 +1,108 @@
+import React from 'react'
+import {createStore} from 'redux'
+import {Provider, connect} from 'react-redux'
+import {render, Simulate} from '../'
+
+// counter.js
+class Counter extends React.Component {
+ increment = () => {
+ this.props.dispatch({type: 'INCREMENT'})
+ }
+
+ decrement = () => {
+ this.props.dispatch({type: 'DECREMENT'})
+ }
+
+ render() {
+ return (
+
+
Counter
+
+
+ {this.props.count}
+
+
+
+ )
+ }
+}
+
+// normally this would be:
+// export default connect(state => ({count: state.count}))(Counter)
+// but for this test we'll give it a variable name
+// because we're doing this all in one file
+const ConnectedCounter = connect(state => ({count: state.count}))(Counter)
+
+// app.js
+function reducer(state = {count: 0}, action) {
+ switch (action.type) {
+ case 'INCREMENT':
+ return {
+ count: state.count + 1,
+ }
+ case 'DECREMENT':
+ return {
+ count: state.count - 1,
+ }
+ default:
+ return state
+ }
+}
+
+// normally here you'd do:
+// const store = createStore(reducer)
+// ReactDOM.render(
+//
+//
+// ,
+// document.getElementById('root'),
+// )
+// but for this test we'll umm... not do that :)
+
+// Now here's what your test will look like:
+
+// this is a handy function that I normally make available for all my tests
+// that deal with connected components.
+// you can provide initialState or the entire store that the ui is rendered with
+function renderWithRedux(
+ ui,
+ {initialState, store = createStore(reducer, initialState)} = {},
+) {
+ return {
+ ...render({ui}),
+ // adding `store` to the returned utilities to allow us
+ // to reference it in our tests (just try to avoid using
+ // this to test implementation details).
+ store,
+ }
+}
+
+test('can render with redux with defaults', () => {
+ const {queryByTestId} = renderWithRedux()
+ Simulate.click(queryByTestId('incrementer'))
+ expect(queryByTestId('count-value').textContent).toBe('1')
+})
+
+test('can render with redux with custom initial state', () => {
+ const {queryByTestId} = renderWithRedux(, {
+ initialState: {count: 3},
+ })
+ Simulate.click(queryByTestId('decrementer'))
+ expect(queryByTestId('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(, {
+ store,
+ })
+ Simulate.click(queryByTestId('incrementer'))
+ expect(queryByTestId('count-value').textContent).toBe('1000')
+ Simulate.click(queryByTestId('decrementer'))
+ expect(queryByTestId('count-value').textContent).toBe('1000')
+})
diff --git a/src/__tests__/react-router.js b/src/__tests__/react-router.js
new file mode 100644
index 00000000..57c2fc83
--- /dev/null
+++ b/src/__tests__/react-router.js
@@ -0,0 +1,73 @@
+import React from 'react'
+import {withRouter} from 'react-router'
+import {Link, Route, Router, Switch} from 'react-router-dom'
+import {createMemoryHistory} from 'history'
+import {render, Simulate} from '../'
+
+const About = () =>
+ )
+}
+
+// Ok, so here's what your tests might look like
+
+// this is a handy function that I would utilize for any component
+// that relies on the router being in context
+function renderWithRouter(
+ ui,
+ {route = '/', history = createMemoryHistory({initialEntries: [route]})} = {},
+) {
+ return {
+ ...render({ui}),
+ // adding `history` to the returned utilities to allow us
+ // to reference it in our tests (just try to avoid using
+ // this to test implementation details).
+ history,
+ }
+}
+
+test('full app rendering/navigating', () => {
+ const {container, queryByTestId} = 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)
+ // 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')
+})
+
+test('landing on a bad page', () => {
+ const {container} = renderWithRouter(, {
+ route: '/something-that-does-not-match',
+ })
+ // normally I'd use a data-testid, but just wanted to show this is also possible
+ expect(container.innerHTML).toMatch('No match')
+})
+
+test('rendering a component that uses withRouter', () => {
+ const route = '/some-route'
+ const {queryByTestId} = renderWithRouter(, {route})
+ expect(queryByTestId('location-display').textContent).toBe(route)
+})
diff --git a/src/__tests__/stopwatch.js b/src/__tests__/stopwatch.js
new file mode 100644
index 00000000..e4b15c37
--- /dev/null
+++ b/src/__tests__/stopwatch.js
@@ -0,0 +1,59 @@
+import React from 'react'
+import {render, Simulate} from '../'
+
+class StopWatch extends React.Component {
+ state = {lapse: 0, running: false}
+ handleRunClick = () => {
+ this.setState(state => {
+ if (state.running) {
+ clearInterval(this.timer)
+ } else {
+ const startTime = Date.now() - this.state.lapse
+ this.timer = setInterval(() => {
+ this.setState({lapse: Date.now() - startTime})
+ })
+ }
+ return {running: !state.running}
+ })
+ }
+ handleClearClick = () => {
+ clearInterval(this.timer)
+ this.setState({lapse: 0, running: false})
+ }
+ componentWillUnmount() {
+ clearInterval(this.timer)
+ }
+ render() {
+ const {lapse, running} = this.state
+ return (
+
+ {lapse}ms
+
+
+
+ )
+ }
+}
+
+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'))
+ 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.
+ // You don't need to do this in your apps. Just rely on the fact that this works.
+ expect(container.innerHTML).toBe('')
+ // just wait to see if the interval is cleared or not
+ // if it's not, then we'll call setState on an unmounted component
+ // and get an error.
+ await wait()
+ // eslint-disable-next-line no-console
+ expect(console.error).not.toHaveBeenCalled()
+})
diff --git a/src/index.js b/src/index.js
index 7d99459d..b1258b96 100644
--- a/src/index.js
+++ b/src/index.js
@@ -15,6 +15,7 @@ function render(ui, {container = document.createElement('div')} = {}) {
ReactDOM.render(ui, container)
return {
container,
+ unmount: () => ReactDOM.unmountComponentAtNode(container),
queryByTestId: queryDivByTestId.bind(null, container),
}
}