Skip to content

Commit 5c353ab

Browse files
author
Kent C. Dodds
committed
feat(act): Support ReactDOM.TestUtils.act
This also removes `flushEffects` which is no longer necessary!
1 parent 6388adf commit 5c353ab

File tree

9 files changed

+123
-26
lines changed

9 files changed

+123
-26
lines changed

README.md

+16-6
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,16 @@
22
<h1>react-testing-library</h1>
33

44
<a href="https://www.emojione.com/emoji/1f410">
5-
<img height="80" width="80" alt="goat" src="https://raw.githubusercontent.com/kentcdodds/react-testing-library/master/other/goat.png" />
5+
<img
6+
height="80"
7+
width="80"
8+
alt="goat"
9+
src="https://raw.githubusercontent.com/kentcdodds/react-testing-library/master/other/goat.png"
10+
/>
611
</a>
712

8-
<p>Simple and complete React DOM testing utilities that encourage good testing practices.</p>
13+
<p>Simple and complete React DOM testing utilities that encourage good testing
14+
practices.</p>
915

1016
[**Read The Docs**](https://testing-library.com/react) |
1117
[Edit the docs](https://github.com/alexkrolick/testing-library-docs)
@@ -30,9 +36,13 @@
3036
<!-- prettier-ignore-end -->
3137

3238
<div align="center">
33-
<a href="https://testingjavascript.com">
34-
<img width="500" alt="TestingJavaScript.com Learn the smart, efficient way to test any JavaScript application." src="https://raw.githubusercontent.com/kentcdodds/react-testing-library/master/other/testingjavascript.jpg" />
35-
</a>
39+
<a href="https://testingjavascript.com">
40+
<img
41+
width="500"
42+
alt="TestingJavaScript.com Learn the smart, efficient way to test any JavaScript application."
43+
src="https://raw.githubusercontent.com/kentcdodds/react-testing-library/master/other/testingjavascript.jpg"
44+
/>
45+
</a>
3646
</div>
3747

3848
## Table of Contents
@@ -102,7 +112,7 @@ test('Fetch makes an API call and displays the greeting when load-greeting is cl
102112
)
103113

104114
// Act
105-
fireEvent.click(getByText('Load Greeting'))
115+
fireEvent.click(getByText(/load greeting/i))
106116

107117
// Let's wait until our mocked `get` request promise resolves and
108118
// the component calls setState and re-renders.

package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@
4848
"jest-dom": "^2.0.4",
4949
"jest-in-case": "^1.0.2",
5050
"kcd-scripts": "^0.44.0",
51-
"react": "16.8.0",
52-
"react-dom": "16.8.0",
51+
"react": "^16.8.0",
52+
"react-dom": "^16.8.0",
5353
"react-redux": "^5.0.7",
5454
"react-router": "^4.3.1",
5555
"react-router-dom": "^4.3.1",

src/__tests__/act.js

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import 'jest-dom/extend-expect'
2+
import React from 'react'
3+
import {render, cleanup, fireEvent} from '../'
4+
5+
afterEach(cleanup)
6+
7+
test('render calls useEffect immediately', () => {
8+
const effectCb = jest.fn()
9+
function MyUselessComponent() {
10+
React.useEffect(effectCb)
11+
return null
12+
}
13+
render(<MyUselessComponent />)
14+
expect(effectCb).toHaveBeenCalledTimes(1)
15+
})
16+
17+
test('fireEvent triggers useEffect calls', () => {
18+
const effectCb = jest.fn()
19+
function Counter() {
20+
React.useEffect(effectCb)
21+
const [count, setCount] = React.useState(0)
22+
return <button onClick={() => setCount(count + 1)}>{count}</button>
23+
}
24+
const {
25+
container: {firstChild: buttonNode},
26+
} = render(<Counter />)
27+
28+
effectCb.mockClear()
29+
fireEvent.click(buttonNode)
30+
expect(buttonNode).toHaveTextContent('1')
31+
expect(effectCb).toHaveBeenCalledTimes(1)
32+
})
33+
34+
test('calls to hydrate will run useEffects', () => {
35+
const effectCb = jest.fn()
36+
function MyUselessComponent() {
37+
React.useEffect(effectCb)
38+
return null
39+
}
40+
render(<MyUselessComponent />, {hydrate: true})
41+
expect(effectCb).toHaveBeenCalledTimes(1)
42+
})

src/__tests__/no-act.js

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import {act} from '..'
2+
3+
jest.mock('react-dom/test-utils', () => ({}))
4+
5+
test('act works even when there is no act from test utils', () => {
6+
const callback = jest.fn()
7+
act(callback)
8+
expect(callback).toHaveBeenCalledTimes(1)
9+
expect(callback).toHaveBeenCalledWith(/* nothing */)
10+
})

src/__tests__/render.js

+1-8
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import 'jest-dom/extend-expect'
22
import React from 'react'
33
import ReactDOM from 'react-dom'
4-
import {render, cleanup, flushEffects} from '../'
4+
import {render, cleanup} from '../'
55

66
afterEach(cleanup)
77

@@ -90,10 +90,3 @@ it('supports fragments', () => {
9090
cleanup()
9191
expect(document.body.innerHTML).toBe('')
9292
})
93-
94-
test('flushEffects can be called without causing issues', () => {
95-
render(<div />)
96-
const preHtml = document.documentElement.innerHTML
97-
flushEffects()
98-
expect(document.documentElement.innerHTML).toBe(preHtml)
99-
})
File renamed without changes.

src/act-compat.js

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import {act as reactAct} from 'react-dom/test-utils'
2+
3+
// act is supported [email protected]
4+
// and is only needed for versions higher than that
5+
// so we do nothing for versions that don't support act.
6+
const act = reactAct || (cb => cb())
7+
8+
function rtlAct(...args) {
9+
return act(...args)
10+
}
11+
12+
export default rtlAct

src/index.js

+36-8
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import React from 'react'
22
import ReactDOM from 'react-dom'
3-
import {getQueriesForElement, prettyDOM, fireEvent} from 'dom-testing-library'
3+
import {
4+
getQueriesForElement,
5+
prettyDOM,
6+
fireEvent as dtlFireEvent,
7+
} from 'dom-testing-library'
8+
import act from './act-compat'
49

510
const mountedContainers = new Set()
611

@@ -21,9 +26,13 @@ function render(
2126
mountedContainers.add(container)
2227

2328
if (hydrate) {
24-
ReactDOM.hydrate(ui, container)
29+
act(() => {
30+
ReactDOM.hydrate(ui, container)
31+
})
2532
} else {
26-
ReactDOM.render(ui, container)
33+
act(() => {
34+
ReactDOM.render(ui, container)
35+
})
2736
}
2837
return {
2938
container,
@@ -65,10 +74,6 @@ function cleanup() {
6574
mountedContainers.forEach(cleanupAtContainer)
6675
}
6776

68-
function flushEffects() {
69-
ReactDOM.render(null, document.createElement('div'))
70-
}
71-
7277
// maybe one day we'll expose this (perhaps even as a utility returned by render).
7378
// but let's wait until someone asks for it.
7479
function cleanupAtContainer(container) {
@@ -79,6 +84,29 @@ function cleanupAtContainer(container) {
7984
mountedContainers.delete(container)
8085
}
8186

87+
// react-testing-library's version of fireEvent will call
88+
// dom-testing-library's version of fireEvent wrapped inside
89+
// an "act" call so that after all event callbacks have been
90+
// been called, the resulting useEffect callbacks will also
91+
// be called.
92+
function fireEvent(...args) {
93+
let returnValue
94+
act(() => {
95+
returnValue = dtlFireEvent(...args)
96+
})
97+
return returnValue
98+
}
99+
100+
Object.keys(dtlFireEvent).forEach(key => {
101+
fireEvent[key] = (...args) => {
102+
let returnValue
103+
act(() => {
104+
returnValue = dtlFireEvent[key](...args)
105+
})
106+
return returnValue
107+
}
108+
})
109+
82110
// React event system tracks native mouseOver/mouseOut events for
83111
// running onMouseEnter/onMouseLeave handlers
84112
// @link https://github.com/facebook/react/blob/b87aabdfe1b7461e7331abb3601d9e6bb27544bc/packages/react-dom/src/events/EnterLeaveEventPlugin.js#L24-L31
@@ -102,6 +130,6 @@ fireEvent.select = (node, init) => {
102130

103131
// just re-export everything from dom-testing-library
104132
export * from 'dom-testing-library'
105-
export {render, testHook, cleanup, flushEffects}
133+
export {render, testHook, cleanup, fireEvent, act}
106134

107135
/* eslint func-name-matching:0 */

typings/index.d.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ export function testHook(callback: () => void): void
5151
export function cleanup(): void
5252

5353
/**
54-
* Forces React's `useEffect` hook to run synchronously.
54+
* Simply calls ReactDOMTestUtils.act(cb)
55+
* If that's not available (older version of react) then it
56+
* simply calls the given callback immediately
5557
*/
56-
export function flushEffects(): void
58+
export function act(callback: () => void): void

0 commit comments

Comments
 (0)