From 183c3cefa1e7635e714245d63c5dc1c1a7a7a3d8 Mon Sep 17 00:00:00 2001 From: Michael Peyper Date: Thu, 10 Oct 2019 21:53:58 +1100 Subject: [PATCH 1/4] Added cleanup and autocleanup functionality --- src/index.js | 33 ++++++++++++++++++---- test/autoCleanup.disabled.test.js | 28 +++++++++++++++++++ test/autoCleanup.noAfterEach.test.js | 28 +++++++++++++++++++ test/autoCleanup.test.js | 24 ++++++++++++++++ test/cleanup.test.js | 41 ++++++++++++++++++++++++++++ 5 files changed, 148 insertions(+), 6 deletions(-) create mode 100644 test/autoCleanup.disabled.test.js create mode 100644 test/autoCleanup.noAfterEach.test.js create mode 100644 test/autoCleanup.test.js create mode 100644 test/cleanup.test.js diff --git a/src/index.js b/src/index.js index f4411f5b..354ea3f9 100644 --- a/src/index.js +++ b/src/index.js @@ -1,6 +1,8 @@ import React, { Suspense } from 'react' import { act, create } from 'react-test-renderer' +let cleanupCallbacks = [] + function TestHook({ callback, hookProps, onError, children }) { try { children(callback(hookProps)) @@ -73,6 +75,15 @@ function renderHook(callback, { initialProps, wrapper } = {}) { }) const { unmount, update } = testRenderer + function unmountHook() { + act(() => { + cleanupCallbacks = cleanupCallbacks.filter((cb) => cb !== unmountHook) + unmount() + }) + } + + cleanupCallbacks.push(unmountHook) + let waitingForNextUpdate = null const resolveOnNextUpdate = (resolve) => { addResolver((...args) => { @@ -93,12 +104,22 @@ function renderHook(callback, { initialProps, wrapper } = {}) { update(toRender()) }) }, - unmount: () => { - act(() => { - unmount() - }) - } + unmount: unmountHook } } -export { renderHook, act } +async function cleanup() { + await act(async () => { + await act(async () => {}) + cleanupCallbacks.forEach((cb) => cb()) + cleanupCallbacks = [] + }) +} + +if (typeof afterEach === 'function' && !process.env.RHTL_SKIP_AUTO_CLEANUP) { + afterEach(async () => { + await cleanup() + }) +} + +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) + }) +}) From ba02be0caa2cd8241a8597484998b19f341df887 Mon Sep 17 00:00:00 2001 From: Michael Peyper Date: Thu, 10 Oct 2019 22:10:25 +1100 Subject: [PATCH 2/4] Documented the cleanup and auto-cleanup functionality --- docs/api-reference.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/docs/api-reference.md b/docs/api-reference.md index 337871e3..9568683f 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` + +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. + +```js +async function cleanup: void +``` + +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. From 73c1f6122f0c9bad3235460748fbc4d0aca77984 Mon Sep 17 00:00:00 2001 From: Michael Peyper Date: Mon, 14 Oct 2019 10:16:15 +1100 Subject: [PATCH 3/4] Moved cleanup logic into seperate file --- src/cleanup.js | 26 ++++++++++++++++++++++++++ src/index.js | 21 +++------------------ 2 files changed, 29 insertions(+), 18 deletions(-) create mode 100644 src/cleanup.js 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 354ea3f9..f33d362f 100644 --- a/src/index.js +++ b/src/index.js @@ -1,7 +1,6 @@ import React, { Suspense } from 'react' import { act, create } from 'react-test-renderer' - -let cleanupCallbacks = [] +import { cleanup, addCleanup, removeCleanup } from './cleanup' function TestHook({ callback, hookProps, onError, children }) { try { @@ -77,12 +76,12 @@ function renderHook(callback, { initialProps, wrapper } = {}) { function unmountHook() { act(() => { - cleanupCallbacks = cleanupCallbacks.filter((cb) => cb !== unmountHook) + removeCleanup(unmountHook) unmount() }) } - cleanupCallbacks.push(unmountHook) + addCleanup(unmountHook) let waitingForNextUpdate = null const resolveOnNextUpdate = (resolve) => { @@ -108,18 +107,4 @@ function renderHook(callback, { initialProps, wrapper } = {}) { } } -async function cleanup() { - await act(async () => { - await act(async () => {}) - cleanupCallbacks.forEach((cb) => cb()) - cleanupCallbacks = [] - }) -} - -if (typeof afterEach === 'function' && !process.env.RHTL_SKIP_AUTO_CLEANUP) { - afterEach(async () => { - await cleanup() - }) -} - export { renderHook, cleanup, act } From 217e29914557e8169c184525a44e2668253578f9 Mon Sep 17 00:00:00 2001 From: Michael Peyper Date: Mon, 14 Oct 2019 11:34:57 +1100 Subject: [PATCH 4/4] Moved cleanup definition in logs to match other API references --- docs/api-reference.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/api-reference.md b/docs/api-reference.md index 9568683f..33ca1c62 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -108,6 +108,10 @@ This is the same [`act` function](https://reactjs.org/docs/test-utils.html#act) ## `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 @@ -117,9 +121,5 @@ Unmounts any rendered hooks rendered with `renderHook`, ensuring all effects hav > Setting the `RHTL_SKIP_AUTO_CLEANUP` environment variable to `true` before the > `@testing-library/react-hooks` is imported will disable this feature. -```js -async function cleanup: void -``` - 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.