Skip to content

Commit 09c2fdd

Browse files
authored
Merge pull request #199 from testing-library/pr/cleanup
Global cleanup
2 parents d80825d + 217e299 commit 09c2fdd

7 files changed

+180
-6
lines changed

Diff for: docs/api-reference.md

+21
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ route: '/reference/api'
99

1010
- [`renderHook`](/reference/api#renderhook)
1111
- [`act`](/reference/api#act)
12+
- [`cleanup`](/reference/api#cleanup)
1213

1314
---
1415

@@ -102,3 +103,23 @@ A function to unmount the test component. This is commonly used to trigger clean
102103

103104
This is the same [`act` function](https://reactjs.org/docs/test-utils.html#act) that is exported by
104105
`react-test-renderer`.
106+
107+
---
108+
109+
## `cleanup`
110+
111+
```js
112+
function cleanup: Promise<void>
113+
```
114+
115+
Unmounts any rendered hooks rendered with `renderHook`, ensuring all effects have been flushed.
116+
117+
> Please note that this is done automatically if the testing framework you're using supports the
118+
> `afterEach` global (like mocha, Jest, and Jasmine). If not, you will need to do manual cleanups
119+
> after each test.
120+
>
121+
> Setting the `RHTL_SKIP_AUTO_CLEANUP` environment variable to `true` before the
122+
> `@testing-library/react-hooks` is imported will disable this feature.
123+
124+
The `cleanup` function should be called after each test to ensure that previously rendered hooks
125+
will not have any unintended side-effects on the following tests.

Diff for: src/cleanup.js

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { act } from 'react-test-renderer'
2+
3+
let cleanupCallbacks = []
4+
5+
async function cleanup() {
6+
await act(async () => {})
7+
cleanupCallbacks.forEach((cb) => cb())
8+
cleanupCallbacks = []
9+
}
10+
11+
function addCleanup(callback) {
12+
cleanupCallbacks.push(callback)
13+
}
14+
15+
function removeCleanup(callback) {
16+
cleanupCallbacks = cleanupCallbacks.filter((cb) => cb !== callback)
17+
}
18+
19+
// Automatically registers cleanup in supported testing frameworks
20+
if (typeof afterEach === 'function' && !process.env.RHTL_SKIP_AUTO_CLEANUP) {
21+
afterEach(async () => {
22+
await cleanup()
23+
})
24+
}
25+
26+
export { cleanup, addCleanup, removeCleanup }

Diff for: src/index.js

+12-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React, { Suspense } from 'react'
22
import { act, create } from 'react-test-renderer'
3+
import { cleanup, addCleanup, removeCleanup } from './cleanup'
34

45
function TestHook({ callback, hookProps, onError, children }) {
56
try {
@@ -73,6 +74,15 @@ function renderHook(callback, { initialProps, wrapper } = {}) {
7374
})
7475
const { unmount, update } = testRenderer
7576

77+
function unmountHook() {
78+
act(() => {
79+
removeCleanup(unmountHook)
80+
unmount()
81+
})
82+
}
83+
84+
addCleanup(unmountHook)
85+
7686
let waitingForNextUpdate = null
7787
const resolveOnNextUpdate = (resolve) => {
7888
addResolver((...args) => {
@@ -93,12 +103,8 @@ function renderHook(callback, { initialProps, wrapper } = {}) {
93103
update(toRender())
94104
})
95105
},
96-
unmount: () => {
97-
act(() => {
98-
unmount()
99-
})
100-
}
106+
unmount: unmountHook
101107
}
102108
}
103109

104-
export { renderHook, act }
110+
export { renderHook, cleanup, act }

Diff for: test/autoCleanup.disabled.test.js

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { useEffect } from 'react'
2+
3+
// This verifies that if RHTL_SKIP_AUTO_CLEANUP is set
4+
// then we DON'T auto-wire up the afterEach for folks
5+
describe('skip auto cleanup (disabled) tests', () => {
6+
let cleanupCalled = false
7+
let renderHook
8+
9+
beforeAll(() => {
10+
process.env.RHTL_SKIP_AUTO_CLEANUP = 'true'
11+
renderHook = require('src').renderHook
12+
})
13+
14+
test('first', () => {
15+
const hookWithCleanup = () => {
16+
useEffect(() => {
17+
return () => {
18+
cleanupCalled = true
19+
}
20+
})
21+
}
22+
renderHook(() => hookWithCleanup())
23+
})
24+
25+
test('second', () => {
26+
expect(cleanupCalled).toBe(false)
27+
})
28+
})

Diff for: test/autoCleanup.noAfterEach.test.js

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { useEffect } from 'react'
2+
3+
// This verifies that if RHTL_SKIP_AUTO_CLEANUP is set
4+
// then we DON'T auto-wire up the afterEach for folks
5+
describe('skip auto cleanup (no afterEach) tests', () => {
6+
let cleanupCalled = false
7+
let renderHook
8+
9+
beforeAll(() => {
10+
afterEach = false
11+
renderHook = require('src').renderHook
12+
})
13+
14+
test('first', () => {
15+
const hookWithCleanup = () => {
16+
useEffect(() => {
17+
return () => {
18+
cleanupCalled = true
19+
}
20+
})
21+
}
22+
renderHook(() => hookWithCleanup())
23+
})
24+
25+
test('second', () => {
26+
expect(cleanupCalled).toBe(false)
27+
})
28+
})

Diff for: test/autoCleanup.test.js

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { useEffect } from 'react'
2+
import { renderHook } from 'src'
3+
4+
// This verifies that by importing RHTL in an
5+
// environment which supports afterEach (like jest)
6+
// we'll get automatic cleanup between tests.
7+
describe('auto cleanup tests', () => {
8+
let cleanupCalled = false
9+
10+
test('first', () => {
11+
const hookWithCleanup = () => {
12+
useEffect(() => {
13+
return () => {
14+
cleanupCalled = true
15+
}
16+
})
17+
}
18+
renderHook(() => hookWithCleanup())
19+
})
20+
21+
test('second', () => {
22+
expect(cleanupCalled).toBe(true)
23+
})
24+
})

Diff for: test/cleanup.test.js

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { useEffect } from 'react'
2+
import { renderHook, cleanup } from 'src'
3+
4+
describe('cleanup tests', () => {
5+
test('should flush effects on cleanup', async () => {
6+
let cleanupCalled = false
7+
8+
const hookWithCleanup = () => {
9+
useEffect(() => {
10+
return () => {
11+
cleanupCalled = true
12+
}
13+
})
14+
}
15+
16+
renderHook(() => hookWithCleanup())
17+
18+
await cleanup()
19+
20+
expect(cleanupCalled).toBe(true)
21+
})
22+
23+
test('should cleanup all rendered hooks', async () => {
24+
let cleanupCalled = []
25+
const hookWithCleanup = (id) => {
26+
useEffect(() => {
27+
return () => {
28+
cleanupCalled[id] = true
29+
}
30+
})
31+
}
32+
33+
renderHook(() => hookWithCleanup(1))
34+
renderHook(() => hookWithCleanup(2))
35+
36+
await cleanup()
37+
38+
expect(cleanupCalled[1]).toBe(true)
39+
expect(cleanupCalled[2]).toBe(true)
40+
})
41+
})

0 commit comments

Comments
 (0)