From 6862dd130a206432a75b579c8e2c99ca2b5fdd28 Mon Sep 17 00:00:00 2001 From: "Kent C. Dodds" Date: Thu, 8 Aug 2019 15:50:51 -0600 Subject: [PATCH 1/4] feat(cleanup): automatically cleanup if afterEach is detected You can disable this with the RTL_SKIP_CLEANUP environment variable if you so choose, but it's recommended to have cleanup work this way. Closes #428 --- src/__tests__/auto-cleanup-skip.js | 18 ++++++++++++++++++ src/__tests__/auto-cleanup.js | 13 +++++++++++++ src/index.js | 10 ++++++++++ tests/setup-env.js | 5 ----- 4 files changed, 41 insertions(+), 5 deletions(-) create mode 100644 src/__tests__/auto-cleanup-skip.js create mode 100644 src/__tests__/auto-cleanup.js diff --git a/src/__tests__/auto-cleanup-skip.js b/src/__tests__/auto-cleanup-skip.js new file mode 100644 index 00000000..4e58d6c5 --- /dev/null +++ b/src/__tests__/auto-cleanup-skip.js @@ -0,0 +1,18 @@ +import React from 'react' + +let render +beforeAll(() => { + process.env.RTL_SKIP_CLEANUP = 'true' + const rtl = require('../') + render = rtl.render +}) + +// This one verifies that if RTL_SKIP_CLEANUP is set +// that we DON'T auto-wire up the afterEach for folks +test('first', () => { + render(
hi
) +}) + +test('second', () => { + expect(document.body.innerHTML).toEqual('
hi
') +}) diff --git a/src/__tests__/auto-cleanup.js b/src/__tests__/auto-cleanup.js new file mode 100644 index 00000000..1d0a7954 --- /dev/null +++ b/src/__tests__/auto-cleanup.js @@ -0,0 +1,13 @@ +import React from 'react' +import {render} from '../' + +// This just verifies that by importing RTL in an +// environment which supports afterEach (like jest) +// we'll get automatic cleanup between tests. +test('first', () => { + render(
hi
) +}) + +test('second', () => { + expect(document.body.innerHTML).toEqual('') +}) diff --git a/src/index.js b/src/index.js index 4565074b..f6e1c1ef 100644 --- a/src/index.js +++ b/src/index.js @@ -142,6 +142,16 @@ fireEvent.select = (node, init) => { fireEvent.keyUp(node, init) } +// if we're running in a test runner that supports afterEach +// then we'll automatically run cleanup afterEach test +// this ensures that tests run in isolation from each other +if (typeof afterEach === 'function' && !process.env.RTL_SKIP_CLEANUP) { + afterEach(async () => { + await asyncAct(async () => {}) + cleanup() + }) +} + // just re-export everything from dom-testing-library export * from '@testing-library/dom' export {render, cleanup, fireEvent, act} diff --git a/tests/setup-env.js b/tests/setup-env.js index d1d6d891..264828a9 100644 --- a/tests/setup-env.js +++ b/tests/setup-env.js @@ -1,6 +1 @@ import '@testing-library/jest-dom/extend-expect' - -afterEach(() => { - // have to do a dynamic import so we don't mess up jest mocking for old-act.js - require('../src').cleanup() -}) From c5f9a12a91dcadfa28ea5c74752ee1800fb28079 Mon Sep 17 00:00:00 2001 From: "Kent C. Dodds" Date: Fri, 9 Aug 2019 12:20:07 -0600 Subject: [PATCH 2/4] Update src/__tests__/auto-cleanup-skip.js MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: AdriĆ  Fontcuberta --- src/__tests__/auto-cleanup-skip.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/__tests__/auto-cleanup-skip.js b/src/__tests__/auto-cleanup-skip.js index 4e58d6c5..c52d741b 100644 --- a/src/__tests__/auto-cleanup-skip.js +++ b/src/__tests__/auto-cleanup-skip.js @@ -8,7 +8,7 @@ beforeAll(() => { }) // This one verifies that if RTL_SKIP_CLEANUP is set -// that we DON'T auto-wire up the afterEach for folks +// then we DON'T auto-wire up the afterEach for folks test('first', () => { render(
hi
) }) From 0ffbd8586f7631a77804c92870294fa2a2d6dd36 Mon Sep 17 00:00:00 2001 From: "Kent C. Dodds" Date: Thu, 8 Aug 2019 15:50:51 -0600 Subject: [PATCH 3/4] feat(cleanup): automatically cleanup if afterEach is detected You can disable this with the RTL_SKIP_CLEANUP environment variable if you so choose, but it's recommended to have cleanup work this way. Closes #428 BREAKING CHANGE: If your tests were not isolated before (and you referenced the same component between tests) then this change will break your tests. You should [keep your tests isolated](https://kentcdodds.com/blog/test-isolation-with-react), but if you're unable/unwilling to do that right away, then you can either run your tests with the environment variable `RTL_SKIP_AUTO_CLEANUP` set to `true` or import `@testing-library/react/pure` instead of `@testing-library/react`. --- package.json | 3 +- pure.js | 2 + src/__tests__/auto-cleanup-skip.js | 4 +- src/index.js | 159 ++--------------------------- src/pure.js | 153 +++++++++++++++++++++++++++ 5 files changed, 165 insertions(+), 156 deletions(-) create mode 100644 pure.js create mode 100644 src/pure.js diff --git a/package.json b/package.json index b326a1a6..d27b209a 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,8 @@ "files": [ "dist", "typings", - "cleanup-after-each.js" + "cleanup-after-each.js", + "pure.js" ], "keywords": [ "testing", diff --git a/pure.js b/pure.js new file mode 100644 index 00000000..75dc0452 --- /dev/null +++ b/pure.js @@ -0,0 +1,2 @@ +// makes it so people can import from '@testing-library/react/pure' +module.exports = require('./dist/pure') diff --git a/src/__tests__/auto-cleanup-skip.js b/src/__tests__/auto-cleanup-skip.js index c52d741b..e5ef35ae 100644 --- a/src/__tests__/auto-cleanup-skip.js +++ b/src/__tests__/auto-cleanup-skip.js @@ -2,12 +2,12 @@ import React from 'react' let render beforeAll(() => { - process.env.RTL_SKIP_CLEANUP = 'true' + process.env.RTL_SKIP_AUTO_CLEANUP = 'true' const rtl = require('../') render = rtl.render }) -// This one verifies that if RTL_SKIP_CLEANUP is set +// This one verifies that if RTL_SKIP_AUTO_CLEANUP is set // then we DON'T auto-wire up the afterEach for folks test('first', () => { render(
hi
) diff --git a/src/index.js b/src/index.js index f6e1c1ef..5e05b725 100644 --- a/src/index.js +++ b/src/index.js @@ -1,163 +1,16 @@ -import React from 'react' -import ReactDOM from 'react-dom' -import { - getQueriesForElement, - prettyDOM, - fireEvent as dtlFireEvent, - configure as configureDTL, -} from '@testing-library/dom' -import act, {asyncAct} from './act-compat' - -configureDTL({ - asyncWrapper: async cb => { - let result - await asyncAct(async () => { - result = await cb() - }) - return result - }, -}) - -const mountedContainers = new Set() - -function render( - ui, - { - container, - baseElement = container, - queries, - hydrate = false, - wrapper: WrapperComponent, - } = {}, -) { - if (!baseElement) { - // default to document.body instead of documentElement to avoid output of potentially-large - // head elements (such as JSS style blocks) in debug output - baseElement = document.body - } - if (!container) { - container = baseElement.appendChild(document.createElement('div')) - } - - // we'll add it to the mounted containers regardless of whether it's actually - // added to document.body so the cleanup method works regardless of whether - // they're passing us a custom container or not. - mountedContainers.add(container) - - const wrapUiIfNeeded = innerElement => - WrapperComponent - ? React.createElement(WrapperComponent, null, innerElement) - : innerElement - - act(() => { - if (hydrate) { - ReactDOM.hydrate(wrapUiIfNeeded(ui), container) - } else { - ReactDOM.render(wrapUiIfNeeded(ui), container) - } - }) - - return { - container, - baseElement, - // eslint-disable-next-line no-console - debug: (el = baseElement) => console.log(prettyDOM(el)), - unmount: () => ReactDOM.unmountComponentAtNode(container), - rerender: rerenderUi => { - render(wrapUiIfNeeded(rerenderUi), {container, baseElement}) - // Intentionally do not return anything to avoid unnecessarily complicating the API. - // folks can use all the same utilities we return in the first place that are bound to the container - }, - asFragment: () => { - /* istanbul ignore if (jsdom limitation) */ - if (typeof document.createRange === 'function') { - return document - .createRange() - .createContextualFragment(container.innerHTML) - } - - const template = document.createElement('template') - template.innerHTML = container.innerHTML - return template.content - }, - ...getQueriesForElement(baseElement, queries), - } -} - -function cleanup() { - mountedContainers.forEach(cleanupAtContainer) -} - -// 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) { - ReactDOM.unmountComponentAtNode(container) - if (container.parentNode === document.body) { - document.body.removeChild(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 -fireEvent.mouseEnter = fireEvent.mouseOver -fireEvent.mouseLeave = fireEvent.mouseOut - -fireEvent.select = (node, init) => { - // React tracks this event only on focused inputs - node.focus() - - // React creates this event when one of the following native events happens - // - contextMenu - // - mouseUp - // - dragEnd - // - keyUp - // - keyDown - // so we can use any here - // @link https://github.com/facebook/react/blob/b87aabdfe1b7461e7331abb3601d9e6bb27544bc/packages/react-dom/src/events/SelectEventPlugin.js#L203-L224 - fireEvent.keyUp(node, init) -} +import {asyncAct} from './act-compat' +import {cleanup} from './pure' // if we're running in a test runner that supports afterEach // then we'll automatically run cleanup afterEach test // this ensures that tests run in isolation from each other -if (typeof afterEach === 'function' && !process.env.RTL_SKIP_CLEANUP) { +// if you don't like this then either import the `pure` module +// or set the RTL_SKIP_AUTO_CLEANUP env variable to 'true'. +if (typeof afterEach === 'function' && !process.env.RTL_SKIP_AUTO_CLEANUP) { afterEach(async () => { await asyncAct(async () => {}) cleanup() }) } -// just re-export everything from dom-testing-library -export * from '@testing-library/dom' -export {render, cleanup, fireEvent, act} - -// NOTE: we're not going to export asyncAct because that's our own compatibility -// thing for people using react-dom@16.8.0. Anyone else doesn't need it and -// people should just upgrade anyway. - -/* eslint func-name-matching:0 */ +export * from './pure' diff --git a/src/pure.js b/src/pure.js new file mode 100644 index 00000000..4565074b --- /dev/null +++ b/src/pure.js @@ -0,0 +1,153 @@ +import React from 'react' +import ReactDOM from 'react-dom' +import { + getQueriesForElement, + prettyDOM, + fireEvent as dtlFireEvent, + configure as configureDTL, +} from '@testing-library/dom' +import act, {asyncAct} from './act-compat' + +configureDTL({ + asyncWrapper: async cb => { + let result + await asyncAct(async () => { + result = await cb() + }) + return result + }, +}) + +const mountedContainers = new Set() + +function render( + ui, + { + container, + baseElement = container, + queries, + hydrate = false, + wrapper: WrapperComponent, + } = {}, +) { + if (!baseElement) { + // default to document.body instead of documentElement to avoid output of potentially-large + // head elements (such as JSS style blocks) in debug output + baseElement = document.body + } + if (!container) { + container = baseElement.appendChild(document.createElement('div')) + } + + // we'll add it to the mounted containers regardless of whether it's actually + // added to document.body so the cleanup method works regardless of whether + // they're passing us a custom container or not. + mountedContainers.add(container) + + const wrapUiIfNeeded = innerElement => + WrapperComponent + ? React.createElement(WrapperComponent, null, innerElement) + : innerElement + + act(() => { + if (hydrate) { + ReactDOM.hydrate(wrapUiIfNeeded(ui), container) + } else { + ReactDOM.render(wrapUiIfNeeded(ui), container) + } + }) + + return { + container, + baseElement, + // eslint-disable-next-line no-console + debug: (el = baseElement) => console.log(prettyDOM(el)), + unmount: () => ReactDOM.unmountComponentAtNode(container), + rerender: rerenderUi => { + render(wrapUiIfNeeded(rerenderUi), {container, baseElement}) + // Intentionally do not return anything to avoid unnecessarily complicating the API. + // folks can use all the same utilities we return in the first place that are bound to the container + }, + asFragment: () => { + /* istanbul ignore if (jsdom limitation) */ + if (typeof document.createRange === 'function') { + return document + .createRange() + .createContextualFragment(container.innerHTML) + } + + const template = document.createElement('template') + template.innerHTML = container.innerHTML + return template.content + }, + ...getQueriesForElement(baseElement, queries), + } +} + +function cleanup() { + mountedContainers.forEach(cleanupAtContainer) +} + +// 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) { + ReactDOM.unmountComponentAtNode(container) + if (container.parentNode === document.body) { + document.body.removeChild(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 +fireEvent.mouseEnter = fireEvent.mouseOver +fireEvent.mouseLeave = fireEvent.mouseOut + +fireEvent.select = (node, init) => { + // React tracks this event only on focused inputs + node.focus() + + // React creates this event when one of the following native events happens + // - contextMenu + // - mouseUp + // - dragEnd + // - keyUp + // - keyDown + // so we can use any here + // @link https://github.com/facebook/react/blob/b87aabdfe1b7461e7331abb3601d9e6bb27544bc/packages/react-dom/src/events/SelectEventPlugin.js#L203-L224 + fireEvent.keyUp(node, init) +} + +// just re-export everything from dom-testing-library +export * from '@testing-library/dom' +export {render, cleanup, fireEvent, act} + +// NOTE: we're not going to export asyncAct because that's our own compatibility +// thing for people using react-dom@16.8.0. Anyone else doesn't need it and +// people should just upgrade anyway. + +/* eslint func-name-matching:0 */ From c23151199b55f646e4a69af16bac40387d954234 Mon Sep 17 00:00:00 2001 From: "Kent C. Dodds" Date: Fri, 9 Aug 2019 13:45:48 -0600 Subject: [PATCH 4/4] feat(deps): update @testing-library/dom to 6.0.0 BREAKING CHANGE: If you were using `debugDOM` before, use `prettyDOM` instead. Note that `debugDOM` is different from `debug` which you get back from `render`. That is unchanged. --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index d27b209a..84e734d3 100644 --- a/package.json +++ b/package.json @@ -43,12 +43,12 @@ "license": "MIT", "dependencies": { "@babel/runtime": "^7.5.5", - "@testing-library/dom": "^5.6.1" + "@testing-library/dom": "^6.0.0" }, "devDependencies": { "@reach/router": "^1.2.1", "@testing-library/jest-dom": "^4.0.0", - "@types/react": "^16.8.25", + "@types/react": "^16.9.1", "@types/react-dom": "^16.8.5", "kcd-scripts": "^1.5.2", "react": "^16.9.0",