Skip to content

Commit

Permalink
feat(act): Support ReactDOM.TestUtils.act (#278)
Browse files Browse the repository at this point in the history
This also removes `flushEffects` which is no longer necessary!
  • Loading branch information
Kent C. Dodds committed Feb 6, 2019
1 parent d01fbc1 commit 8e08ccf
Show file tree
Hide file tree
Showing 9 changed files with 123 additions and 26 deletions.
22 changes: 16 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,16 @@
<h1>react-testing-library</h1>

<a href="https://www.emojione.com/emoji/1f410">
<img height="80" width="80" alt="goat" src="https://github.com/kentcdodds/react-testing-library/master/other/goat.png" />
<img
height="80"
width="80"
alt="goat"
src="https://github.com/kentcdodds/react-testing-library/master/other/goat.png"
/>
</a>

<p>Simple and complete React DOM testing utilities that encourage good testing practices.</p>
<p>Simple and complete React DOM testing utilities that encourage good testing
practices.</p>

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

<div align="center">
<a href="https://testingjavascript.com">
<img width="500" alt="TestingJavaScript.com Learn the smart, efficient way to test any JavaScript application." src="https://github.com/kentcdodds/react-testing-library/master/other/testingjavascript.jpg" />
</a>
<a href="https://testingjavascript.com">
<img
width="500"
alt="TestingJavaScript.com Learn the smart, efficient way to test any JavaScript application."
src="https://github.com/kentcdodds/react-testing-library/master/other/testingjavascript.jpg"
/>
</a>
</div>

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

// Act
fireEvent.click(getByText('Load Greeting'))
fireEvent.click(getByText(/load greeting/i))

// Let's wait until our mocked `get` request promise resolves and
// the component calls setState and re-renders.
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@
"jest-dom": "^2.0.4",
"jest-in-case": "^1.0.2",
"kcd-scripts": "^0.44.0",
"react": "16.8.0",
"react-dom": "16.8.0",
"react": "^16.8.0",
"react-dom": "^16.8.0",
"react-redux": "^5.0.7",
"react-router": "^4.3.1",
"react-router-dom": "^4.3.1",
Expand Down
42 changes: 42 additions & 0 deletions src/__tests__/act.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import 'jest-dom/extend-expect'
import React from 'react'
import {render, cleanup, fireEvent} from '../'

afterEach(cleanup)

test('render calls useEffect immediately', () => {
const effectCb = jest.fn()
function MyUselessComponent() {
React.useEffect(effectCb)
return null
}
render(<MyUselessComponent />)
expect(effectCb).toHaveBeenCalledTimes(1)
})

test('fireEvent triggers useEffect calls', () => {
const effectCb = jest.fn()
function Counter() {
React.useEffect(effectCb)
const [count, setCount] = React.useState(0)
return <button onClick={() => setCount(count + 1)}>{count}</button>
}
const {
container: {firstChild: buttonNode},
} = render(<Counter />)

effectCb.mockClear()
fireEvent.click(buttonNode)
expect(buttonNode).toHaveTextContent('1')
expect(effectCb).toHaveBeenCalledTimes(1)
})

test('calls to hydrate will run useEffects', () => {
const effectCb = jest.fn()
function MyUselessComponent() {
React.useEffect(effectCb)
return null
}
render(<MyUselessComponent />, {hydrate: true})
expect(effectCb).toHaveBeenCalledTimes(1)
})
10 changes: 10 additions & 0 deletions src/__tests__/no-act.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import {act} from '..'

jest.mock('react-dom/test-utils', () => ({}))

test('act works even when there is no act from test utils', () => {
const callback = jest.fn()
act(callback)
expect(callback).toHaveBeenCalledTimes(1)
expect(callback).toHaveBeenCalledWith(/* nothing */)
})
9 changes: 1 addition & 8 deletions src/__tests__/render.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import 'jest-dom/extend-expect'
import React from 'react'
import ReactDOM from 'react-dom'
import {render, cleanup, flushEffects} from '../'
import {render, cleanup} from '../'

afterEach(cleanup)

Expand Down Expand Up @@ -90,10 +90,3 @@ it('supports fragments', () => {
cleanup()
expect(document.body.innerHTML).toBe('')
})

test('flushEffects can be called without causing issues', () => {
render(<div />)
const preHtml = document.documentElement.innerHTML
flushEffects()
expect(document.documentElement.innerHTML).toBe(preHtml)
})
File renamed without changes.
12 changes: 12 additions & 0 deletions src/act-compat.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import {act as reactAct} from 'react-dom/test-utils'

// act is supported react-dom@16.8.0
// and is only needed for versions higher than that
// so we do nothing for versions that don't support act.
const act = reactAct || (cb => cb())

function rtlAct(...args) {
return act(...args)
}

export default rtlAct
44 changes: 36 additions & 8 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import React from 'react'
import ReactDOM from 'react-dom'
import {getQueriesForElement, prettyDOM, fireEvent} from 'dom-testing-library'
import {
getQueriesForElement,
prettyDOM,
fireEvent as dtlFireEvent,
} from 'dom-testing-library'
import act from './act-compat'

const mountedContainers = new Set()

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

if (hydrate) {
ReactDOM.hydrate(ui, container)
act(() => {
ReactDOM.hydrate(ui, container)
})
} else {
ReactDOM.render(ui, container)
act(() => {
ReactDOM.render(ui, container)
})
}
return {
container,
Expand Down Expand Up @@ -65,10 +74,6 @@ function cleanup() {
mountedContainers.forEach(cleanupAtContainer)
}

function flushEffects() {
ReactDOM.render(null, document.createElement('div'))
}

// maybe one day we'll expose this (perhaps even as a utility returned by render).
// but let's wait until someone asks for it.
function cleanupAtContainer(container) {
Expand All @@ -79,6 +84,29 @@ function cleanupAtContainer(container) {
mountedContainers.delete(container)
}

// react-testing-library's version of fireEvent will call
// dom-testing-library's version of fireEvent wrapped inside
// an "act" call so that after all event callbacks have been
// been called, the resulting useEffect callbacks will also
// be called.
function fireEvent(...args) {
let returnValue
act(() => {
returnValue = dtlFireEvent(...args)
})
return returnValue
}

Object.keys(dtlFireEvent).forEach(key => {
fireEvent[key] = (...args) => {
let returnValue
act(() => {
returnValue = dtlFireEvent[key](...args)
})
return returnValue
}
})

// React event system tracks native mouseOver/mouseOut events for
// running onMouseEnter/onMouseLeave handlers
// @link https://github.com/facebook/react/blob/b87aabdfe1b7461e7331abb3601d9e6bb27544bc/packages/react-dom/src/events/EnterLeaveEventPlugin.js#L24-L31
Expand All @@ -102,6 +130,6 @@ fireEvent.select = (node, init) => {

// just re-export everything from dom-testing-library
export * from 'dom-testing-library'
export {render, testHook, cleanup, flushEffects}
export {render, testHook, cleanup, fireEvent, act}

/* eslint func-name-matching:0 */
6 changes: 4 additions & 2 deletions typings/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ export function testHook(callback: () => void): void
export function cleanup(): void

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

0 comments on commit 8e08ccf

Please sign in to comment.