Skip to content

Commit

Permalink
fix: only suppress console.error for non-pure imports
Browse files Browse the repository at this point in the history
Fixes #546
  • Loading branch information
mpeyper committed Jan 18, 2021
1 parent b87c30c commit 2e7b588
Show file tree
Hide file tree
Showing 15 changed files with 310 additions and 32 deletions.
1 change: 1 addition & 0 deletions disable-error-filtering.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
process.env.RHTL_DISABLE_ERROR_FILTERING = true
49 changes: 48 additions & 1 deletion docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ route: '/reference/api'
- [`cleanup`](/reference/api#cleanup)
- [`addCleanup`](/reference/api#addcleanup)
- [`removeCleanup`](/reference/api#removecleanup)
- [`console.error`](/reference/api#consoleerror)

---

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"native",
"server",
"pure",
"disable-error-filtering.js",
"dont-cleanup-after-each.js"
],
"author": "Michael Peyper <mpeyper7@gmail.com>",
Expand Down
25 changes: 25 additions & 0 deletions src/core/console.ts
Original file line number Diff line number Diff line change
@@ -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 <TestComponent> 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 }
53 changes: 50 additions & 3 deletions src/dom/__tests__/errorHook.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react'
import { renderHook } from '..'
import { renderHook, act } from '..'

describe('error hook tests', () => {
function useError(throwError?: boolean) {
Expand Down Expand Up @@ -109,15 +109,15 @@ describe('error hook tests', () => {
})

describe('effect', () => {
test('should raise effect error', () => {
test('this one - should raise effect error', () => {
const { result } = renderHook(() => useEffectError(true))

expect(() => {
expect(result.current).not.toBe(undefined)
}).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'))
})
Expand All @@ -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
}
})
})
})
39 changes: 39 additions & 0 deletions src/dom/__tests__/errorSuppression.disabled.test.ts
Original file line number Diff line number Diff line change
@@ -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 <TestComponent> component:/)
)
expect(mockConsoleError).toBeCalledTimes(2)
})
})
35 changes: 35 additions & 0 deletions src/dom/__tests__/errorSuppression.pure.test.ts
Original file line number Diff line number Diff line change
@@ -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 <TestComponent> component:/)
)
expect(mockConsoleError).toBeCalledTimes(2)
})
})
2 changes: 2 additions & 0 deletions src/dom/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { autoRegisterCleanup } from '../core/cleanup'
import { enableErrorOutputSuppression } from '../core/console'

autoRegisterCleanup()
enableErrorOutputSuppression()

export * from './pure'
25 changes: 0 additions & 25 deletions src/helpers/createTestHarness.tsx
Original file line number Diff line number Diff line change
@@ -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 <TestComponent> component:/, // error boundary output
/^Error: Uncaught .+/ // jsdom output
],
{
methods: ['error']
}
)
addCleanup(removeConsoleFilter)
}

function createTestHarness<TProps, TResult>(
{ callback, setValue, setError }: RendererProps<TProps, TResult>,
Wrapper?: WrapperComponent<TProps>,
Expand All @@ -47,8 +24,6 @@ function createTestHarness<TProps, TResult>(
return null
}

suppressErrorOutput()

const testHarness = (props?: TProps) => {
resetErrorBoundary()

Expand Down
6 changes: 5 additions & 1 deletion src/helpers/promises.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<unknown>).then === 'function'
}

export { resolveAfter, callAfter, isPromise }
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { autoRegisterCleanup } from './core/cleanup'
import { enableErrorOutputSuppression } from './core/console'

autoRegisterCleanup()
enableErrorOutputSuppression()

export * from './pure'
49 changes: 48 additions & 1 deletion src/native/__tests__/errorHook.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react'
import { renderHook } from '..'
import { renderHook, act } from '..'

describe('error hook tests', () => {
function useError(throwError?: boolean) {
Expand Down Expand Up @@ -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
}
})
})
})
2 changes: 2 additions & 0 deletions src/native/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { autoRegisterCleanup } from '../core/cleanup'
import { enableErrorOutputSuppression } from '../core/console'

autoRegisterCleanup()
enableErrorOutputSuppression()

export * from './pure'

0 comments on commit 2e7b588

Please sign in to comment.