From 804d9acf873f3225fcb06fb45421924dc97544c5 Mon Sep 17 00:00:00 2001 From: Michael Peyper Date: Fri, 22 Jan 2021 21:08:29 +1100 Subject: [PATCH] fix: only suppress console.error for non-pure imports (#549) * fix: only suppress console.error for non-pure imports * refactor: remove unused promise util * chore: fix tests for error suppression to * docs: update docs to with more detail on side effects * refactor: only add suppression in beforeEach block and move restoration to afterEach * chore: refactor error suppression tests to require in setup so hooks can actually be registered * chore: added additional tests to ensure pure imports don't add side effects * refactor: clean up unnecessary additional types in internal console suppression function * docs: remove link in API reference docs Fixes #546 --- disable-error-filtering.js | 1 + docs/api-reference.md | 51 ++++++++++++++++-- docs/installation.md | 28 +++++++++- package.json | 1 + src/core/console.ts | 28 ++++++++++ .../__tests__/autoCleanup.disabled.test.ts | 3 +- .../__tests__/autoCleanup.noAfterEach.test.ts | 6 +-- src/dom/__tests__/autoCleanup.pure.test.ts | 29 ++++++++++ src/dom/__tests__/errorHook.test.ts | 53 +++++++++++++++++-- .../errorSuppression.disabled.test.ts | 16 ++++++ .../errorSuppression.noAfterEach.test.ts | 17 ++++++ .../errorSuppression.noBeforeEach.test.ts | 17 ++++++ .../__tests__/errorSuppression.pure.test.ts | 15 ++++++ src/dom/index.ts | 2 + src/helpers/createTestHarness.tsx | 25 --------- src/index.ts | 2 + .../__tests__/autoCleanup.disabled.test.ts | 3 +- .../__tests__/autoCleanup.noAfterEach.test.ts | 6 +-- src/native/__tests__/autoCleanup.pure.test.ts | 29 ++++++++++ src/native/__tests__/errorHook.test.ts | 49 ++++++++++++++++- .../errorSuppression.disabled.test.ts | 16 ++++++ .../errorSuppression.noAfterEach.test.ts | 17 ++++++ .../errorSuppression.noBeforeEach.test.ts | 17 ++++++ .../__tests__/errorSuppression.pure.test.ts | 15 ++++++ src/native/index.ts | 2 + .../__tests__/autoCleanup.disabled.test.ts | 3 +- .../__tests__/autoCleanup.noAfterEach.test.ts | 6 +-- src/server/__tests__/autoCleanup.pure.test.ts | 29 ++++++++++ src/server/__tests__/errorHook.test.ts | 51 +++++++++++++++++- .../errorSuppression.disabled.test.ts | 16 ++++++ .../errorSuppression.noAfterEach.test.ts | 17 ++++++ .../errorSuppression.noBeforeEach.test.ts | 17 ++++++ .../__tests__/errorSuppression.pure.test.ts | 15 ++++++ src/server/index.ts | 2 + 34 files changed, 551 insertions(+), 53 deletions(-) create mode 100644 disable-error-filtering.js create mode 100644 src/core/console.ts create mode 100644 src/dom/__tests__/autoCleanup.pure.test.ts create mode 100644 src/dom/__tests__/errorSuppression.disabled.test.ts create mode 100644 src/dom/__tests__/errorSuppression.noAfterEach.test.ts create mode 100644 src/dom/__tests__/errorSuppression.noBeforeEach.test.ts create mode 100644 src/dom/__tests__/errorSuppression.pure.test.ts create mode 100644 src/native/__tests__/autoCleanup.pure.test.ts create mode 100644 src/native/__tests__/errorSuppression.disabled.test.ts create mode 100644 src/native/__tests__/errorSuppression.noAfterEach.test.ts create mode 100644 src/native/__tests__/errorSuppression.noBeforeEach.test.ts create mode 100644 src/native/__tests__/errorSuppression.pure.test.ts create mode 100644 src/server/__tests__/autoCleanup.pure.test.ts create mode 100644 src/server/__tests__/errorSuppression.disabled.test.ts create mode 100644 src/server/__tests__/errorSuppression.noAfterEach.test.ts create mode 100644 src/server/__tests__/errorSuppression.noBeforeEach.test.ts create mode 100644 src/server/__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..8576e076 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -153,9 +153,8 @@ 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 -[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' @@ -270,3 +269,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 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 + +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` (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' ++ 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/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 diff --git a/package.json b/package.json index 543375c2..52e07618 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..1482cf2b --- /dev/null +++ b/src/core/console.ts @@ -0,0 +1,28 @@ +import filterConsole from 'filter-console' + +function enableErrorOutputSuppression() { + // 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 + + beforeEach(() => { + restoreConsole = filterConsole( + [ + /^The above error occurred in the component:/, // error boundary output + /^Error: Uncaught .+/ // jsdom output + ], + { + methods: ['error'] + } + ) + }) + + afterEach(() => restoreConsole?.()) + } +} + +export { enableErrorOutputSuppression } diff --git a/src/dom/__tests__/autoCleanup.disabled.test.ts b/src/dom/__tests__/autoCleanup.disabled.test.ts index 1485ff47..2c574b83 100644 --- a/src/dom/__tests__/autoCleanup.disabled.test.ts +++ b/src/dom/__tests__/autoCleanup.disabled.test.ts @@ -6,11 +6,10 @@ 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' - // 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..40b33f16 100644 --- a/src/dom/__tests__/autoCleanup.noAfterEach.test.ts +++ b/src/dom/__tests__/autoCleanup.noAfterEach.test.ts @@ -2,17 +2,15 @@ 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 - let renderHook: (arg0: () => void) => void + let renderHook: ReactHooksRenderer['renderHook'] 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__/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/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..843a405e --- /dev/null +++ b/src/dom/__tests__/errorSuppression.disabled.test.ts @@ -0,0 +1,16 @@ +// 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 + + 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/dom/__tests__/errorSuppression.noAfterEach.test.ts b/src/dom/__tests__/errorSuppression.noAfterEach.test.ts new file mode 100644 index 00000000..c736020e --- /dev/null +++ b/src/dom/__tests__/errorSuppression.noAfterEach.test.ts @@ -0,0 +1,17 @@ +// 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 + require('..') + }) + + test('should not patch console.error', () => { + 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..c3f2496f --- /dev/null +++ b/src/dom/__tests__/errorSuppression.noBeforeEach.test.ts @@ -0,0 +1,17 @@ +// 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 + require('..') + }) + + test('should not patch console.error', () => { + expect(console.error).toBe(originalConsoleError) + }) +}) + +export {} diff --git a/src/dom/__tests__/errorSuppression.pure.test.ts b/src/dom/__tests__/errorSuppression.pure.test.ts new file mode 100644 index 00000000..1a3368b2 --- /dev/null +++ b/src/dom/__tests__/errorSuppression.pure.test.ts @@ -0,0 +1,15 @@ +// 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 + + beforeAll(() => { + require('../pure') + }) + + test('should not patch console.error', () => { + expect(console.error).toBe(originalConsoleError) + }) +}) + +export {} 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/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__/autoCleanup.disabled.test.ts b/src/native/__tests__/autoCleanup.disabled.test.ts index 1485ff47..2c574b83 100644 --- a/src/native/__tests__/autoCleanup.disabled.test.ts +++ b/src/native/__tests__/autoCleanup.disabled.test.ts @@ -6,11 +6,10 @@ 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' - // 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..40b33f16 100644 --- a/src/native/__tests__/autoCleanup.noAfterEach.test.ts +++ b/src/native/__tests__/autoCleanup.noAfterEach.test.ts @@ -2,17 +2,15 @@ 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 - let renderHook: (arg0: () => void) => void + let renderHook: ReactHooksRenderer['renderHook'] 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__/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/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/__tests__/errorSuppression.disabled.test.ts b/src/native/__tests__/errorSuppression.disabled.test.ts new file mode 100644 index 00000000..843a405e --- /dev/null +++ b/src/native/__tests__/errorSuppression.disabled.test.ts @@ -0,0 +1,16 @@ +// 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 + + 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.noAfterEach.test.ts b/src/native/__tests__/errorSuppression.noAfterEach.test.ts new file mode 100644 index 00000000..c736020e --- /dev/null +++ b/src/native/__tests__/errorSuppression.noAfterEach.test.ts @@ -0,0 +1,17 @@ +// 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 + require('..') + }) + + test('should not patch console.error', () => { + 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..c3f2496f --- /dev/null +++ b/src/native/__tests__/errorSuppression.noBeforeEach.test.ts @@ -0,0 +1,17 @@ +// 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 + require('..') + }) + + test('should not patch console.error', () => { + expect(console.error).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..1a3368b2 --- /dev/null +++ b/src/native/__tests__/errorSuppression.pure.test.ts @@ -0,0 +1,15 @@ +// 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 + + beforeAll(() => { + require('../pure') + }) + + test('should not patch console.error', () => { + expect(console.error).toBe(originalConsoleError) + }) +}) + +export {} 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__/autoCleanup.disabled.test.ts b/src/server/__tests__/autoCleanup.disabled.test.ts index 1485ff47..2c574b83 100644 --- a/src/server/__tests__/autoCleanup.disabled.test.ts +++ b/src/server/__tests__/autoCleanup.disabled.test.ts @@ -6,11 +6,10 @@ 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' - // 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..40b33f16 100644 --- a/src/server/__tests__/autoCleanup.noAfterEach.test.ts +++ b/src/server/__tests__/autoCleanup.noAfterEach.test.ts @@ -2,17 +2,15 @@ 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 - let renderHook: (arg0: () => void) => void + let renderHook: ReactHooksRenderer['renderHook'] 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__/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) + }) +}) 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/__tests__/errorSuppression.disabled.test.ts b/src/server/__tests__/errorSuppression.disabled.test.ts new file mode 100644 index 00000000..843a405e --- /dev/null +++ b/src/server/__tests__/errorSuppression.disabled.test.ts @@ -0,0 +1,16 @@ +// 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 + + 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.noAfterEach.test.ts b/src/server/__tests__/errorSuppression.noAfterEach.test.ts new file mode 100644 index 00000000..c736020e --- /dev/null +++ b/src/server/__tests__/errorSuppression.noAfterEach.test.ts @@ -0,0 +1,17 @@ +// 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 + require('..') + }) + + test('should not patch console.error', () => { + 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..c3f2496f --- /dev/null +++ b/src/server/__tests__/errorSuppression.noBeforeEach.test.ts @@ -0,0 +1,17 @@ +// 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 + require('..') + }) + + test('should not patch console.error', () => { + expect(console.error).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..1a3368b2 --- /dev/null +++ b/src/server/__tests__/errorSuppression.pure.test.ts @@ -0,0 +1,15 @@ +// 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 + + beforeAll(() => { + require('../pure') + }) + + test('should not patch console.error', () => { + expect(console.error).toBe(originalConsoleError) + }) +}) + +export {} 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'