From 2e7b588cfb10a491b09ef8c6091dfe7e94693ffa Mon Sep 17 00:00:00 2001 From: Michael Peyper Date: Tue, 19 Jan 2021 06:55:34 +1100 Subject: [PATCH 1/9] fix: only suppress console.error for non-pure imports Fixes #546 --- disable-error-filtering.js | 1 + docs/api-reference.md | 49 ++++++++++++++++- package.json | 1 + src/core/console.ts | 25 +++++++++ src/dom/__tests__/errorHook.test.ts | 53 +++++++++++++++++-- .../errorSuppression.disabled.test.ts | 39 ++++++++++++++ .../__tests__/errorSuppression.pure.test.ts | 35 ++++++++++++ src/dom/index.ts | 2 + src/helpers/createTestHarness.tsx | 25 --------- src/helpers/promises.ts | 6 ++- src/index.ts | 2 + src/native/__tests__/errorHook.test.ts | 49 ++++++++++++++++- src/native/index.ts | 2 + src/server/__tests__/errorHook.test.ts | 51 +++++++++++++++++- src/server/index.ts | 2 + 15 files changed, 310 insertions(+), 32 deletions(-) create mode 100644 disable-error-filtering.js create mode 100644 src/core/console.ts create mode 100644 src/dom/__tests__/errorSuppression.disabled.test.ts create mode 100644 src/dom/__tests__/errorSuppression.pure.test.ts diff --git a/disable-error-filtering.js b/disable-error-filtering.js new file mode 100644 index 00000000..25e71c79 --- /dev/null +++ b/disable-error-filtering.js @@ -0,0 +1 @@ +process.env.RHTL_DISABLE_ERROR_FILTERING = true diff --git a/docs/api-reference.md b/docs/api-reference.md index 22ab6df4..c5bbee07 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -12,6 +12,7 @@ route: '/reference/api' - [`cleanup`](/reference/api#cleanup) - [`addCleanup`](/reference/api#addcleanup) - [`removeCleanup`](/reference/api#removecleanup) +- [`console.error`](/reference/api#consoleerror) --- @@ -154,7 +155,7 @@ module.exports = { ``` Alternatively, you can change your test to import from `@testing-library/react-hooks/pure` instead -of the regular imports. This applys to any of our export methods documented in +of the regular imports. This applies to any of our export methods documented in [Rendering](/installation#being-specific). ```diff @@ -270,3 +271,49 @@ Interval checking is disabled if `interval` is not provided as a `falsy`. _Default: 1000_ The maximum amount of time in milliseconds (ms) to wait. + +--- + +## `console.error` + +In order to catch errors that are produced in all parts of the hook's lifecycle, the test harness +used to wrap the hook call includes an +[Error Boundary](https://reactjs.org/docs/error-boundaries.html) which causes a +[significant amount of output noise](https://reactjs.org/docs/error-boundaries.html#component-stack-traces) +in tests. + +To keep test output clean, we patch `console.error` when `renderHook` is called to filter out the +unnecessary logging and restore the original version during cleanup. This side-effect can affect +tests that also patch `console.error` (e.g. to assert a specific error message get logged) by +replacing their custom implementation as well. + +### Disabling `console.error` filtering + +Importing `@testing-library/react-hooks/disable-error-filtering.js` in test setup files disable the +error filtering feature and not patch `console.error` in any way. + +For example, in [Jest](https://jestjs.io/) this can be added to your +[Jest config](https://jestjs.io/docs/configuration): + +```js +module.exports = { + setupFilesAfterEnv: [ + '@testing-library/react-hooks/disable-error-filtering.js' + // other setup files + ] +} +``` + +Alternatively, you can change your test to import from `@testing-library/react-hooks/pure` instead +of the regular imports. This applies to any of our export methods documented in +[Rendering](/installation#being-specific). + +```diff +- import { renderHook, cleanup, act } from '@testing-library/react-hooks' ++ import { renderHook, cleanup, act } from '@testing-library/react-hooks/pure' +``` + +If neither of these approaches are suitable, setting the `RHTL_DISABLE_ERROR_FILTERING` environment +variable to `true` before importing `@testing-library/react-hooks` will also disable this feature. + +> Please note that this may result is a significant amount of additional logging in you test output. diff --git a/package.json b/package.json index fa49afbf..053d6602 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "native", "server", "pure", + "disable-error-filtering.js", "dont-cleanup-after-each.js" ], "author": "Michael Peyper ", diff --git a/src/core/console.ts b/src/core/console.ts new file mode 100644 index 00000000..a4bd0914 --- /dev/null +++ b/src/core/console.ts @@ -0,0 +1,25 @@ +import filterConsole from 'filter-console' + +function enableErrorOutputSuppression() { + if (!process.env.RHTL_DISABLE_ERROR_FILTERING) { + const restoreConsole = filterConsole( + [ + /^The above error occurred in the component:/, // error boundary output + /^Error: Uncaught .+/ // jsdom output + ], + { + methods: ['error'] + } + ) + + // Automatically registers restoration in supported testing frameworks + if (typeof afterAll === 'function') { + afterAll(async () => { + await new Promise((resolve) => setTimeout(resolve, 100)) + restoreConsole() + }) + } + } +} + +export { enableErrorOutputSuppression } diff --git a/src/dom/__tests__/errorHook.test.ts b/src/dom/__tests__/errorHook.test.ts index c7f21847..8b3760b9 100644 --- a/src/dom/__tests__/errorHook.test.ts +++ b/src/dom/__tests__/errorHook.test.ts @@ -1,5 +1,5 @@ import { useState, useEffect } from 'react' -import { renderHook } from '..' +import { renderHook, act } from '..' describe('error hook tests', () => { function useError(throwError?: boolean) { @@ -109,7 +109,7 @@ describe('error hook tests', () => { }) describe('effect', () => { - test('should raise effect error', () => { + test('this one - should raise effect error', () => { const { result } = renderHook(() => useEffectError(true)) expect(() => { @@ -117,7 +117,7 @@ describe('error hook tests', () => { }).toThrow(Error('expected')) }) - test('should capture effect error', () => { + test('this one - should capture effect error', () => { const { result } = renderHook(() => useEffectError(true)) expect(result.error).toEqual(Error('expected')) }) @@ -142,4 +142,51 @@ describe('error hook tests', () => { expect(result.error).toBe(undefined) }) }) + + describe('error output suppression', () => { + test('should allow console.error to be mocked', async () => { + const consoleError = console.error + console.error = jest.fn() + + try { + const { rerender, unmount } = renderHook( + (stage) => { + useEffect(() => { + console.error(`expected in effect`) + return () => { + console.error(`expected in unmount`) + } + }, []) + console.error(`expected in ${stage}`) + }, + { + initialProps: 'render' + } + ) + + act(() => { + console.error('expected in act') + }) + + await act(async () => { + await new Promise((resolve) => setTimeout(resolve, 100)) + console.error('expected in async act') + }) + + rerender('rerender') + + unmount() + + expect(console.error).toBeCalledWith('expected in render') + expect(console.error).toBeCalledWith('expected in effect') + expect(console.error).toBeCalledWith('expected in act') + expect(console.error).toBeCalledWith('expected in async act') + expect(console.error).toBeCalledWith('expected in rerender') + expect(console.error).toBeCalledWith('expected in unmount') + expect(console.error).toBeCalledTimes(6) + } finally { + console.error = consoleError + } + }) + }) }) diff --git a/src/dom/__tests__/errorSuppression.disabled.test.ts b/src/dom/__tests__/errorSuppression.disabled.test.ts new file mode 100644 index 00000000..472204c9 --- /dev/null +++ b/src/dom/__tests__/errorSuppression.disabled.test.ts @@ -0,0 +1,39 @@ +import { renderHook } from '..' + +describe('error output suppression (disabled) tests', () => { + function useError(throwError?: boolean) { + if (throwError) { + throw new Error('expected') + } + return true + } + + const originalConsoleError = console.error + const mockConsoleError = jest.fn() + + beforeAll(() => { + process.env.RHTL_DISABLE_ERROR_FILTERING = 'true' + }) + + beforeEach(() => { + console.error = mockConsoleError + }) + + afterEach(() => { + console.error = originalConsoleError + }) + + test('should not suppress error output', () => { + const { result } = renderHook(() => useError(true)) + + expect(result.error).toEqual(Error('expected')) + expect(mockConsoleError).toBeCalledWith( + expect.stringMatching(/^Error: Uncaught \[Error: expected\]/), + expect.any(Error) + ) + expect(mockConsoleError).toBeCalledWith( + expect.stringMatching(/^The above error occurred in the component:/) + ) + expect(mockConsoleError).toBeCalledTimes(2) + }) +}) diff --git a/src/dom/__tests__/errorSuppression.pure.test.ts b/src/dom/__tests__/errorSuppression.pure.test.ts new file mode 100644 index 00000000..56ac58f2 --- /dev/null +++ b/src/dom/__tests__/errorSuppression.pure.test.ts @@ -0,0 +1,35 @@ +import { renderHook } from '../pure' + +describe('error output suppression (pure) tests', () => { + function useError(throwError?: boolean) { + if (throwError) { + throw new Error('expected') + } + return true + } + + const originalConsoleError = console.error + const mockConsoleError = jest.fn() + + beforeEach(() => { + console.error = mockConsoleError + }) + + afterEach(() => { + console.error = originalConsoleError + }) + + test('should not suppress error output', () => { + const { result } = renderHook(() => useError(true)) + + expect(result.error).toEqual(Error('expected')) + expect(mockConsoleError).toBeCalledWith( + expect.stringMatching(/^Error: Uncaught \[Error: expected\]/), + expect.any(Error) + ) + expect(mockConsoleError).toBeCalledWith( + expect.stringMatching(/^The above error occurred in the component:/) + ) + expect(mockConsoleError).toBeCalledTimes(2) + }) +}) diff --git a/src/dom/index.ts b/src/dom/index.ts index 7d558c25..3e32d48f 100644 --- a/src/dom/index.ts +++ b/src/dom/index.ts @@ -1,5 +1,7 @@ import { autoRegisterCleanup } from '../core/cleanup' +import { enableErrorOutputSuppression } from '../core/console' autoRegisterCleanup() +enableErrorOutputSuppression() export * from './pure' diff --git a/src/helpers/createTestHarness.tsx b/src/helpers/createTestHarness.tsx index 0d1d4838..5ee7fb6b 100644 --- a/src/helpers/createTestHarness.tsx +++ b/src/helpers/createTestHarness.tsx @@ -1,31 +1,8 @@ import React, { Suspense } from 'react' import { ErrorBoundary, FallbackProps } from 'react-error-boundary' -import filterConsole from 'filter-console' - -import { addCleanup } from '../core' import { RendererProps, WrapperComponent } from '../types/react' -function suppressErrorOutput() { - // The error output from error boundaries is notoriously difficult to suppress. To save - // out users from having to work it out, we crudely suppress the output matching the patterns - // below. For more information, see these issues: - // - https://github.com/testing-library/react-hooks-testing-library/issues/50 - // - https://github.com/facebook/react/issues/11098#issuecomment-412682721 - // - https://github.com/facebook/react/issues/15520 - // - https://github.com/facebook/react/issues/18841 - const removeConsoleFilter = filterConsole( - [ - /^The above error occurred in the component:/, // error boundary output - /^Error: Uncaught .+/ // jsdom output - ], - { - methods: ['error'] - } - ) - addCleanup(removeConsoleFilter) -} - function createTestHarness( { callback, setValue, setError }: RendererProps, Wrapper?: WrapperComponent, @@ -47,8 +24,6 @@ function createTestHarness( return null } - suppressErrorOutput() - const testHarness = (props?: TProps) => { resetErrorBoundary() diff --git a/src/helpers/promises.ts b/src/helpers/promises.ts index 2fa89e5f..83e9d3a6 100644 --- a/src/helpers/promises.ts +++ b/src/helpers/promises.ts @@ -7,4 +7,8 @@ async function callAfter(callback: () => void, ms: number) { callback() } -export { resolveAfter, callAfter } +function isPromise(value: unknown): boolean { + return value !== undefined && typeof (value as PromiseLike).then === 'function' +} + +export { resolveAfter, callAfter, isPromise } diff --git a/src/index.ts b/src/index.ts index 10b0b905..5af3c9b5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,7 @@ import { autoRegisterCleanup } from './core/cleanup' +import { enableErrorOutputSuppression } from './core/console' autoRegisterCleanup() +enableErrorOutputSuppression() export * from './pure' diff --git a/src/native/__tests__/errorHook.test.ts b/src/native/__tests__/errorHook.test.ts index c7f21847..69e54270 100644 --- a/src/native/__tests__/errorHook.test.ts +++ b/src/native/__tests__/errorHook.test.ts @@ -1,5 +1,5 @@ import { useState, useEffect } from 'react' -import { renderHook } from '..' +import { renderHook, act } from '..' describe('error hook tests', () => { function useError(throwError?: boolean) { @@ -142,4 +142,51 @@ describe('error hook tests', () => { expect(result.error).toBe(undefined) }) }) + + describe('error output suppression', () => { + test('should allow console.error to be mocked', async () => { + const consoleError = console.error + console.error = jest.fn() + + try { + const { rerender, unmount } = renderHook( + (stage) => { + useEffect(() => { + console.error(`expected in effect`) + return () => { + console.error(`expected in unmount`) + } + }, []) + console.error(`expected in ${stage}`) + }, + { + initialProps: 'render' + } + ) + + act(() => { + console.error('expected in act') + }) + + await act(async () => { + await new Promise((resolve) => setTimeout(resolve, 100)) + console.error('expected in async act') + }) + + rerender('rerender') + + unmount() + + expect(console.error).toBeCalledWith('expected in render') + expect(console.error).toBeCalledWith('expected in effect') + expect(console.error).toBeCalledWith('expected in act') + expect(console.error).toBeCalledWith('expected in async act') + expect(console.error).toBeCalledWith('expected in rerender') + expect(console.error).toBeCalledWith('expected in unmount') + expect(console.error).toBeCalledTimes(6) + } finally { + console.error = consoleError + } + }) + }) }) diff --git a/src/native/index.ts b/src/native/index.ts index 7d558c25..3e32d48f 100644 --- a/src/native/index.ts +++ b/src/native/index.ts @@ -1,5 +1,7 @@ import { autoRegisterCleanup } from '../core/cleanup' +import { enableErrorOutputSuppression } from '../core/console' autoRegisterCleanup() +enableErrorOutputSuppression() export * from './pure' diff --git a/src/server/__tests__/errorHook.test.ts b/src/server/__tests__/errorHook.test.ts index f7977465..f3ce0442 100644 --- a/src/server/__tests__/errorHook.test.ts +++ b/src/server/__tests__/errorHook.test.ts @@ -1,6 +1,6 @@ import { useState, useEffect } from 'react' -import { renderHook } from '..' +import { renderHook, act } from '..' describe('error hook tests', () => { function useError(throwError?: boolean) { @@ -163,4 +163,53 @@ describe('error hook tests', () => { expect(result.error).toBe(undefined) }) }) + + describe('error output suppression', () => { + test('should allow console.error to be mocked', async () => { + const consoleError = console.error + console.error = jest.fn() + + try { + const { hydrate, rerender, unmount } = renderHook( + (stage) => { + useEffect(() => { + console.error(`expected in effect`) + return () => { + console.error(`expected in unmount`) + } + }, []) + console.error(`expected in ${stage}`) + }, + { + initialProps: 'render' + } + ) + + hydrate() + + act(() => { + console.error('expected in act') + }) + + await act(async () => { + await new Promise((resolve) => setTimeout(resolve, 100)) + console.error('expected in async act') + }) + + rerender('rerender') + + unmount() + + expect(console.error).toBeCalledWith('expected in render') // twice render/hydrate + expect(console.error).toBeCalledWith('expected in effect') + expect(console.error).toBeCalledWith('expected in act') + expect(console.error).toBeCalledWith('expected in async act') + expect(console.error).toBeCalledWith('expected in rerender') + expect(console.error).toBeCalledWith('expected in unmount') + expect(console.error).toBeCalledTimes(7) + } finally { + console.error = consoleError + } + }) + }) }) diff --git a/src/server/index.ts b/src/server/index.ts index 7d558c25..3e32d48f 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -1,5 +1,7 @@ import { autoRegisterCleanup } from '../core/cleanup' +import { enableErrorOutputSuppression } from '../core/console' autoRegisterCleanup() +enableErrorOutputSuppression() export * from './pure' From a1d44b2a36bc5e9a963fe69c693b3629dd08bc6f Mon Sep 17 00:00:00 2001 From: Michael Peyper Date: Tue, 19 Jan 2021 09:24:13 +1100 Subject: [PATCH 2/9] refactor: remove unused promise util --- src/helpers/promises.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/helpers/promises.ts b/src/helpers/promises.ts index 83e9d3a6..2fa89e5f 100644 --- a/src/helpers/promises.ts +++ b/src/helpers/promises.ts @@ -7,8 +7,4 @@ async function callAfter(callback: () => void, ms: number) { callback() } -function isPromise(value: unknown): boolean { - return value !== undefined && typeof (value as PromiseLike).then === 'function' -} - -export { resolveAfter, callAfter, isPromise } +export { resolveAfter, callAfter } From e7cce51af9ef175c0cf214130a24d0c0a83b0f6c Mon Sep 17 00:00:00 2001 From: Michael Peyper Date: Tue, 19 Jan 2021 09:25:19 +1100 Subject: [PATCH 3/9] chore: fix tests for error suppression to --- .../errorSuppression.disabled.test.ts | 35 +++---------------- .../errorSuppression.noAfterAll.test.ts | 24 +++++++++++++ .../__tests__/errorSuppression.pure.test.ts | 35 +++---------------- .../errorSuppression.disabled.test.ts | 14 ++++++++ .../errorSuppression.noAfterAll.test.ts | 24 +++++++++++++ .../__tests__/errorSuppression.pure.test.ts | 10 ++++++ .../errorSuppression.disabled.test.ts | 14 ++++++++ .../errorSuppression.noAfterAll.test.ts | 24 +++++++++++++ .../__tests__/errorSuppression.pure.test.ts | 10 ++++++ 9 files changed, 130 insertions(+), 60 deletions(-) create mode 100644 src/dom/__tests__/errorSuppression.noAfterAll.test.ts create mode 100644 src/native/__tests__/errorSuppression.disabled.test.ts create mode 100644 src/native/__tests__/errorSuppression.noAfterAll.test.ts create mode 100644 src/native/__tests__/errorSuppression.pure.test.ts create mode 100644 src/server/__tests__/errorSuppression.disabled.test.ts create mode 100644 src/server/__tests__/errorSuppression.noAfterAll.test.ts create mode 100644 src/server/__tests__/errorSuppression.pure.test.ts diff --git a/src/dom/__tests__/errorSuppression.disabled.test.ts b/src/dom/__tests__/errorSuppression.disabled.test.ts index 472204c9..29af7ba9 100644 --- a/src/dom/__tests__/errorSuppression.disabled.test.ts +++ b/src/dom/__tests__/errorSuppression.disabled.test.ts @@ -1,39 +1,14 @@ -import { renderHook } from '..' - describe('error output suppression (disabled) tests', () => { - function useError(throwError?: boolean) { - if (throwError) { - throw new Error('expected') - } - return true - } - const originalConsoleError = console.error - const mockConsoleError = jest.fn() beforeAll(() => { process.env.RHTL_DISABLE_ERROR_FILTERING = 'true' }) - beforeEach(() => { - console.error = mockConsoleError - }) - - afterEach(() => { - console.error = originalConsoleError - }) - - test('should not suppress error output', () => { - const { result } = renderHook(() => useError(true)) - - expect(result.error).toEqual(Error('expected')) - expect(mockConsoleError).toBeCalledWith( - expect.stringMatching(/^Error: Uncaught \[Error: expected\]/), - expect.any(Error) - ) - expect(mockConsoleError).toBeCalledWith( - expect.stringMatching(/^The above error occurred in the component:/) - ) - expect(mockConsoleError).toBeCalledTimes(2) + test('should not patch console.error', () => { + require('..') + expect(console.error).toBe(originalConsoleError) }) }) + +export {} diff --git a/src/dom/__tests__/errorSuppression.noAfterAll.test.ts b/src/dom/__tests__/errorSuppression.noAfterAll.test.ts new file mode 100644 index 00000000..dfe343d3 --- /dev/null +++ b/src/dom/__tests__/errorSuppression.noAfterAll.test.ts @@ -0,0 +1,24 @@ +describe('error output suppression (noAfterAll) tests', () => { + const originalConsoleError = console.error + + beforeAll(() => { + // @ts-expect-error Turning off AfterAll -- ignore Jest LifeCycle Type + // eslint-disable-next-line no-global-assign + afterAll = false + }) + + describe('first', () => { + test('should patch console.error', () => { + require('..') + expect(console.error).not.toBe(originalConsoleError) + }) + }) + + describe('second', () => { + test('should still used patched console.error', () => { + expect(console.error).not.toBe(originalConsoleError) + }) + }) +}) + +export {} diff --git a/src/dom/__tests__/errorSuppression.pure.test.ts b/src/dom/__tests__/errorSuppression.pure.test.ts index 56ac58f2..4a494d81 100644 --- a/src/dom/__tests__/errorSuppression.pure.test.ts +++ b/src/dom/__tests__/errorSuppression.pure.test.ts @@ -1,35 +1,10 @@ -import { renderHook } from '../pure' - describe('error output suppression (pure) tests', () => { - function useError(throwError?: boolean) { - if (throwError) { - throw new Error('expected') - } - return true - } - const originalConsoleError = console.error - const mockConsoleError = jest.fn() - - beforeEach(() => { - console.error = mockConsoleError - }) - afterEach(() => { - console.error = originalConsoleError - }) - - test('should not suppress error output', () => { - const { result } = renderHook(() => useError(true)) - - expect(result.error).toEqual(Error('expected')) - expect(mockConsoleError).toBeCalledWith( - expect.stringMatching(/^Error: Uncaught \[Error: expected\]/), - expect.any(Error) - ) - expect(mockConsoleError).toBeCalledWith( - expect.stringMatching(/^The above error occurred in the component:/) - ) - expect(mockConsoleError).toBeCalledTimes(2) + test('should not patch console.error', () => { + require('../pure') + expect(console.error).toBe(originalConsoleError) }) }) + +export {} diff --git a/src/native/__tests__/errorSuppression.disabled.test.ts b/src/native/__tests__/errorSuppression.disabled.test.ts new file mode 100644 index 00000000..29af7ba9 --- /dev/null +++ b/src/native/__tests__/errorSuppression.disabled.test.ts @@ -0,0 +1,14 @@ +describe('error output suppression (disabled) tests', () => { + const originalConsoleError = console.error + + beforeAll(() => { + process.env.RHTL_DISABLE_ERROR_FILTERING = 'true' + }) + + test('should not patch console.error', () => { + require('..') + expect(console.error).toBe(originalConsoleError) + }) +}) + +export {} diff --git a/src/native/__tests__/errorSuppression.noAfterAll.test.ts b/src/native/__tests__/errorSuppression.noAfterAll.test.ts new file mode 100644 index 00000000..dfe343d3 --- /dev/null +++ b/src/native/__tests__/errorSuppression.noAfterAll.test.ts @@ -0,0 +1,24 @@ +describe('error output suppression (noAfterAll) tests', () => { + const originalConsoleError = console.error + + beforeAll(() => { + // @ts-expect-error Turning off AfterAll -- ignore Jest LifeCycle Type + // eslint-disable-next-line no-global-assign + afterAll = false + }) + + describe('first', () => { + test('should patch console.error', () => { + require('..') + expect(console.error).not.toBe(originalConsoleError) + }) + }) + + describe('second', () => { + test('should still used patched console.error', () => { + expect(console.error).not.toBe(originalConsoleError) + }) + }) +}) + +export {} diff --git a/src/native/__tests__/errorSuppression.pure.test.ts b/src/native/__tests__/errorSuppression.pure.test.ts new file mode 100644 index 00000000..4a494d81 --- /dev/null +++ b/src/native/__tests__/errorSuppression.pure.test.ts @@ -0,0 +1,10 @@ +describe('error output suppression (pure) tests', () => { + const originalConsoleError = console.error + + test('should not patch console.error', () => { + require('../pure') + expect(console.error).toBe(originalConsoleError) + }) +}) + +export {} diff --git a/src/server/__tests__/errorSuppression.disabled.test.ts b/src/server/__tests__/errorSuppression.disabled.test.ts new file mode 100644 index 00000000..29af7ba9 --- /dev/null +++ b/src/server/__tests__/errorSuppression.disabled.test.ts @@ -0,0 +1,14 @@ +describe('error output suppression (disabled) tests', () => { + const originalConsoleError = console.error + + beforeAll(() => { + process.env.RHTL_DISABLE_ERROR_FILTERING = 'true' + }) + + test('should not patch console.error', () => { + require('..') + expect(console.error).toBe(originalConsoleError) + }) +}) + +export {} diff --git a/src/server/__tests__/errorSuppression.noAfterAll.test.ts b/src/server/__tests__/errorSuppression.noAfterAll.test.ts new file mode 100644 index 00000000..dfe343d3 --- /dev/null +++ b/src/server/__tests__/errorSuppression.noAfterAll.test.ts @@ -0,0 +1,24 @@ +describe('error output suppression (noAfterAll) tests', () => { + const originalConsoleError = console.error + + beforeAll(() => { + // @ts-expect-error Turning off AfterAll -- ignore Jest LifeCycle Type + // eslint-disable-next-line no-global-assign + afterAll = false + }) + + describe('first', () => { + test('should patch console.error', () => { + require('..') + expect(console.error).not.toBe(originalConsoleError) + }) + }) + + describe('second', () => { + test('should still used patched console.error', () => { + expect(console.error).not.toBe(originalConsoleError) + }) + }) +}) + +export {} diff --git a/src/server/__tests__/errorSuppression.pure.test.ts b/src/server/__tests__/errorSuppression.pure.test.ts new file mode 100644 index 00000000..4a494d81 --- /dev/null +++ b/src/server/__tests__/errorSuppression.pure.test.ts @@ -0,0 +1,10 @@ +describe('error output suppression (pure) tests', () => { + const originalConsoleError = console.error + + test('should not patch console.error', () => { + require('../pure') + expect(console.error).toBe(originalConsoleError) + }) +}) + +export {} From 698579c1c23f05557979f873293624aa6f5b3e42 Mon Sep 17 00:00:00 2001 From: Michael Peyper Date: Tue, 19 Jan 2021 09:39:13 +1100 Subject: [PATCH 4/9] docs: update docs to with more detail on side effects --- docs/api-reference.md | 19 +++++++++---------- docs/installation.md | 28 ++++++++++++++++++++++++++-- 2 files changed, 35 insertions(+), 12 deletions(-) diff --git a/docs/api-reference.md b/docs/api-reference.md index c5bbee07..3fc61a60 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -154,9 +154,8 @@ module.exports = { } ``` -Alternatively, you can change your test to import from `@testing-library/react-hooks/pure` instead -of the regular imports. This applies to any of our export methods documented in -[Rendering](/installation#being-specific). +Alternatively, you can change your test to import from `@testing-library/react-hooks/pure` (or any +of the [other non-pure imports](/installation#pure-imports)) instead of the regular imports. ```diff - import { renderHook, cleanup, act } from '@testing-library/react-hooks' @@ -282,10 +281,11 @@ used to wrap the hook call includes an [significant amount of output noise](https://reactjs.org/docs/error-boundaries.html#component-stack-traces) in tests. -To keep test output clean, we patch `console.error` when `renderHook` is called to filter out the -unnecessary logging and restore the original version during cleanup. This side-effect can affect -tests that also patch `console.error` (e.g. to assert a specific error message get logged) by -replacing their custom implementation as well. +To keep test output clean, we patch `console.error` when importing from +`@testing-library/react-hooks` (or any of the [other non-pure imports](/installation#pure-imports)) +to filter out the unnecessary logging and restore the original version during cleanup. This +side-effect can affect tests that also patch `console.error` (e.g. to assert a specific error +message get logged) by replacing their custom implementation as well. ### Disabling `console.error` filtering @@ -304,9 +304,8 @@ module.exports = { } ``` -Alternatively, you can change your test to import from `@testing-library/react-hooks/pure` instead -of the regular imports. This applies to any of our export methods documented in -[Rendering](/installation#being-specific). +Alternatively, you can change your test to import from `@testing-library/react-hooks/pure` (or any +of the [other non-pure imports](/installation#pure-imports)) instead of the regular imports. ```diff - import { renderHook, cleanup, act } from '@testing-library/react-hooks' diff --git a/docs/installation.md b/docs/installation.md index 942178cd..d83eed05 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -17,7 +17,7 @@ npm install --save-dev @testing-library/react-hooks yarn add --dev @testing-library/react-hooks ``` -### Peer Dependencies +### Peer dependencies `react-hooks-testing-library` does not come bundled with a version of [`react`](https://www.npmjs.com/package/react) to allow you to install the specific version you want @@ -92,7 +92,31 @@ import { renderHook, act } from '@testing-library/react-hooks/native' // will us import { renderHook, act } from '@testing-library/react-hooks/server' // will use react-dom/server ``` -## Testing Framework +## Pure imports + +Importing from any of the previously mentioned imports will cause some side effects in the test +environment: + +1. `cleanup` is automatically called in an `afterEach` block +2. `console.error` is patched to hide some React errors + +The specifics of these side effects are discussed in more detail in the +[API reference](/reference/api). + +If you want to ensure the imports are free of side-effects, you can use the `pure` imports instead, +which can be accessed by appending `/pure` to the end of any of the other imports: + +```ts +import { renderHook, act } from '@testing-library/react-hooks/pure' + +import { renderHook, act } from '@testing-library/react-hooks/dom/pure' + +import { renderHook, act } from '@testing-library/react-hooks/native/pure' + +import { renderHook, act } from '@testing-library/react-hooks/server/pure' +``` + +## Testing framework In order to run tests, you will probably want to be using a test framework. If you have not already got one, we recommend using [Jest](https://jestjs.io/), but this library should work without issues From 033734589ebee025cf40dbeb1aaca0f5c119aa92 Mon Sep 17 00:00:00 2001 From: Michael Peyper Date: Tue, 19 Jan 2021 14:29:45 +1100 Subject: [PATCH 5/9] refactor: only add suppression in beforeEach block and move restoration to afterEach --- src/core/console.ts | 37 ++++++++++--------- .../__tests__/autoCleanup.disabled.test.ts | 1 - .../__tests__/autoCleanup.noAfterEach.test.ts | 4 +- .../errorSuppression.disabled.test.ts | 2 + .../errorSuppression.noAfterAll.test.ts | 24 ------------ .../errorSuppression.noAfterEach.test.ts | 19 ++++++++++ .../errorSuppression.noBeforeEach.test.ts | 19 ++++++++++ .../__tests__/autoCleanup.disabled.test.ts | 1 - .../__tests__/autoCleanup.noAfterEach.test.ts | 4 +- .../errorSuppression.disabled.test.ts | 2 + .../errorSuppression.noAfterAll.test.ts | 24 ------------ .../errorSuppression.noAfterEach.test.ts | 19 ++++++++++ .../errorSuppression.noBeforeEach.test.ts | 19 ++++++++++ .../__tests__/autoCleanup.disabled.test.ts | 1 - .../__tests__/autoCleanup.noAfterEach.test.ts | 4 +- .../errorSuppression.disabled.test.ts | 2 + .../errorSuppression.noAfterAll.test.ts | 24 ------------ .../errorSuppression.noAfterEach.test.ts | 19 ++++++++++ .../errorSuppression.noBeforeEach.test.ts | 19 ++++++++++ 19 files changed, 143 insertions(+), 101 deletions(-) delete mode 100644 src/dom/__tests__/errorSuppression.noAfterAll.test.ts create mode 100644 src/dom/__tests__/errorSuppression.noAfterEach.test.ts create mode 100644 src/dom/__tests__/errorSuppression.noBeforeEach.test.ts delete mode 100644 src/native/__tests__/errorSuppression.noAfterAll.test.ts create mode 100644 src/native/__tests__/errorSuppression.noAfterEach.test.ts create mode 100644 src/native/__tests__/errorSuppression.noBeforeEach.test.ts delete mode 100644 src/server/__tests__/errorSuppression.noAfterAll.test.ts create mode 100644 src/server/__tests__/errorSuppression.noAfterEach.test.ts create mode 100644 src/server/__tests__/errorSuppression.noBeforeEach.test.ts diff --git a/src/core/console.ts b/src/core/console.ts index a4bd0914..e1a326d0 100644 --- a/src/core/console.ts +++ b/src/core/console.ts @@ -1,24 +1,27 @@ import filterConsole from 'filter-console' function enableErrorOutputSuppression() { - if (!process.env.RHTL_DISABLE_ERROR_FILTERING) { - const restoreConsole = filterConsole( - [ - /^The above error occurred in the component:/, // error boundary output - /^Error: Uncaught .+/ // jsdom output - ], - { - methods: ['error'] - } - ) + // Automatically registers console error suppression and restoration in supported testing frameworks + if ( + typeof beforeEach === 'function' && + typeof afterEach === 'function' && + !process.env.RHTL_DISABLE_ERROR_FILTERING + ) { + let restoreConsole: (() => void) | null = null - // Automatically registers restoration in supported testing frameworks - if (typeof afterAll === 'function') { - afterAll(async () => { - await new Promise((resolve) => setTimeout(resolve, 100)) - restoreConsole() - }) - } + beforeEach(() => { + restoreConsole = filterConsole( + [ + /^The above error occurred in the component:/, // error boundary output + /^Error: Uncaught .+/ // jsdom output + ], + { + methods: ['error'] + } + ) + }) + + afterEach(() => restoreConsole?.()) } } diff --git a/src/dom/__tests__/autoCleanup.disabled.test.ts b/src/dom/__tests__/autoCleanup.disabled.test.ts index 1485ff47..c5d4eaf5 100644 --- a/src/dom/__tests__/autoCleanup.disabled.test.ts +++ b/src/dom/__tests__/autoCleanup.disabled.test.ts @@ -10,7 +10,6 @@ describe('skip auto cleanup (disabled) tests', () => { beforeAll(() => { process.env.RHTL_SKIP_AUTO_CLEANUP = 'true' - // eslint-disable-next-line @typescript-eslint/no-var-requires renderHook = (require('..') as ReactHooksRenderer).renderHook }) diff --git a/src/dom/__tests__/autoCleanup.noAfterEach.test.ts b/src/dom/__tests__/autoCleanup.noAfterEach.test.ts index afe4514d..09f6bba6 100644 --- a/src/dom/__tests__/autoCleanup.noAfterEach.test.ts +++ b/src/dom/__tests__/autoCleanup.noAfterEach.test.ts @@ -2,7 +2,7 @@ import { useEffect } from 'react' import { ReactHooksRenderer } from '../../types/react' -// This verifies that if RHTL_SKIP_AUTO_CLEANUP is set +// This verifies that if afterEach is unavailable // then we DON'T auto-wire up the afterEach for folks describe('skip auto cleanup (no afterEach) tests', () => { let cleanupCalled = false @@ -10,9 +10,7 @@ describe('skip auto cleanup (no afterEach) tests', () => { beforeAll(() => { // @ts-expect-error Turning off AfterEach -- ignore Jest LifeCycle Type - // eslint-disable-next-line no-global-assign afterEach = false - // eslint-disable-next-line @typescript-eslint/no-var-requires renderHook = (require('..') as ReactHooksRenderer).renderHook }) diff --git a/src/dom/__tests__/errorSuppression.disabled.test.ts b/src/dom/__tests__/errorSuppression.disabled.test.ts index 29af7ba9..843a405e 100644 --- a/src/dom/__tests__/errorSuppression.disabled.test.ts +++ b/src/dom/__tests__/errorSuppression.disabled.test.ts @@ -1,3 +1,5 @@ +// This verifies that if RHTL_DISABLE_ERROR_FILTERING is set +// then we DON'T auto-wire up the afterEach for folks describe('error output suppression (disabled) tests', () => { const originalConsoleError = console.error diff --git a/src/dom/__tests__/errorSuppression.noAfterAll.test.ts b/src/dom/__tests__/errorSuppression.noAfterAll.test.ts deleted file mode 100644 index dfe343d3..00000000 --- a/src/dom/__tests__/errorSuppression.noAfterAll.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -describe('error output suppression (noAfterAll) tests', () => { - const originalConsoleError = console.error - - beforeAll(() => { - // @ts-expect-error Turning off AfterAll -- ignore Jest LifeCycle Type - // eslint-disable-next-line no-global-assign - afterAll = false - }) - - describe('first', () => { - test('should patch console.error', () => { - require('..') - expect(console.error).not.toBe(originalConsoleError) - }) - }) - - describe('second', () => { - test('should still used patched console.error', () => { - expect(console.error).not.toBe(originalConsoleError) - }) - }) -}) - -export {} diff --git a/src/dom/__tests__/errorSuppression.noAfterEach.test.ts b/src/dom/__tests__/errorSuppression.noAfterEach.test.ts new file mode 100644 index 00000000..9acc48f5 --- /dev/null +++ b/src/dom/__tests__/errorSuppression.noAfterEach.test.ts @@ -0,0 +1,19 @@ +// This verifies that if afterEach is unavailable +// then we DON'T auto-wire up the afterEach for folks +describe('error output suppression (noAfterEach) tests', () => { + const originalConsoleError = console.error + + beforeAll(() => { + // @ts-expect-error Turning off AfterEach -- ignore Jest LifeCycle Type + afterEach = false + }) + + describe('first', () => { + test('should not patch console.error', () => { + require('..') + expect(console.error).toBe(originalConsoleError) + }) + }) +}) + +export {} diff --git a/src/dom/__tests__/errorSuppression.noBeforeEach.test.ts b/src/dom/__tests__/errorSuppression.noBeforeEach.test.ts new file mode 100644 index 00000000..2e28b4f8 --- /dev/null +++ b/src/dom/__tests__/errorSuppression.noBeforeEach.test.ts @@ -0,0 +1,19 @@ +// This verifies that if afterEach is unavailable +// then we DON'T auto-wire up the afterEach for folks +describe('error output suppression (noBeforeEach) tests', () => { + const originalConsoleError = console.error + + beforeAll(() => { + // @ts-expect-error Turning off BeforeEach -- ignore Jest LifeCycle Type + beforeEach = false + }) + + describe('first', () => { + test('should not patch console.error', () => { + require('..') + expect(console.error).toBe(originalConsoleError) + }) + }) +}) + +export {} diff --git a/src/native/__tests__/autoCleanup.disabled.test.ts b/src/native/__tests__/autoCleanup.disabled.test.ts index 1485ff47..c5d4eaf5 100644 --- a/src/native/__tests__/autoCleanup.disabled.test.ts +++ b/src/native/__tests__/autoCleanup.disabled.test.ts @@ -10,7 +10,6 @@ describe('skip auto cleanup (disabled) tests', () => { beforeAll(() => { process.env.RHTL_SKIP_AUTO_CLEANUP = 'true' - // eslint-disable-next-line @typescript-eslint/no-var-requires renderHook = (require('..') as ReactHooksRenderer).renderHook }) diff --git a/src/native/__tests__/autoCleanup.noAfterEach.test.ts b/src/native/__tests__/autoCleanup.noAfterEach.test.ts index afe4514d..09f6bba6 100644 --- a/src/native/__tests__/autoCleanup.noAfterEach.test.ts +++ b/src/native/__tests__/autoCleanup.noAfterEach.test.ts @@ -2,7 +2,7 @@ import { useEffect } from 'react' import { ReactHooksRenderer } from '../../types/react' -// This verifies that if RHTL_SKIP_AUTO_CLEANUP is set +// This verifies that if afterEach is unavailable // then we DON'T auto-wire up the afterEach for folks describe('skip auto cleanup (no afterEach) tests', () => { let cleanupCalled = false @@ -10,9 +10,7 @@ describe('skip auto cleanup (no afterEach) tests', () => { beforeAll(() => { // @ts-expect-error Turning off AfterEach -- ignore Jest LifeCycle Type - // eslint-disable-next-line no-global-assign afterEach = false - // eslint-disable-next-line @typescript-eslint/no-var-requires renderHook = (require('..') as ReactHooksRenderer).renderHook }) diff --git a/src/native/__tests__/errorSuppression.disabled.test.ts b/src/native/__tests__/errorSuppression.disabled.test.ts index 29af7ba9..843a405e 100644 --- a/src/native/__tests__/errorSuppression.disabled.test.ts +++ b/src/native/__tests__/errorSuppression.disabled.test.ts @@ -1,3 +1,5 @@ +// This verifies that if RHTL_DISABLE_ERROR_FILTERING is set +// then we DON'T auto-wire up the afterEach for folks describe('error output suppression (disabled) tests', () => { const originalConsoleError = console.error diff --git a/src/native/__tests__/errorSuppression.noAfterAll.test.ts b/src/native/__tests__/errorSuppression.noAfterAll.test.ts deleted file mode 100644 index dfe343d3..00000000 --- a/src/native/__tests__/errorSuppression.noAfterAll.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -describe('error output suppression (noAfterAll) tests', () => { - const originalConsoleError = console.error - - beforeAll(() => { - // @ts-expect-error Turning off AfterAll -- ignore Jest LifeCycle Type - // eslint-disable-next-line no-global-assign - afterAll = false - }) - - describe('first', () => { - test('should patch console.error', () => { - require('..') - expect(console.error).not.toBe(originalConsoleError) - }) - }) - - describe('second', () => { - test('should still used patched console.error', () => { - expect(console.error).not.toBe(originalConsoleError) - }) - }) -}) - -export {} diff --git a/src/native/__tests__/errorSuppression.noAfterEach.test.ts b/src/native/__tests__/errorSuppression.noAfterEach.test.ts new file mode 100644 index 00000000..9acc48f5 --- /dev/null +++ b/src/native/__tests__/errorSuppression.noAfterEach.test.ts @@ -0,0 +1,19 @@ +// This verifies that if afterEach is unavailable +// then we DON'T auto-wire up the afterEach for folks +describe('error output suppression (noAfterEach) tests', () => { + const originalConsoleError = console.error + + beforeAll(() => { + // @ts-expect-error Turning off AfterEach -- ignore Jest LifeCycle Type + afterEach = false + }) + + describe('first', () => { + test('should not patch console.error', () => { + require('..') + expect(console.error).toBe(originalConsoleError) + }) + }) +}) + +export {} diff --git a/src/native/__tests__/errorSuppression.noBeforeEach.test.ts b/src/native/__tests__/errorSuppression.noBeforeEach.test.ts new file mode 100644 index 00000000..2e28b4f8 --- /dev/null +++ b/src/native/__tests__/errorSuppression.noBeforeEach.test.ts @@ -0,0 +1,19 @@ +// This verifies that if afterEach is unavailable +// then we DON'T auto-wire up the afterEach for folks +describe('error output suppression (noBeforeEach) tests', () => { + const originalConsoleError = console.error + + beforeAll(() => { + // @ts-expect-error Turning off BeforeEach -- ignore Jest LifeCycle Type + beforeEach = false + }) + + describe('first', () => { + test('should not patch console.error', () => { + require('..') + expect(console.error).toBe(originalConsoleError) + }) + }) +}) + +export {} diff --git a/src/server/__tests__/autoCleanup.disabled.test.ts b/src/server/__tests__/autoCleanup.disabled.test.ts index 1485ff47..c5d4eaf5 100644 --- a/src/server/__tests__/autoCleanup.disabled.test.ts +++ b/src/server/__tests__/autoCleanup.disabled.test.ts @@ -10,7 +10,6 @@ describe('skip auto cleanup (disabled) tests', () => { beforeAll(() => { process.env.RHTL_SKIP_AUTO_CLEANUP = 'true' - // eslint-disable-next-line @typescript-eslint/no-var-requires renderHook = (require('..') as ReactHooksRenderer).renderHook }) diff --git a/src/server/__tests__/autoCleanup.noAfterEach.test.ts b/src/server/__tests__/autoCleanup.noAfterEach.test.ts index afe4514d..09f6bba6 100644 --- a/src/server/__tests__/autoCleanup.noAfterEach.test.ts +++ b/src/server/__tests__/autoCleanup.noAfterEach.test.ts @@ -2,7 +2,7 @@ import { useEffect } from 'react' import { ReactHooksRenderer } from '../../types/react' -// This verifies that if RHTL_SKIP_AUTO_CLEANUP is set +// This verifies that if afterEach is unavailable // then we DON'T auto-wire up the afterEach for folks describe('skip auto cleanup (no afterEach) tests', () => { let cleanupCalled = false @@ -10,9 +10,7 @@ describe('skip auto cleanup (no afterEach) tests', () => { beforeAll(() => { // @ts-expect-error Turning off AfterEach -- ignore Jest LifeCycle Type - // eslint-disable-next-line no-global-assign afterEach = false - // eslint-disable-next-line @typescript-eslint/no-var-requires renderHook = (require('..') as ReactHooksRenderer).renderHook }) diff --git a/src/server/__tests__/errorSuppression.disabled.test.ts b/src/server/__tests__/errorSuppression.disabled.test.ts index 29af7ba9..843a405e 100644 --- a/src/server/__tests__/errorSuppression.disabled.test.ts +++ b/src/server/__tests__/errorSuppression.disabled.test.ts @@ -1,3 +1,5 @@ +// This verifies that if RHTL_DISABLE_ERROR_FILTERING is set +// then we DON'T auto-wire up the afterEach for folks describe('error output suppression (disabled) tests', () => { const originalConsoleError = console.error diff --git a/src/server/__tests__/errorSuppression.noAfterAll.test.ts b/src/server/__tests__/errorSuppression.noAfterAll.test.ts deleted file mode 100644 index dfe343d3..00000000 --- a/src/server/__tests__/errorSuppression.noAfterAll.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -describe('error output suppression (noAfterAll) tests', () => { - const originalConsoleError = console.error - - beforeAll(() => { - // @ts-expect-error Turning off AfterAll -- ignore Jest LifeCycle Type - // eslint-disable-next-line no-global-assign - afterAll = false - }) - - describe('first', () => { - test('should patch console.error', () => { - require('..') - expect(console.error).not.toBe(originalConsoleError) - }) - }) - - describe('second', () => { - test('should still used patched console.error', () => { - expect(console.error).not.toBe(originalConsoleError) - }) - }) -}) - -export {} diff --git a/src/server/__tests__/errorSuppression.noAfterEach.test.ts b/src/server/__tests__/errorSuppression.noAfterEach.test.ts new file mode 100644 index 00000000..9acc48f5 --- /dev/null +++ b/src/server/__tests__/errorSuppression.noAfterEach.test.ts @@ -0,0 +1,19 @@ +// This verifies that if afterEach is unavailable +// then we DON'T auto-wire up the afterEach for folks +describe('error output suppression (noAfterEach) tests', () => { + const originalConsoleError = console.error + + beforeAll(() => { + // @ts-expect-error Turning off AfterEach -- ignore Jest LifeCycle Type + afterEach = false + }) + + describe('first', () => { + test('should not patch console.error', () => { + require('..') + expect(console.error).toBe(originalConsoleError) + }) + }) +}) + +export {} diff --git a/src/server/__tests__/errorSuppression.noBeforeEach.test.ts b/src/server/__tests__/errorSuppression.noBeforeEach.test.ts new file mode 100644 index 00000000..2e28b4f8 --- /dev/null +++ b/src/server/__tests__/errorSuppression.noBeforeEach.test.ts @@ -0,0 +1,19 @@ +// This verifies that if afterEach is unavailable +// then we DON'T auto-wire up the afterEach for folks +describe('error output suppression (noBeforeEach) tests', () => { + const originalConsoleError = console.error + + beforeAll(() => { + // @ts-expect-error Turning off BeforeEach -- ignore Jest LifeCycle Type + beforeEach = false + }) + + describe('first', () => { + test('should not patch console.error', () => { + require('..') + expect(console.error).toBe(originalConsoleError) + }) + }) +}) + +export {} From e22adbb18e35bb63c42372acbe8af73d4259616d Mon Sep 17 00:00:00 2001 From: Michael Peyper Date: Tue, 19 Jan 2021 15:52:31 +1100 Subject: [PATCH 6/9] chore: refactor error suppression tests to require in setup so hooks can actually be registered --- src/dom/__tests__/errorSuppression.noAfterEach.test.ts | 8 +++----- src/dom/__tests__/errorSuppression.noBeforeEach.test.ts | 8 +++----- src/dom/__tests__/errorSuppression.pure.test.ts | 7 ++++++- src/native/__tests__/errorSuppression.noAfterEach.test.ts | 8 +++----- .../__tests__/errorSuppression.noBeforeEach.test.ts | 8 +++----- src/native/__tests__/errorSuppression.pure.test.ts | 7 ++++++- src/server/__tests__/errorSuppression.noAfterEach.test.ts | 8 +++----- .../__tests__/errorSuppression.noBeforeEach.test.ts | 8 +++----- src/server/__tests__/errorSuppression.pure.test.ts | 7 ++++++- 9 files changed, 36 insertions(+), 33 deletions(-) diff --git a/src/dom/__tests__/errorSuppression.noAfterEach.test.ts b/src/dom/__tests__/errorSuppression.noAfterEach.test.ts index 9acc48f5..c736020e 100644 --- a/src/dom/__tests__/errorSuppression.noAfterEach.test.ts +++ b/src/dom/__tests__/errorSuppression.noAfterEach.test.ts @@ -6,13 +6,11 @@ describe('error output suppression (noAfterEach) tests', () => { beforeAll(() => { // @ts-expect-error Turning off AfterEach -- ignore Jest LifeCycle Type afterEach = false + require('..') }) - describe('first', () => { - test('should not patch console.error', () => { - require('..') - expect(console.error).toBe(originalConsoleError) - }) + test('should not patch console.error', () => { + expect(console.error).toBe(originalConsoleError) }) }) diff --git a/src/dom/__tests__/errorSuppression.noBeforeEach.test.ts b/src/dom/__tests__/errorSuppression.noBeforeEach.test.ts index 2e28b4f8..c3f2496f 100644 --- a/src/dom/__tests__/errorSuppression.noBeforeEach.test.ts +++ b/src/dom/__tests__/errorSuppression.noBeforeEach.test.ts @@ -6,13 +6,11 @@ describe('error output suppression (noBeforeEach) tests', () => { beforeAll(() => { // @ts-expect-error Turning off BeforeEach -- ignore Jest LifeCycle Type beforeEach = false + require('..') }) - describe('first', () => { - test('should not patch console.error', () => { - require('..') - expect(console.error).toBe(originalConsoleError) - }) + test('should not patch console.error', () => { + expect(console.error).toBe(originalConsoleError) }) }) diff --git a/src/dom/__tests__/errorSuppression.pure.test.ts b/src/dom/__tests__/errorSuppression.pure.test.ts index 4a494d81..1a3368b2 100644 --- a/src/dom/__tests__/errorSuppression.pure.test.ts +++ b/src/dom/__tests__/errorSuppression.pure.test.ts @@ -1,8 +1,13 @@ +// This verifies that if pure imports are used +// then we DON'T auto-wire up the afterEach for folks describe('error output suppression (pure) tests', () => { const originalConsoleError = console.error - test('should not patch console.error', () => { + beforeAll(() => { require('../pure') + }) + + test('should not patch console.error', () => { expect(console.error).toBe(originalConsoleError) }) }) diff --git a/src/native/__tests__/errorSuppression.noAfterEach.test.ts b/src/native/__tests__/errorSuppression.noAfterEach.test.ts index 9acc48f5..c736020e 100644 --- a/src/native/__tests__/errorSuppression.noAfterEach.test.ts +++ b/src/native/__tests__/errorSuppression.noAfterEach.test.ts @@ -6,13 +6,11 @@ describe('error output suppression (noAfterEach) tests', () => { beforeAll(() => { // @ts-expect-error Turning off AfterEach -- ignore Jest LifeCycle Type afterEach = false + require('..') }) - describe('first', () => { - test('should not patch console.error', () => { - require('..') - expect(console.error).toBe(originalConsoleError) - }) + test('should not patch console.error', () => { + expect(console.error).toBe(originalConsoleError) }) }) diff --git a/src/native/__tests__/errorSuppression.noBeforeEach.test.ts b/src/native/__tests__/errorSuppression.noBeforeEach.test.ts index 2e28b4f8..c3f2496f 100644 --- a/src/native/__tests__/errorSuppression.noBeforeEach.test.ts +++ b/src/native/__tests__/errorSuppression.noBeforeEach.test.ts @@ -6,13 +6,11 @@ describe('error output suppression (noBeforeEach) tests', () => { beforeAll(() => { // @ts-expect-error Turning off BeforeEach -- ignore Jest LifeCycle Type beforeEach = false + require('..') }) - describe('first', () => { - test('should not patch console.error', () => { - require('..') - expect(console.error).toBe(originalConsoleError) - }) + test('should not patch console.error', () => { + expect(console.error).toBe(originalConsoleError) }) }) diff --git a/src/native/__tests__/errorSuppression.pure.test.ts b/src/native/__tests__/errorSuppression.pure.test.ts index 4a494d81..1a3368b2 100644 --- a/src/native/__tests__/errorSuppression.pure.test.ts +++ b/src/native/__tests__/errorSuppression.pure.test.ts @@ -1,8 +1,13 @@ +// This verifies that if pure imports are used +// then we DON'T auto-wire up the afterEach for folks describe('error output suppression (pure) tests', () => { const originalConsoleError = console.error - test('should not patch console.error', () => { + beforeAll(() => { require('../pure') + }) + + test('should not patch console.error', () => { expect(console.error).toBe(originalConsoleError) }) }) diff --git a/src/server/__tests__/errorSuppression.noAfterEach.test.ts b/src/server/__tests__/errorSuppression.noAfterEach.test.ts index 9acc48f5..c736020e 100644 --- a/src/server/__tests__/errorSuppression.noAfterEach.test.ts +++ b/src/server/__tests__/errorSuppression.noAfterEach.test.ts @@ -6,13 +6,11 @@ describe('error output suppression (noAfterEach) tests', () => { beforeAll(() => { // @ts-expect-error Turning off AfterEach -- ignore Jest LifeCycle Type afterEach = false + require('..') }) - describe('first', () => { - test('should not patch console.error', () => { - require('..') - expect(console.error).toBe(originalConsoleError) - }) + test('should not patch console.error', () => { + expect(console.error).toBe(originalConsoleError) }) }) diff --git a/src/server/__tests__/errorSuppression.noBeforeEach.test.ts b/src/server/__tests__/errorSuppression.noBeforeEach.test.ts index 2e28b4f8..c3f2496f 100644 --- a/src/server/__tests__/errorSuppression.noBeforeEach.test.ts +++ b/src/server/__tests__/errorSuppression.noBeforeEach.test.ts @@ -6,13 +6,11 @@ describe('error output suppression (noBeforeEach) tests', () => { beforeAll(() => { // @ts-expect-error Turning off BeforeEach -- ignore Jest LifeCycle Type beforeEach = false + require('..') }) - describe('first', () => { - test('should not patch console.error', () => { - require('..') - expect(console.error).toBe(originalConsoleError) - }) + test('should not patch console.error', () => { + expect(console.error).toBe(originalConsoleError) }) }) diff --git a/src/server/__tests__/errorSuppression.pure.test.ts b/src/server/__tests__/errorSuppression.pure.test.ts index 4a494d81..1a3368b2 100644 --- a/src/server/__tests__/errorSuppression.pure.test.ts +++ b/src/server/__tests__/errorSuppression.pure.test.ts @@ -1,8 +1,13 @@ +// This verifies that if pure imports are used +// then we DON'T auto-wire up the afterEach for folks describe('error output suppression (pure) tests', () => { const originalConsoleError = console.error - test('should not patch console.error', () => { + beforeAll(() => { require('../pure') + }) + + test('should not patch console.error', () => { expect(console.error).toBe(originalConsoleError) }) }) From 94371170e1efc2edc7b00570593d6f9c27e14249 Mon Sep 17 00:00:00 2001 From: Michael Peyper Date: Tue, 19 Jan 2021 15:56:50 +1100 Subject: [PATCH 7/9] chore: added additional tests to ensure pure imports don't add side effects --- .../__tests__/autoCleanup.disabled.test.ts | 2 +- .../__tests__/autoCleanup.noAfterEach.test.ts | 2 +- src/dom/__tests__/autoCleanup.pure.test.ts | 29 +++++++++++++++++++ .../__tests__/autoCleanup.disabled.test.ts | 2 +- .../__tests__/autoCleanup.noAfterEach.test.ts | 2 +- src/native/__tests__/autoCleanup.pure.test.ts | 29 +++++++++++++++++++ .../__tests__/autoCleanup.disabled.test.ts | 2 +- .../__tests__/autoCleanup.noAfterEach.test.ts | 2 +- src/server/__tests__/autoCleanup.pure.test.ts | 29 +++++++++++++++++++ 9 files changed, 93 insertions(+), 6 deletions(-) create mode 100644 src/dom/__tests__/autoCleanup.pure.test.ts create mode 100644 src/native/__tests__/autoCleanup.pure.test.ts create mode 100644 src/server/__tests__/autoCleanup.pure.test.ts diff --git a/src/dom/__tests__/autoCleanup.disabled.test.ts b/src/dom/__tests__/autoCleanup.disabled.test.ts index c5d4eaf5..2c574b83 100644 --- a/src/dom/__tests__/autoCleanup.disabled.test.ts +++ b/src/dom/__tests__/autoCleanup.disabled.test.ts @@ -6,7 +6,7 @@ import { ReactHooksRenderer } from '../../types/react' // then we DON'T auto-wire up the afterEach for folks describe('skip auto cleanup (disabled) tests', () => { let cleanupCalled = false - let renderHook: (arg0: () => void) => void + let renderHook: ReactHooksRenderer['renderHook'] beforeAll(() => { process.env.RHTL_SKIP_AUTO_CLEANUP = 'true' diff --git a/src/dom/__tests__/autoCleanup.noAfterEach.test.ts b/src/dom/__tests__/autoCleanup.noAfterEach.test.ts index 09f6bba6..40b33f16 100644 --- a/src/dom/__tests__/autoCleanup.noAfterEach.test.ts +++ b/src/dom/__tests__/autoCleanup.noAfterEach.test.ts @@ -6,7 +6,7 @@ import { ReactHooksRenderer } from '../../types/react' // then we DON'T auto-wire up the afterEach for folks describe('skip auto cleanup (no afterEach) tests', () => { let cleanupCalled = false - let renderHook: (arg0: () => void) => void + let renderHook: ReactHooksRenderer['renderHook'] beforeAll(() => { // @ts-expect-error Turning off AfterEach -- ignore Jest LifeCycle Type diff --git a/src/dom/__tests__/autoCleanup.pure.test.ts b/src/dom/__tests__/autoCleanup.pure.test.ts new file mode 100644 index 00000000..1f84b87c --- /dev/null +++ b/src/dom/__tests__/autoCleanup.pure.test.ts @@ -0,0 +1,29 @@ +import { useEffect } from 'react' + +import { ReactHooksRenderer } from '../../types/react' + +// This verifies that if pure imports are used +// then we DON'T auto-wire up the afterEach for folks +describe('skip auto cleanup (pure) tests', () => { + let cleanupCalled = false + let renderHook: ReactHooksRenderer['renderHook'] + + beforeAll(() => { + renderHook = (require('../pure') as ReactHooksRenderer).renderHook + }) + + test('first', () => { + const hookWithCleanup = () => { + useEffect(() => { + return () => { + cleanupCalled = true + } + }) + } + renderHook(() => hookWithCleanup()) + }) + + test('second', () => { + expect(cleanupCalled).toBe(false) + }) +}) diff --git a/src/native/__tests__/autoCleanup.disabled.test.ts b/src/native/__tests__/autoCleanup.disabled.test.ts index c5d4eaf5..2c574b83 100644 --- a/src/native/__tests__/autoCleanup.disabled.test.ts +++ b/src/native/__tests__/autoCleanup.disabled.test.ts @@ -6,7 +6,7 @@ import { ReactHooksRenderer } from '../../types/react' // then we DON'T auto-wire up the afterEach for folks describe('skip auto cleanup (disabled) tests', () => { let cleanupCalled = false - let renderHook: (arg0: () => void) => void + let renderHook: ReactHooksRenderer['renderHook'] beforeAll(() => { process.env.RHTL_SKIP_AUTO_CLEANUP = 'true' diff --git a/src/native/__tests__/autoCleanup.noAfterEach.test.ts b/src/native/__tests__/autoCleanup.noAfterEach.test.ts index 09f6bba6..40b33f16 100644 --- a/src/native/__tests__/autoCleanup.noAfterEach.test.ts +++ b/src/native/__tests__/autoCleanup.noAfterEach.test.ts @@ -6,7 +6,7 @@ import { ReactHooksRenderer } from '../../types/react' // then we DON'T auto-wire up the afterEach for folks describe('skip auto cleanup (no afterEach) tests', () => { let cleanupCalled = false - let renderHook: (arg0: () => void) => void + let renderHook: ReactHooksRenderer['renderHook'] beforeAll(() => { // @ts-expect-error Turning off AfterEach -- ignore Jest LifeCycle Type diff --git a/src/native/__tests__/autoCleanup.pure.test.ts b/src/native/__tests__/autoCleanup.pure.test.ts new file mode 100644 index 00000000..1f84b87c --- /dev/null +++ b/src/native/__tests__/autoCleanup.pure.test.ts @@ -0,0 +1,29 @@ +import { useEffect } from 'react' + +import { ReactHooksRenderer } from '../../types/react' + +// This verifies that if pure imports are used +// then we DON'T auto-wire up the afterEach for folks +describe('skip auto cleanup (pure) tests', () => { + let cleanupCalled = false + let renderHook: ReactHooksRenderer['renderHook'] + + beforeAll(() => { + renderHook = (require('../pure') as ReactHooksRenderer).renderHook + }) + + test('first', () => { + const hookWithCleanup = () => { + useEffect(() => { + return () => { + cleanupCalled = true + } + }) + } + renderHook(() => hookWithCleanup()) + }) + + test('second', () => { + expect(cleanupCalled).toBe(false) + }) +}) diff --git a/src/server/__tests__/autoCleanup.disabled.test.ts b/src/server/__tests__/autoCleanup.disabled.test.ts index c5d4eaf5..2c574b83 100644 --- a/src/server/__tests__/autoCleanup.disabled.test.ts +++ b/src/server/__tests__/autoCleanup.disabled.test.ts @@ -6,7 +6,7 @@ import { ReactHooksRenderer } from '../../types/react' // then we DON'T auto-wire up the afterEach for folks describe('skip auto cleanup (disabled) tests', () => { let cleanupCalled = false - let renderHook: (arg0: () => void) => void + let renderHook: ReactHooksRenderer['renderHook'] beforeAll(() => { process.env.RHTL_SKIP_AUTO_CLEANUP = 'true' diff --git a/src/server/__tests__/autoCleanup.noAfterEach.test.ts b/src/server/__tests__/autoCleanup.noAfterEach.test.ts index 09f6bba6..40b33f16 100644 --- a/src/server/__tests__/autoCleanup.noAfterEach.test.ts +++ b/src/server/__tests__/autoCleanup.noAfterEach.test.ts @@ -6,7 +6,7 @@ import { ReactHooksRenderer } from '../../types/react' // then we DON'T auto-wire up the afterEach for folks describe('skip auto cleanup (no afterEach) tests', () => { let cleanupCalled = false - let renderHook: (arg0: () => void) => void + let renderHook: ReactHooksRenderer['renderHook'] beforeAll(() => { // @ts-expect-error Turning off AfterEach -- ignore Jest LifeCycle Type diff --git a/src/server/__tests__/autoCleanup.pure.test.ts b/src/server/__tests__/autoCleanup.pure.test.ts new file mode 100644 index 00000000..1f84b87c --- /dev/null +++ b/src/server/__tests__/autoCleanup.pure.test.ts @@ -0,0 +1,29 @@ +import { useEffect } from 'react' + +import { ReactHooksRenderer } from '../../types/react' + +// This verifies that if pure imports are used +// then we DON'T auto-wire up the afterEach for folks +describe('skip auto cleanup (pure) tests', () => { + let cleanupCalled = false + let renderHook: ReactHooksRenderer['renderHook'] + + beforeAll(() => { + renderHook = (require('../pure') as ReactHooksRenderer).renderHook + }) + + test('first', () => { + const hookWithCleanup = () => { + useEffect(() => { + return () => { + cleanupCalled = true + } + }) + } + renderHook(() => hookWithCleanup()) + }) + + test('second', () => { + expect(cleanupCalled).toBe(false) + }) +}) From 062de351ee8882c593cf7636cba6e1e22866f972 Mon Sep 17 00:00:00 2001 From: Michael Peyper Date: Tue, 19 Jan 2021 17:01:01 +1100 Subject: [PATCH 8/9] refactor: clean up unnecessary additional types in internal console suppression function --- src/core/console.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/console.ts b/src/core/console.ts index e1a326d0..1482cf2b 100644 --- a/src/core/console.ts +++ b/src/core/console.ts @@ -7,7 +7,7 @@ function enableErrorOutputSuppression() { typeof afterEach === 'function' && !process.env.RHTL_DISABLE_ERROR_FILTERING ) { - let restoreConsole: (() => void) | null = null + let restoreConsole: () => void beforeEach(() => { restoreConsole = filterConsole( From 934fa6a366ed267faf45f4e907b4f5dfaa088bdf Mon Sep 17 00:00:00 2001 From: Michael Peyper Date: Fri, 22 Jan 2021 12:20:48 +1100 Subject: [PATCH 9/9] docs: remove link in API reference docs --- docs/api-reference.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/api-reference.md b/docs/api-reference.md index 3fc61a60..8576e076 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -12,7 +12,6 @@ route: '/reference/api' - [`cleanup`](/reference/api#cleanup) - [`addCleanup`](/reference/api#addcleanup) - [`removeCleanup`](/reference/api#removecleanup) -- [`console.error`](/reference/api#consoleerror) ---