diff --git a/.all-contributorsrc b/.all-contributorsrc index be8f304a..dc53c0ec 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -183,6 +183,17 @@ "contributions": [ "test" ] + }, + { + "login": "huchenme", + "name": "Hu Chen", + "avatar_url": "https://avatars3.githubusercontent.com/u/2078389?v=4", + "profile": "https://huchen.dev/", + "contributions": [ + "code", + "doc", + "example" + ] } ], "commitConvention": "none" diff --git a/README.md b/README.md index e40d2ad9..edef6733 100644 --- a/README.md +++ b/README.md @@ -164,11 +164,13 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
Roman Gusev

πŸ“–
Adam Seckel

πŸ’»
keiya sasaki

⚠️ +
Hu Chen

πŸ’» πŸ“– πŸ’‘ - + + This project follows the [all-contributors](https://allcontributors.org/) specification. diff --git a/docs/api-reference.md b/docs/api-reference.md index 53c7623a..e98f6e92 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -10,6 +10,7 @@ route: '/reference/api' - [`renderHook`](/reference/api#renderhook) - [`act`](/reference/api#act) - [`cleanup`](/reference/api#cleanup) +- [`addCleanup`](/reference/api#addcleanup) --- @@ -108,7 +109,9 @@ This is the same [`act` function](https://reactjs.org/docs/test-utils.html#act) function cleanup: Promise ``` -Unmounts any rendered hooks rendered with `renderHook`, ensuring all effects have been flushed. +Unmounts any rendered hooks rendered with `renderHook`, ensuring all effects have been flushed. Any +callbacks added with [`addCleanup`](<(/reference/api#addcleanup).>) will also be called when +`cleanup` is run. > Please note that this is done automatically if the testing framework you're using supports the > `afterEach` global (like Jest, mocha and Jasmine). If not, you will need to do manual cleanups @@ -147,6 +150,35 @@ variable to `true` before importing `@testing-library/react-hooks` will also dis --- +## `addCleanup` + +```js +function addCleanup(callback: function(): void|Promise): function(): void +``` + +Add a callback to be called during [`cleanup`](/reference/api#cleanup), returning a function to +remove the cleanup if is no longer required. Cleanups are called in reverse order to being added. +This is usually only relevant when wanting a cleanup to run after the component has been unmounted. + +If the provided callback is an `async` function or returns a promise, `cleanup` will wait for it to +be resolved before moving onto the next cleanup callback. + +> Please note that any cleanups added using `addCleanup` are removed after `cleanup` is called. For +> cleanups that need to run with every test, it is advised to add them in a `beforeEach` block (or +> equivalent for your test runner). + +## `removeCleanup` + +```js +function removeCleanup(callback: function(): void|Promise): void +``` + +Removes a cleanup callback previously added with [`addCleanup`](/reference/api#addCleanup). Once +removed, the provided callback will no longer execute as part of running +[`cleanup`](/reference/api#cleanup). + +--- + ## Async Utilities ### `waitForNextUpdate` diff --git a/src/cleanup.js b/src/cleanup.js index c240b5e1..3699180a 100644 --- a/src/cleanup.js +++ b/src/cleanup.js @@ -4,12 +4,15 @@ let cleanupCallbacks = [] async function cleanup() { await flushMicroTasks() - cleanupCallbacks.forEach((cb) => cb()) + for (const callback of cleanupCallbacks) { + await callback() + } cleanupCallbacks = [] } function addCleanup(callback) { - cleanupCallbacks.push(callback) + cleanupCallbacks.unshift(callback) + return () => removeCleanup(callback) } function removeCleanup(callback) { diff --git a/src/pure.js b/src/pure.js index 3b4a475d..53ef84c5 100644 --- a/src/pure.js +++ b/src/pure.js @@ -99,4 +99,4 @@ function renderHook(callback, { initialProps, wrapper } = {}) { } } -export { renderHook, cleanup, act } +export { renderHook, cleanup, addCleanup, removeCleanup, act } diff --git a/test/cleanup.test.js b/test/cleanup.test.js index a8c3bbba..8e7a44b0 100644 --- a/test/cleanup.test.js +++ b/test/cleanup.test.js @@ -1,5 +1,5 @@ import { useEffect } from 'react' -import { renderHook, cleanup } from 'src' +import { renderHook, cleanup, addCleanup, removeCleanup } from 'src/pure' describe('cleanup tests', () => { test('should flush effects on cleanup', async () => { @@ -38,4 +38,98 @@ describe('cleanup tests', () => { expect(cleanupCalled[1]).toBe(true) expect(cleanupCalled[2]).toBe(true) }) + + test('should call cleanups in reverse order', async () => { + let callSequence = [] + addCleanup(() => { + callSequence.push('cleanup') + }) + addCleanup(() => { + callSequence.push('another cleanup') + }) + const hookWithCleanup = () => { + useEffect(() => { + return () => { + callSequence.push('unmount') + } + }) + } + renderHook(() => hookWithCleanup()) + + await cleanup() + + expect(callSequence).toEqual(['unmount', 'another cleanup', 'cleanup']) + }) + + test('should wait for async cleanup', async () => { + let callSequence = [] + addCleanup(() => { + callSequence.push('cleanup') + }) + addCleanup(async () => { + await new Promise((resolve) => setTimeout(resolve, 10)) + callSequence.push('another cleanup') + }) + const hookWithCleanup = () => { + useEffect(() => { + return () => { + callSequence.push('unmount') + } + }) + } + renderHook(() => hookWithCleanup()) + + await cleanup() + + expect(callSequence).toEqual(['unmount', 'another cleanup', 'cleanup']) + }) + + test('should remove cleanup using removeCleanup', async () => { + let callSequence = [] + addCleanup(() => { + callSequence.push('cleanup') + }) + const anotherCleanup = () => { + callSequence.push('another cleanup') + } + addCleanup(anotherCleanup) + const hookWithCleanup = () => { + useEffect(() => { + return () => { + callSequence.push('unmount') + } + }) + } + renderHook(() => hookWithCleanup()) + + removeCleanup(anotherCleanup) + + await cleanup() + + expect(callSequence).toEqual(['unmount', 'cleanup']) + }) + + test('should remove cleanup using returned handler', async () => { + let callSequence = [] + addCleanup(() => { + callSequence.push('cleanup') + }) + const remove = addCleanup(() => { + callSequence.push('another cleanup') + }) + const hookWithCleanup = () => { + useEffect(() => { + return () => { + callSequence.push('unmount') + } + }) + } + renderHook(() => hookWithCleanup()) + + remove() + + await cleanup() + + expect(callSequence).toEqual(['unmount', 'cleanup']) + }) })

AltStyle γ«γ‚ˆγ£γ¦ε€‰ζ›γ•γ‚ŒγŸγƒšγƒΌγ‚Έ (->γ‚ͺγƒͺγ‚ΈγƒŠγƒ«) /