diff --git a/package.json b/package.json index b326a1a6..84e734d3 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", @@ -42,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", 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 new file mode 100644 index 00000000..e5ef35ae --- /dev/null +++ b/src/__tests__/auto-cleanup-skip.js @@ -0,0 +1,18 @@ +import React from 'react' + +let render +beforeAll(() => { + process.env.RTL_SKIP_AUTO_CLEANUP = 'true' + const rtl = require('../') + render = rtl.render +}) + +// 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
) +}) + +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..5e05b725 100644 --- a/src/index.js +++ b/src/index.js @@ -1,153 +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) +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 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() }) - 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 */ +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 */ 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() -})