Skip to content

Commit

Permalink
Merge pull request #199 from testing-library/pr/cleanup
Browse files Browse the repository at this point in the history
Global cleanup
  • Loading branch information
mpeyper committed Oct 16, 2019
2 parents d80825d + 217e299 commit 09c2fdd
Show file tree
Hide file tree
Showing 7 changed files with 180 additions and 6 deletions.
21 changes: 21 additions & 0 deletions docs/api-reference.md
Expand Up @@ -9,6 +9,7 @@ route: '/reference/api'

- [`renderHook`](/reference/api#renderhook)
- [`act`](/reference/api#act)
- [`cleanup`](/reference/api#cleanup)

---

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

This is the same [`act` function](https://reactjs.org/docs/test-utils.html#act) that is exported by
`react-test-renderer`.

---

## `cleanup`

```js
function cleanup: Promise<void>
```

Unmounts any rendered hooks rendered with `renderHook`, ensuring all effects have been flushed.

> Please note that this is done automatically if the testing framework you're using supports the
> `afterEach` global (like mocha, Jest, and Jasmine). If not, you will need to do manual cleanups
> after each test.
>
> Setting the `RHTL_SKIP_AUTO_CLEANUP` environment variable to `true` before the
> `@testing-library/react-hooks` is imported will disable this feature.

The `cleanup` function should be called after each test to ensure that previously rendered hooks
will not have any unintended side-effects on the following tests.
26 changes: 26 additions & 0 deletions src/cleanup.js
@@ -0,0 +1,26 @@
import { act } from 'react-test-renderer'

let cleanupCallbacks = []

async function cleanup() {
await act(async () => {})
cleanupCallbacks.forEach((cb) => cb())
cleanupCallbacks = []
}

function addCleanup(callback) {
cleanupCallbacks.push(callback)
}

function removeCleanup(callback) {
cleanupCallbacks = cleanupCallbacks.filter((cb) => cb !== callback)
}

// Automatically registers cleanup in supported testing frameworks
if (typeof afterEach === 'function' && !process.env.RHTL_SKIP_AUTO_CLEANUP) {
afterEach(async () => {
await cleanup()
})
}

export { cleanup, addCleanup, removeCleanup }
18 changes: 12 additions & 6 deletions src/index.js
@@ -1,5 +1,6 @@
import React, { Suspense } from 'react'
import { act, create } from 'react-test-renderer'
import { cleanup, addCleanup, removeCleanup } from './cleanup'

function TestHook({ callback, hookProps, onError, children }) {
try {
Expand Down Expand Up @@ -73,6 +74,15 @@ function renderHook(callback, { initialProps, wrapper } = {}) {
})
const { unmount, update } = testRenderer

function unmountHook() {
act(() => {
removeCleanup(unmountHook)
unmount()
})
}

addCleanup(unmountHook)

let waitingForNextUpdate = null
const resolveOnNextUpdate = (resolve) => {
addResolver((...args) => {
Expand All @@ -93,12 +103,8 @@ function renderHook(callback, { initialProps, wrapper } = {}) {
update(toRender())
})
},
unmount: () => {
act(() => {
unmount()
})
}
unmount: unmountHook
}
}

export { renderHook, act }
export { renderHook, cleanup, act }
28 changes: 28 additions & 0 deletions test/autoCleanup.disabled.test.js
@@ -0,0 +1,28 @@
import { useEffect } from 'react'

// This verifies that if RHTL_SKIP_AUTO_CLEANUP is set
// then we DON'T auto-wire up the afterEach for folks
describe('skip auto cleanup (disabled) tests', () => {
let cleanupCalled = false
let renderHook

beforeAll(() => {
process.env.RHTL_SKIP_AUTO_CLEANUP = 'true'
renderHook = require('src').renderHook
})

test('first', () => {
const hookWithCleanup = () => {
useEffect(() => {
return () => {
cleanupCalled = true
}
})
}
renderHook(() => hookWithCleanup())
})

test('second', () => {
expect(cleanupCalled).toBe(false)
})
})
28 changes: 28 additions & 0 deletions test/autoCleanup.noAfterEach.test.js
@@ -0,0 +1,28 @@
import { useEffect } from 'react'

// This verifies that if RHTL_SKIP_AUTO_CLEANUP is set
// then we DON'T auto-wire up the afterEach for folks
describe('skip auto cleanup (no afterEach) tests', () => {
let cleanupCalled = false
let renderHook

beforeAll(() => {
afterEach = false
renderHook = require('src').renderHook
})

test('first', () => {
const hookWithCleanup = () => {
useEffect(() => {
return () => {
cleanupCalled = true
}
})
}
renderHook(() => hookWithCleanup())
})

test('second', () => {
expect(cleanupCalled).toBe(false)
})
})
24 changes: 24 additions & 0 deletions test/autoCleanup.test.js
@@ -0,0 +1,24 @@
import { useEffect } from 'react'
import { renderHook } from 'src'

// This verifies that by importing RHTL in an
// environment which supports afterEach (like jest)
// we'll get automatic cleanup between tests.
describe('auto cleanup tests', () => {
let cleanupCalled = false

test('first', () => {
const hookWithCleanup = () => {
useEffect(() => {
return () => {
cleanupCalled = true
}
})
}
renderHook(() => hookWithCleanup())
})

test('second', () => {
expect(cleanupCalled).toBe(true)
})
})
41 changes: 41 additions & 0 deletions test/cleanup.test.js
@@ -0,0 +1,41 @@
import { useEffect } from 'react'
import { renderHook, cleanup } from 'src'

describe('cleanup tests', () => {
test('should flush effects on cleanup', async () => {
let cleanupCalled = false

const hookWithCleanup = () => {
useEffect(() => {
return () => {
cleanupCalled = true
}
})
}

renderHook(() => hookWithCleanup())

await cleanup()

expect(cleanupCalled).toBe(true)
})

test('should cleanup all rendered hooks', async () => {
let cleanupCalled = []
const hookWithCleanup = (id) => {
useEffect(() => {
return () => {
cleanupCalled[id] = true
}
})
}

renderHook(() => hookWithCleanup(1))
renderHook(() => hookWithCleanup(2))

await cleanup()

expect(cleanupCalled[1]).toBe(true)
expect(cleanupCalled[2]).toBe(true)
})
})

0 comments on commit 09c2fdd

Please sign in to comment.