Skip to content

Commit 0ffbd85

Browse files
author
Kent C. Dodds
committed
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`.
1 parent c5f9a12 commit 0ffbd85

File tree

5 files changed

+165
-156
lines changed

5 files changed

+165
-156
lines changed

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
"files": [
2525
"dist",
2626
"typings",
27-
"cleanup-after-each.js"
27+
"cleanup-after-each.js",
28+
"pure.js"
2829
],
2930
"keywords": [
3031
"testing",

pure.js

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// makes it so people can import from '@testing-library/react/pure'
2+
module.exports = require('./dist/pure')

src/__tests__/auto-cleanup-skip.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@ import React from 'react'
22

33
let render
44
beforeAll(() => {
5-
process.env.RTL_SKIP_CLEANUP = 'true'
5+
process.env.RTL_SKIP_AUTO_CLEANUP = 'true'
66
const rtl = require('../')
77
render = rtl.render
88
})
99

10-
// This one verifies that if RTL_SKIP_CLEANUP is set
10+
// This one verifies that if RTL_SKIP_AUTO_CLEANUP is set
1111
// then we DON'T auto-wire up the afterEach for folks
1212
test('first', () => {
1313
render(<div>hi</div>)

src/index.js

+6-153
Original file line numberDiff line numberDiff line change
@@ -1,163 +1,16 @@
1-
import React from 'react'
2-
import ReactDOM from 'react-dom'
3-
import {
4-
getQueriesForElement,
5-
prettyDOM,
6-
fireEvent as dtlFireEvent,
7-
configure as configureDTL,
8-
} from '@testing-library/dom'
9-
import act, {asyncAct} from './act-compat'
10-
11-
configureDTL({
12-
asyncWrapper: async cb => {
13-
let result
14-
await asyncAct(async () => {
15-
result = await cb()
16-
})
17-
return result
18-
},
19-
})
20-
21-
const mountedContainers = new Set()
22-
23-
function render(
24-
ui,
25-
{
26-
container,
27-
baseElement = container,
28-
queries,
29-
hydrate = false,
30-
wrapper: WrapperComponent,
31-
} = {},
32-
) {
33-
if (!baseElement) {
34-
// default to document.body instead of documentElement to avoid output of potentially-large
35-
// head elements (such as JSS style blocks) in debug output
36-
baseElement = document.body
37-
}
38-
if (!container) {
39-
container = baseElement.appendChild(document.createElement('div'))
40-
}
41-
42-
// we'll add it to the mounted containers regardless of whether it's actually
43-
// added to document.body so the cleanup method works regardless of whether
44-
// they're passing us a custom container or not.
45-
mountedContainers.add(container)
46-
47-
const wrapUiIfNeeded = innerElement =>
48-
WrapperComponent
49-
? React.createElement(WrapperComponent, null, innerElement)
50-
: innerElement
51-
52-
act(() => {
53-
if (hydrate) {
54-
ReactDOM.hydrate(wrapUiIfNeeded(ui), container)
55-
} else {
56-
ReactDOM.render(wrapUiIfNeeded(ui), container)
57-
}
58-
})
59-
60-
return {
61-
container,
62-
baseElement,
63-
// eslint-disable-next-line no-console
64-
debug: (el = baseElement) => console.log(prettyDOM(el)),
65-
unmount: () => ReactDOM.unmountComponentAtNode(container),
66-
rerender: rerenderUi => {
67-
render(wrapUiIfNeeded(rerenderUi), {container, baseElement})
68-
// Intentionally do not return anything to avoid unnecessarily complicating the API.
69-
// folks can use all the same utilities we return in the first place that are bound to the container
70-
},
71-
asFragment: () => {
72-
/* istanbul ignore if (jsdom limitation) */
73-
if (typeof document.createRange === 'function') {
74-
return document
75-
.createRange()
76-
.createContextualFragment(container.innerHTML)
77-
}
78-
79-
const template = document.createElement('template')
80-
template.innerHTML = container.innerHTML
81-
return template.content
82-
},
83-
...getQueriesForElement(baseElement, queries),
84-
}
85-
}
86-
87-
function cleanup() {
88-
mountedContainers.forEach(cleanupAtContainer)
89-
}
90-
91-
// maybe one day we'll expose this (perhaps even as a utility returned by render).
92-
// but let's wait until someone asks for it.
93-
function cleanupAtContainer(container) {
94-
ReactDOM.unmountComponentAtNode(container)
95-
if (container.parentNode === document.body) {
96-
document.body.removeChild(container)
97-
}
98-
mountedContainers.delete(container)
99-
}
100-
101-
// react-testing-library's version of fireEvent will call
102-
// dom-testing-library's version of fireEvent wrapped inside
103-
// an "act" call so that after all event callbacks have been
104-
// been called, the resulting useEffect callbacks will also
105-
// be called.
106-
function fireEvent(...args) {
107-
let returnValue
108-
act(() => {
109-
returnValue = dtlFireEvent(...args)
110-
})
111-
return returnValue
112-
}
113-
114-
Object.keys(dtlFireEvent).forEach(key => {
115-
fireEvent[key] = (...args) => {
116-
let returnValue
117-
act(() => {
118-
returnValue = dtlFireEvent[key](...args)
119-
})
120-
return returnValue
121-
}
122-
})
123-
124-
// React event system tracks native mouseOver/mouseOut events for
125-
// running onMouseEnter/onMouseLeave handlers
126-
// @link https://github.com/facebook/react/blob/b87aabdfe1b7461e7331abb3601d9e6bb27544bc/packages/react-dom/src/events/EnterLeaveEventPlugin.js#L24-L31
127-
fireEvent.mouseEnter = fireEvent.mouseOver
128-
fireEvent.mouseLeave = fireEvent.mouseOut
129-
130-
fireEvent.select = (node, init) => {
131-
// React tracks this event only on focused inputs
132-
node.focus()
133-
134-
// React creates this event when one of the following native events happens
135-
// - contextMenu
136-
// - mouseUp
137-
// - dragEnd
138-
// - keyUp
139-
// - keyDown
140-
// so we can use any here
141-
// @link https://github.com/facebook/react/blob/b87aabdfe1b7461e7331abb3601d9e6bb27544bc/packages/react-dom/src/events/SelectEventPlugin.js#L203-L224
142-
fireEvent.keyUp(node, init)
143-
}
1+
import {asyncAct} from './act-compat'
2+
import {cleanup} from './pure'
1443

1454
// if we're running in a test runner that supports afterEach
1465
// then we'll automatically run cleanup afterEach test
1476
// this ensures that tests run in isolation from each other
148-
if (typeof afterEach === 'function' && !process.env.RTL_SKIP_CLEANUP) {
7+
// if you don't like this then either import the `pure` module
8+
// or set the RTL_SKIP_AUTO_CLEANUP env variable to 'true'.
9+
if (typeof afterEach === 'function' && !process.env.RTL_SKIP_AUTO_CLEANUP) {
14910
afterEach(async () => {
15011
await asyncAct(async () => {})
15112
cleanup()
15213
})
15314
}
15415

155-
// just re-export everything from dom-testing-library
156-
export * from '@testing-library/dom'
157-
export {render, cleanup, fireEvent, act}
158-
159-
// NOTE: we're not going to export asyncAct because that's our own compatibility
160-
// thing for people using [email protected]. Anyone else doesn't need it and
161-
// people should just upgrade anyway.
162-
163-
/* eslint func-name-matching:0 */
16+
export * from './pure'

src/pure.js

+153
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import React from 'react'
2+
import ReactDOM from 'react-dom'
3+
import {
4+
getQueriesForElement,
5+
prettyDOM,
6+
fireEvent as dtlFireEvent,
7+
configure as configureDTL,
8+
} from '@testing-library/dom'
9+
import act, {asyncAct} from './act-compat'
10+
11+
configureDTL({
12+
asyncWrapper: async cb => {
13+
let result
14+
await asyncAct(async () => {
15+
result = await cb()
16+
})
17+
return result
18+
},
19+
})
20+
21+
const mountedContainers = new Set()
22+
23+
function render(
24+
ui,
25+
{
26+
container,
27+
baseElement = container,
28+
queries,
29+
hydrate = false,
30+
wrapper: WrapperComponent,
31+
} = {},
32+
) {
33+
if (!baseElement) {
34+
// default to document.body instead of documentElement to avoid output of potentially-large
35+
// head elements (such as JSS style blocks) in debug output
36+
baseElement = document.body
37+
}
38+
if (!container) {
39+
container = baseElement.appendChild(document.createElement('div'))
40+
}
41+
42+
// we'll add it to the mounted containers regardless of whether it's actually
43+
// added to document.body so the cleanup method works regardless of whether
44+
// they're passing us a custom container or not.
45+
mountedContainers.add(container)
46+
47+
const wrapUiIfNeeded = innerElement =>
48+
WrapperComponent
49+
? React.createElement(WrapperComponent, null, innerElement)
50+
: innerElement
51+
52+
act(() => {
53+
if (hydrate) {
54+
ReactDOM.hydrate(wrapUiIfNeeded(ui), container)
55+
} else {
56+
ReactDOM.render(wrapUiIfNeeded(ui), container)
57+
}
58+
})
59+
60+
return {
61+
container,
62+
baseElement,
63+
// eslint-disable-next-line no-console
64+
debug: (el = baseElement) => console.log(prettyDOM(el)),
65+
unmount: () => ReactDOM.unmountComponentAtNode(container),
66+
rerender: rerenderUi => {
67+
render(wrapUiIfNeeded(rerenderUi), {container, baseElement})
68+
// Intentionally do not return anything to avoid unnecessarily complicating the API.
69+
// folks can use all the same utilities we return in the first place that are bound to the container
70+
},
71+
asFragment: () => {
72+
/* istanbul ignore if (jsdom limitation) */
73+
if (typeof document.createRange === 'function') {
74+
return document
75+
.createRange()
76+
.createContextualFragment(container.innerHTML)
77+
}
78+
79+
const template = document.createElement('template')
80+
template.innerHTML = container.innerHTML
81+
return template.content
82+
},
83+
...getQueriesForElement(baseElement, queries),
84+
}
85+
}
86+
87+
function cleanup() {
88+
mountedContainers.forEach(cleanupAtContainer)
89+
}
90+
91+
// maybe one day we'll expose this (perhaps even as a utility returned by render).
92+
// but let's wait until someone asks for it.
93+
function cleanupAtContainer(container) {
94+
ReactDOM.unmountComponentAtNode(container)
95+
if (container.parentNode === document.body) {
96+
document.body.removeChild(container)
97+
}
98+
mountedContainers.delete(container)
99+
}
100+
101+
// react-testing-library's version of fireEvent will call
102+
// dom-testing-library's version of fireEvent wrapped inside
103+
// an "act" call so that after all event callbacks have been
104+
// been called, the resulting useEffect callbacks will also
105+
// be called.
106+
function fireEvent(...args) {
107+
let returnValue
108+
act(() => {
109+
returnValue = dtlFireEvent(...args)
110+
})
111+
return returnValue
112+
}
113+
114+
Object.keys(dtlFireEvent).forEach(key => {
115+
fireEvent[key] = (...args) => {
116+
let returnValue
117+
act(() => {
118+
returnValue = dtlFireEvent[key](...args)
119+
})
120+
return returnValue
121+
}
122+
})
123+
124+
// React event system tracks native mouseOver/mouseOut events for
125+
// running onMouseEnter/onMouseLeave handlers
126+
// @link https://github.com/facebook/react/blob/b87aabdfe1b7461e7331abb3601d9e6bb27544bc/packages/react-dom/src/events/EnterLeaveEventPlugin.js#L24-L31
127+
fireEvent.mouseEnter = fireEvent.mouseOver
128+
fireEvent.mouseLeave = fireEvent.mouseOut
129+
130+
fireEvent.select = (node, init) => {
131+
// React tracks this event only on focused inputs
132+
node.focus()
133+
134+
// React creates this event when one of the following native events happens
135+
// - contextMenu
136+
// - mouseUp
137+
// - dragEnd
138+
// - keyUp
139+
// - keyDown
140+
// so we can use any here
141+
// @link https://github.com/facebook/react/blob/b87aabdfe1b7461e7331abb3601d9e6bb27544bc/packages/react-dom/src/events/SelectEventPlugin.js#L203-L224
142+
fireEvent.keyUp(node, init)
143+
}
144+
145+
// just re-export everything from dom-testing-library
146+
export * from '@testing-library/dom'
147+
export {render, cleanup, fireEvent, act}
148+
149+
// NOTE: we're not going to export asyncAct because that's our own compatibility
150+
// thing for people using [email protected]. Anyone else doesn't need it and
151+
// people should just upgrade anyway.
152+
153+
/* eslint func-name-matching:0 */

0 commit comments

Comments
 (0)