diff --git a/docs/api-reference.md b/docs/api-reference.md index 337871e3..33ca1c62 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -9,6 +9,7 @@ route: '/reference/api' - [`renderHook`](/reference/api#renderhook) - [`act`](/reference/api#act) +- [`cleanup`](/reference/api#cleanup) --- @@ -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 +``` + +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. diff --git a/src/cleanup.js b/src/cleanup.js new file mode 100644 index 00000000..c4f71e8e --- /dev/null +++ b/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 } diff --git a/src/index.js b/src/index.js index f4411f5b..f33d362f 100644 --- a/src/index.js +++ b/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 { @@ -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) => { @@ -93,12 +103,8 @@ function renderHook(callback, { initialProps, wrapper } = {}) { update(toRender()) }) }, - unmount: () => { - act(() => { - unmount() - }) - } + unmount: unmountHook } } -export { renderHook, act } +export { renderHook, cleanup, act } diff --git a/test/autoCleanup.disabled.test.js b/test/autoCleanup.disabled.test.js new file mode 100644 index 00000000..7da342d5 --- /dev/null +++ b/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) + }) +}) diff --git a/test/autoCleanup.noAfterEach.test.js b/test/autoCleanup.noAfterEach.test.js new file mode 100644 index 00000000..c1f51eea --- /dev/null +++ b/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) + }) +}) diff --git a/test/autoCleanup.test.js b/test/autoCleanup.test.js new file mode 100644 index 00000000..fc70d111 --- /dev/null +++ b/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) + }) +}) diff --git a/test/cleanup.test.js b/test/cleanup.test.js new file mode 100644 index 00000000..a8c3bbba --- /dev/null +++ b/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) + }) +})