Skip to content

Commit 804d9ac

Browse files
authored
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
1 parent 9af1343 commit 804d9ac

34 files changed

+551
-53
lines changed

disable-error-filtering.js

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
process.env.RHTL_DISABLE_ERROR_FILTERING = true

docs/api-reference.md

+48-3
Original file line numberDiff line numberDiff line change
@@ -153,9 +153,8 @@ module.exports = {
153153
}
154154
```
155155

156-
Alternatively, you can change your test to import from `@testing-library/react-hooks/pure` instead
157-
of the regular imports. This applys to any of our export methods documented in
158-
[Rendering](/installation#being-specific).
156+
Alternatively, you can change your test to import from `@testing-library/react-hooks/pure` (or any
157+
of the [other non-pure imports](/installation#pure-imports)) instead of the regular imports.
159158

160159
```diff
161160
- 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`.
270269
_Default: 1000_
271270

272271
The maximum amount of time in milliseconds (ms) to wait.
272+
273+
---
274+
275+
## `console.error`
276+
277+
In order to catch errors that are produced in all parts of the hook's lifecycle, the test harness
278+
used to wrap the hook call includes an
279+
[Error Boundary](https://reactjs.org/docs/error-boundaries.html) which causes a
280+
[significant amount of output noise](https://reactjs.org/docs/error-boundaries.html#component-stack-traces)
281+
in tests.
282+
283+
To keep test output clean, we patch `console.error` when importing from
284+
`@testing-library/react-hooks` (or any of the [other non-pure imports](/installation#pure-imports))
285+
to filter out the unnecessary logging and restore the original version during cleanup. This
286+
side-effect can affect tests that also patch `console.error` (e.g. to assert a specific error
287+
message get logged) by replacing their custom implementation as well.
288+
289+
### Disabling `console.error` filtering
290+
291+
Importing `@testing-library/react-hooks/disable-error-filtering.js` in test setup files disable the
292+
error filtering feature and not patch `console.error` in any way.
293+
294+
For example, in [Jest](https://jestjs.io/) this can be added to your
295+
[Jest config](https://jestjs.io/docs/configuration):
296+
297+
```js
298+
module.exports = {
299+
setupFilesAfterEnv: [
300+
'@testing-library/react-hooks/disable-error-filtering.js'
301+
// other setup files
302+
]
303+
}
304+
```
305+
306+
Alternatively, you can change your test to import from `@testing-library/react-hooks/pure` (or any
307+
of the [other non-pure imports](/installation#pure-imports)) instead of the regular imports.
308+
309+
```diff
310+
- import { renderHook, cleanup, act } from '@testing-library/react-hooks'
311+
+ import { renderHook, cleanup, act } from '@testing-library/react-hooks/pure'
312+
```
313+
314+
If neither of these approaches are suitable, setting the `RHTL_DISABLE_ERROR_FILTERING` environment
315+
variable to `true` before importing `@testing-library/react-hooks` will also disable this feature.
316+
317+
> Please note that this may result is a significant amount of additional logging in you test output.

docs/installation.md

+26-2
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ npm install --save-dev @testing-library/react-hooks
1717
yarn add --dev @testing-library/react-hooks
1818
```
1919

20-
### Peer Dependencies
20+
### Peer dependencies
2121

2222
`react-hooks-testing-library` does not come bundled with a version of
2323
[`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
9292
import { renderHook, act } from '@testing-library/react-hooks/server' // will use react-dom/server
9393
```
9494
95-
## Testing Framework
95+
## Pure imports
96+
97+
Importing from any of the previously mentioned imports will cause some side effects in the test
98+
environment:
99+
100+
1. `cleanup` is automatically called in an `afterEach` block
101+
2. `console.error` is patched to hide some React errors
102+
103+
The specifics of these side effects are discussed in more detail in the
104+
[API reference](/reference/api).
105+
106+
If you want to ensure the imports are free of side-effects, you can use the `pure` imports instead,
107+
which can be accessed by appending `/pure` to the end of any of the other imports:
108+
109+
```ts
110+
import { renderHook, act } from '@testing-library/react-hooks/pure'
111+
112+
import { renderHook, act } from '@testing-library/react-hooks/dom/pure'
113+
114+
import { renderHook, act } from '@testing-library/react-hooks/native/pure'
115+
116+
import { renderHook, act } from '@testing-library/react-hooks/server/pure'
117+
```
118+
119+
## Testing framework
96120

97121
In order to run tests, you will probably want to be using a test framework. If you have not already
98122
got one, we recommend using [Jest](https://jestjs.io/), but this library should work without issues

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"native",
1919
"server",
2020
"pure",
21+
"disable-error-filtering.js",
2122
"dont-cleanup-after-each.js"
2223
],
2324
"author": "Michael Peyper <[email protected]>",

src/core/console.ts

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import filterConsole from 'filter-console'
2+
3+
function enableErrorOutputSuppression() {
4+
// Automatically registers console error suppression and restoration in supported testing frameworks
5+
if (
6+
typeof beforeEach === 'function' &&
7+
typeof afterEach === 'function' &&
8+
!process.env.RHTL_DISABLE_ERROR_FILTERING
9+
) {
10+
let restoreConsole: () => void
11+
12+
beforeEach(() => {
13+
restoreConsole = filterConsole(
14+
[
15+
/^The above error occurred in the <TestComponent> component:/, // error boundary output
16+
/^Error: Uncaught .+/ // jsdom output
17+
],
18+
{
19+
methods: ['error']
20+
}
21+
)
22+
})
23+
24+
afterEach(() => restoreConsole?.())
25+
}
26+
}
27+
28+
export { enableErrorOutputSuppression }

src/dom/__tests__/autoCleanup.disabled.test.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,10 @@ import { ReactHooksRenderer } from '../../types/react'
66
// then we DON'T auto-wire up the afterEach for folks
77
describe('skip auto cleanup (disabled) tests', () => {
88
let cleanupCalled = false
9-
let renderHook: (arg0: () => void) => void
9+
let renderHook: ReactHooksRenderer['renderHook']
1010

1111
beforeAll(() => {
1212
process.env.RHTL_SKIP_AUTO_CLEANUP = 'true'
13-
// eslint-disable-next-line @typescript-eslint/no-var-requires
1413
renderHook = (require('..') as ReactHooksRenderer).renderHook
1514
})
1615

src/dom/__tests__/autoCleanup.noAfterEach.test.ts

+2-4
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,15 @@ import { useEffect } from 'react'
22

33
import { ReactHooksRenderer } from '../../types/react'
44

5-
// This verifies that if RHTL_SKIP_AUTO_CLEANUP is set
5+
// This verifies that if afterEach is unavailable
66
// then we DON'T auto-wire up the afterEach for folks
77
describe('skip auto cleanup (no afterEach) tests', () => {
88
let cleanupCalled = false
9-
let renderHook: (arg0: () => void) => void
9+
let renderHook: ReactHooksRenderer['renderHook']
1010

1111
beforeAll(() => {
1212
// @ts-expect-error Turning off AfterEach -- ignore Jest LifeCycle Type
13-
// eslint-disable-next-line no-global-assign
1413
afterEach = false
15-
// eslint-disable-next-line @typescript-eslint/no-var-requires
1614
renderHook = (require('..') as ReactHooksRenderer).renderHook
1715
})
1816

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { useEffect } from 'react'
2+
3+
import { ReactHooksRenderer } from '../../types/react'
4+
5+
// This verifies that if pure imports are used
6+
// then we DON'T auto-wire up the afterEach for folks
7+
describe('skip auto cleanup (pure) tests', () => {
8+
let cleanupCalled = false
9+
let renderHook: ReactHooksRenderer['renderHook']
10+
11+
beforeAll(() => {
12+
renderHook = (require('../pure') as ReactHooksRenderer).renderHook
13+
})
14+
15+
test('first', () => {
16+
const hookWithCleanup = () => {
17+
useEffect(() => {
18+
return () => {
19+
cleanupCalled = true
20+
}
21+
})
22+
}
23+
renderHook(() => hookWithCleanup())
24+
})
25+
26+
test('second', () => {
27+
expect(cleanupCalled).toBe(false)
28+
})
29+
})

src/dom/__tests__/errorHook.test.ts

+50-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useState, useEffect } from 'react'
2-
import { renderHook } from '..'
2+
import { renderHook, act } from '..'
33

44
describe('error hook tests', () => {
55
function useError(throwError?: boolean) {
@@ -109,15 +109,15 @@ describe('error hook tests', () => {
109109
})
110110

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

115115
expect(() => {
116116
expect(result.current).not.toBe(undefined)
117117
}).toThrow(Error('expected'))
118118
})
119119

120-
test('should capture effect error', () => {
120+
test('this one - should capture effect error', () => {
121121
const { result } = renderHook(() => useEffectError(true))
122122
expect(result.error).toEqual(Error('expected'))
123123
})
@@ -142,4 +142,51 @@ describe('error hook tests', () => {
142142
expect(result.error).toBe(undefined)
143143
})
144144
})
145+
146+
describe('error output suppression', () => {
147+
test('should allow console.error to be mocked', async () => {
148+
const consoleError = console.error
149+
console.error = jest.fn()
150+
151+
try {
152+
const { rerender, unmount } = renderHook(
153+
(stage) => {
154+
useEffect(() => {
155+
console.error(`expected in effect`)
156+
return () => {
157+
console.error(`expected in unmount`)
158+
}
159+
}, [])
160+
console.error(`expected in ${stage}`)
161+
},
162+
{
163+
initialProps: 'render'
164+
}
165+
)
166+
167+
act(() => {
168+
console.error('expected in act')
169+
})
170+
171+
await act(async () => {
172+
await new Promise((resolve) => setTimeout(resolve, 100))
173+
console.error('expected in async act')
174+
})
175+
176+
rerender('rerender')
177+
178+
unmount()
179+
180+
expect(console.error).toBeCalledWith('expected in render')
181+
expect(console.error).toBeCalledWith('expected in effect')
182+
expect(console.error).toBeCalledWith('expected in act')
183+
expect(console.error).toBeCalledWith('expected in async act')
184+
expect(console.error).toBeCalledWith('expected in rerender')
185+
expect(console.error).toBeCalledWith('expected in unmount')
186+
expect(console.error).toBeCalledTimes(6)
187+
} finally {
188+
console.error = consoleError
189+
}
190+
})
191+
})
145192
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// This verifies that if RHTL_DISABLE_ERROR_FILTERING is set
2+
// then we DON'T auto-wire up the afterEach for folks
3+
describe('error output suppression (disabled) tests', () => {
4+
const originalConsoleError = console.error
5+
6+
beforeAll(() => {
7+
process.env.RHTL_DISABLE_ERROR_FILTERING = 'true'
8+
})
9+
10+
test('should not patch console.error', () => {
11+
require('..')
12+
expect(console.error).toBe(originalConsoleError)
13+
})
14+
})
15+
16+
export {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// This verifies that if afterEach is unavailable
2+
// then we DON'T auto-wire up the afterEach for folks
3+
describe('error output suppression (noAfterEach) tests', () => {
4+
const originalConsoleError = console.error
5+
6+
beforeAll(() => {
7+
// @ts-expect-error Turning off AfterEach -- ignore Jest LifeCycle Type
8+
afterEach = false
9+
require('..')
10+
})
11+
12+
test('should not patch console.error', () => {
13+
expect(console.error).toBe(originalConsoleError)
14+
})
15+
})
16+
17+
export {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// This verifies that if afterEach is unavailable
2+
// then we DON'T auto-wire up the afterEach for folks
3+
describe('error output suppression (noBeforeEach) tests', () => {
4+
const originalConsoleError = console.error
5+
6+
beforeAll(() => {
7+
// @ts-expect-error Turning off BeforeEach -- ignore Jest LifeCycle Type
8+
beforeEach = false
9+
require('..')
10+
})
11+
12+
test('should not patch console.error', () => {
13+
expect(console.error).toBe(originalConsoleError)
14+
})
15+
})
16+
17+
export {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// This verifies that if pure imports are used
2+
// then we DON'T auto-wire up the afterEach for folks
3+
describe('error output suppression (pure) tests', () => {
4+
const originalConsoleError = console.error
5+
6+
beforeAll(() => {
7+
require('../pure')
8+
})
9+
10+
test('should not patch console.error', () => {
11+
expect(console.error).toBe(originalConsoleError)
12+
})
13+
})
14+
15+
export {}

src/dom/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { autoRegisterCleanup } from '../core/cleanup'
2+
import { enableErrorOutputSuppression } from '../core/console'
23

34
autoRegisterCleanup()
5+
enableErrorOutputSuppression()
46

57
export * from './pure'

0 commit comments

Comments
 (0)