Skip to content

Commit cdc351a

Browse files
committed
fix: reduce console.error suppressions to only while acting
1 parent 6335c4e commit cdc351a

File tree

9 files changed

+211
-34
lines changed

9 files changed

+211
-34
lines changed

src/dom/__tests__/errorHook.test.ts

+48-1
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) {
@@ -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
})

src/dom/pure.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import ReactDOM from 'react-dom'
2-
import { act } from 'react-dom/test-utils'
2+
import { act as baseAct } from 'react-dom/test-utils'
33

4-
import { RendererProps, RendererOptions } from '../types/react'
4+
import { RendererProps, RendererOptions, Act } from '../types/react'
55

66
import { createRenderHook } from '../core'
7+
import { createActWrapper } from '../helpers/act'
78
import { createTestHarness } from '../helpers/createTestHarness'
89

10+
const act = createActWrapper(baseAct)
11+
912
function createDomRenderer<TProps, TResult>(
1013
rendererProps: RendererProps<TProps, TResult>,
1114
{ wrapper }: RendererOptions<TProps>

src/helpers/act.ts

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
2+
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
3+
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
4+
import { Act } from '../types/react'
5+
6+
import { suppressErrorOutput } from './console'
7+
8+
function createActWrapper(baseAct: Act) {
9+
const act: Act = async (callback: () => any) => {
10+
const restoreOutput = suppressErrorOutput()
11+
try {
12+
let awaitRequired = false
13+
const actResult = (baseAct(() => {
14+
const callbackResult = callback()
15+
awaitRequired = callbackResult !== undefined && !!callbackResult.then
16+
return callbackResult
17+
}) as any) as PromiseLike<undefined>
18+
19+
return awaitRequired ? await actResult : undefined
20+
} finally {
21+
restoreOutput()
22+
}
23+
}
24+
25+
return act
26+
}
27+
28+
export { createActWrapper }

src/helpers/console.ts

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import filterConsole from 'filter-console'
2+
3+
function suppressErrorOutput() {
4+
// The error output from error boundaries is notoriously difficult to suppress. To save
5+
// our users from having to work it out, we crudely suppress the output matching the patterns
6+
// below. For more information, see these issues:
7+
// - https://github.com/testing-library/react-hooks-testing-library/issues/50
8+
// - https://github.com/facebook/react/issues/11098#issuecomment-412682721
9+
// - https://github.com/facebook/react/issues/15520
10+
// - https://github.com/facebook/react/issues/18841
11+
return filterConsole(
12+
[
13+
/^The above error occurred in the <TestComponent> component:/, // error boundary output
14+
/^Error: Uncaught .+/ // jsdom output
15+
],
16+
{
17+
methods: ['error']
18+
}
19+
)
20+
}
21+
22+
export { suppressErrorOutput }

src/helpers/createTestHarness.tsx

-25
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,8 @@
11
import React, { Suspense } from 'react'
22
import { ErrorBoundary, FallbackProps } from 'react-error-boundary'
3-
import filterConsole from 'filter-console'
4-
5-
import { addCleanup } from '../core'
63

74
import { RendererProps, WrapperComponent } from '../types/react'
85

9-
function suppressErrorOutput() {
10-
// The error output from error boundaries is notoriously difficult to suppress. To save
11-
// out users from having to work it out, we crudely suppress the output matching the patterns
12-
// below. For more information, see these issues:
13-
// - https://github.com/testing-library/react-hooks-testing-library/issues/50
14-
// - https://github.com/facebook/react/issues/11098#issuecomment-412682721
15-
// - https://github.com/facebook/react/issues/15520
16-
// - https://github.com/facebook/react/issues/18841
17-
const removeConsoleFilter = filterConsole(
18-
[
19-
/^The above error occurred in the <TestComponent> component:/, // error boundary output
20-
/^Error: Uncaught .+/ // jsdom output
21-
],
22-
{
23-
methods: ['error']
24-
}
25-
)
26-
addCleanup(removeConsoleFilter)
27-
}
28-
296
function createTestHarness<TProps, TResult>(
307
{ callback, setValue, setError }: RendererProps<TProps, TResult>,
318
Wrapper?: WrapperComponent<TProps>,
@@ -47,8 +24,6 @@ function createTestHarness<TProps, TResult>(
4724
return null
4825
}
4926

50-
suppressErrorOutput()
51-
5227
const testHarness = (props?: TProps) => {
5328
resetErrorBoundary()
5429

src/native/__tests__/errorHook.test.ts

+48-1
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) {
@@ -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
})

src/native/pure.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1-
import { act, create, ReactTestRenderer } from 'react-test-renderer'
1+
import { act as baseAct, create, ReactTestRenderer } from 'react-test-renderer'
22

3-
import { RendererProps, RendererOptions } from '../types/react'
3+
import { RendererProps, RendererOptions, Act } from '../types/react'
44

55
import { createRenderHook } from '../core'
6+
import { createActWrapper } from '../helpers/act'
67
import { createTestHarness } from '../helpers/createTestHarness'
78

9+
const act = createActWrapper(baseAct)
10+
811
function createNativeRenderer<TProps, TResult>(
912
rendererProps: RendererProps<TProps, TResult>,
1013
{ wrapper }: RendererOptions<TProps>

src/server/__tests__/errorHook.test.ts

+50-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useState, useEffect } from 'react'
22

3-
import { renderHook } from '..'
3+
import { renderHook, act } from '..'
44

55
describe('error hook tests', () => {
66
function useError(throwError?: boolean) {
@@ -163,4 +163,53 @@ describe('error hook tests', () => {
163163
expect(result.error).toBe(undefined)
164164
})
165165
})
166+
167+
describe('error output suppression', () => {
168+
test('should allow console.error to be mocked', async () => {
169+
const consoleError = console.error
170+
console.error = jest.fn()
171+
172+
try {
173+
const { hydrate, rerender, unmount } = renderHook(
174+
(stage) => {
175+
useEffect(() => {
176+
console.error(`expected in effect`)
177+
return () => {
178+
console.error(`expected in unmount`)
179+
}
180+
}, [])
181+
console.error(`expected in ${stage}`)
182+
},
183+
{
184+
initialProps: 'render'
185+
}
186+
)
187+
188+
hydrate()
189+
190+
act(() => {
191+
console.error('expected in act')
192+
})
193+
194+
await act(async () => {
195+
await new Promise((resolve) => setTimeout(resolve, 100))
196+
console.error('expected in async act')
197+
})
198+
199+
rerender('rerender')
200+
201+
unmount()
202+
203+
expect(console.error).toBeCalledWith('expected in render') // twice render/hydrate
204+
expect(console.error).toBeCalledWith('expected in effect')
205+
expect(console.error).toBeCalledWith('expected in act')
206+
expect(console.error).toBeCalledWith('expected in async act')
207+
expect(console.error).toBeCalledWith('expected in rerender')
208+
expect(console.error).toBeCalledWith('expected in unmount')
209+
expect(console.error).toBeCalledTimes(7)
210+
} finally {
211+
console.error = consoleError
212+
}
213+
})
214+
})
166215
})

src/server/pure.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import ReactDOMServer from 'react-dom/server'
22
import ReactDOM from 'react-dom'
3-
import { act } from 'react-dom/test-utils'
3+
import { act as baseAct } from 'react-dom/test-utils'
44

5-
import { RendererProps, RendererOptions } from '../types/react'
5+
import { RendererProps, RendererOptions, Act } from '../types/react'
66

77
import { createRenderHook } from '../core'
8+
import { createActWrapper } from '../helpers/act'
89
import { createTestHarness } from '../helpers/createTestHarness'
910

11+
const act = createActWrapper(baseAct)
12+
1013
function createServerRenderer<TProps, TResult>(
1114
rendererProps: RendererProps<TProps, TResult>,
1215
{ wrapper }: RendererOptions<TProps>

0 commit comments

Comments
 (0)