From 5c353ab4e8b46fb7528352c8cead30e9b7144301 Mon Sep 17 00:00:00 2001 From: "Kent C. Dodds" Date: Tue, 5 Feb 2019 16:12:31 -0700 Subject: [PATCH] feat(act): Support ReactDOM.TestUtils.act This also removes `flushEffects` which is no longer necessary! --- README.md | 22 ++++++++--- package.json | 4 +- src/__tests__/act.js | 42 ++++++++++++++++++++ src/__tests__/no-act.js | 10 +++++ src/__tests__/render.js | 9 +---- src/__tests__/{testHook.js => test-hook.js} | 0 src/act-compat.js | 12 ++++++ src/index.js | 44 +++++++++++++++++---- typings/index.d.ts | 6 ++- 9 files changed, 123 insertions(+), 26 deletions(-) create mode 100644 src/__tests__/act.js create mode 100644 src/__tests__/no-act.js rename src/__tests__/{testHook.js => test-hook.js} (100%) create mode 100644 src/act-compat.js diff --git a/README.md b/README.md index ade2fa8b..4428ad81 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,16 @@

react-testing-library

-goat + goat -

Simple and complete React DOM testing utilities that encourage good testing practices.

+

Simple and complete React DOM testing utilities that encourage good testing +practices.

[**Read The Docs**](https://testing-library.com/react) | [Edit the docs](https://github.com/alexkrolick/testing-library-docs) @@ -30,9 +36,13 @@
- -TestingJavaScript.com Learn the smart, efficient way to test any JavaScript application. - + + TestingJavaScript.com Learn the smart, efficient way to test any JavaScript application. +
## Table of Contents @@ -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. diff --git a/package.json b/package.json index 3bb7c531..78091217 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/__tests__/act.js b/src/__tests__/act.js new file mode 100644 index 00000000..49729f3b --- /dev/null +++ b/src/__tests__/act.js @@ -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() + 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 + } + const { + container: {firstChild: buttonNode}, + } = render() + + 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(, {hydrate: true}) + expect(effectCb).toHaveBeenCalledTimes(1) +}) diff --git a/src/__tests__/no-act.js b/src/__tests__/no-act.js new file mode 100644 index 00000000..07ee091d --- /dev/null +++ b/src/__tests__/no-act.js @@ -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 */) +}) diff --git a/src/__tests__/render.js b/src/__tests__/render.js index f2ec2351..5ee0dc6f 100644 --- a/src/__tests__/render.js +++ b/src/__tests__/render.js @@ -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) @@ -90,10 +90,3 @@ it('supports fragments', () => { cleanup() expect(document.body.innerHTML).toBe('') }) - -test('flushEffects can be called without causing issues', () => { - render(
) - const preHtml = document.documentElement.innerHTML - flushEffects() - expect(document.documentElement.innerHTML).toBe(preHtml) -}) diff --git a/src/__tests__/testHook.js b/src/__tests__/test-hook.js similarity index 100% rename from src/__tests__/testHook.js rename to src/__tests__/test-hook.js diff --git a/src/act-compat.js b/src/act-compat.js new file mode 100644 index 00000000..65f85971 --- /dev/null +++ b/src/act-compat.js @@ -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 diff --git a/src/index.js b/src/index.js index 24b982ac..0f4c7d47 100644 --- a/src/index.js +++ b/src/index.js @@ -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() @@ -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, @@ -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) { @@ -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 @@ -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 */ diff --git a/typings/index.d.ts b/typings/index.d.ts index ce6408f3..c87c817c 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -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