Skip to content

Commit e4b0aa3

Browse files
authoredMay 24, 2021
feat: Remove node specific dependencies and code to better support testing in browser environments
* feat: removed filter-console dependency and fallback if process.env is not available (#624) * fix: protect import helpers for setting env variables and comment why try/catch is being used BREAKING CHANGE: `suppressErrorOutput` will now work when explicitly called, even if the `RHTL_DISABLE_ERROR_FILTERING` env variable has been set Fixes #617
1 parent e11b63a commit e4b0aa3

25 files changed

+559
-188
lines changed
 

‎disable-error-filtering.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,8 @@
1-
process.env.RHTL_DISABLE_ERROR_FILTERING = true
1+
try {
2+
process.env.RHTL_DISABLE_ERROR_FILTERING = true
3+
} catch {
4+
// falling back in the case that process.env.RHTL_DISABLE_ERROR_FILTERING cannot be accessed (e.g. browser environment)
5+
console.warn(
6+
'Could not disable error filtering as process.env.RHTL_DISABLE_ERROR_FILTERING could not be accessed.'
7+
)
8+
}

‎dont-cleanup-after-each.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,8 @@
1-
process.env.RHTL_SKIP_AUTO_CLEANUP = true
1+
try {
2+
process.env.RHTL_SKIP_AUTO_CLEANUP = true
3+
} catch {
4+
// falling back in the case that process.env.RHTL_SKIP_AUTO_CLEANUP cannot be accessed (e.g. browser environment)
5+
console.warn(
6+
'Could not skip auto cleanup as process.env.RHTL_SKIP_AUTO_CLEANUP could not be accessed.'
7+
)
8+
}

‎package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@
5050
"@types/react": ">=16.9.0",
5151
"@types/react-dom": ">=16.9.0",
5252
"@types/react-test-renderer": ">=16.9.0",
53-
"filter-console": "^0.1.1",
5453
"react-error-boundary": "^3.1.0"
5554
},
5655
"devDependencies": {

‎src/core/cleanup.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,18 @@ function removeCleanup(callback: CleanupCallback) {
1818
cleanupCallbacks = cleanupCallbacks.filter((cb) => cb !== callback)
1919
}
2020

21+
function skipAutoCleanup() {
22+
try {
23+
return !!process.env.RHTL_SKIP_AUTO_CLEANUP
24+
} catch {
25+
// falling back in the case that process.env.RHTL_SKIP_AUTO_CLEANUP cannot be accessed (e.g. browser environment)
26+
return false
27+
}
28+
}
29+
2130
function autoRegisterCleanup() {
2231
// Automatically registers cleanup in supported testing frameworks
23-
if (typeof afterEach === 'function' && !process.env.RHTL_SKIP_AUTO_CLEANUP) {
32+
if (typeof afterEach === 'function' && !skipAutoCleanup()) {
2433
afterEach(async () => {
2534
await cleanup()
2635
})

‎src/core/console.ts

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,41 @@
1-
import filterConsole from 'filter-console'
1+
const consoleFilters = [
2+
/^The above error occurred in the <.*?> component:/, // error boundary output
3+
/^Error: Uncaught .+/ // jsdom output
4+
]
25

36
function suppressErrorOutput() {
4-
if (process.env.RHTL_DISABLE_ERROR_FILTERING) {
5-
return () => {}
6-
}
7+
const originalError = console.error
78

8-
return filterConsole(
9-
[
10-
/^The above error occurred in the <TestComponent> component:/, // error boundary output
11-
/^Error: Uncaught .+/ // jsdom output
12-
],
13-
{
14-
methods: ['error']
9+
const error = (...args: Parameters<typeof originalError>) => {
10+
const message = typeof args[0] === 'string' ? args[0] : null
11+
if (!message || !consoleFilters.some((filter) => filter.test(message))) {
12+
originalError(...args)
1513
}
16-
)
14+
}
15+
16+
console.error = error
17+
18+
return () => {
19+
console.error = originalError
20+
}
21+
}
22+
23+
function errorFilteringDisabled() {
24+
try {
25+
return !!process.env.RHTL_DISABLE_ERROR_FILTERING
26+
} catch {
27+
// falling back in the case that process.env.RHTL_DISABLE_ERROR_FILTERING cannot be accessed (e.g. browser environment)
28+
return false
29+
}
1730
}
1831

1932
function enableErrorOutputSuppression() {
2033
// Automatically registers console error suppression and restoration in supported testing frameworks
21-
if (typeof beforeEach === 'function' && typeof afterEach === 'function') {
34+
if (
35+
typeof beforeEach === 'function' &&
36+
typeof afterEach === 'function' &&
37+
!errorFilteringDisabled()
38+
) {
2239
let restoreConsole!: () => void
2340

2441
beforeEach(() => {
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { useEffect } from 'react'
2+
3+
import { ReactHooksRenderer } from '../../types/react'
4+
5+
// This verifies that if process.env is unavailable
6+
// then we still auto-wire up the afterEach for folks
7+
describe('skip auto cleanup (no process.env) tests', () => {
8+
const originalEnv = process.env
9+
let cleanupCalled = false
10+
let renderHook: ReactHooksRenderer['renderHook']
11+
12+
beforeAll(() => {
13+
process.env = {
14+
...process.env,
15+
get RHTL_SKIP_AUTO_CLEANUP(): string | undefined {
16+
throw new Error('expected')
17+
}
18+
}
19+
renderHook = (require('..') as ReactHooksRenderer).renderHook
20+
})
21+
22+
afterAll(() => {
23+
process.env = originalEnv
24+
})
25+
26+
test('first', () => {
27+
const hookWithCleanup = () => {
28+
useEffect(() => {
29+
return () => {
30+
cleanupCalled = true
31+
}
32+
})
33+
}
34+
renderHook(() => hookWithCleanup())
35+
})
36+
37+
test('second', () => {
38+
expect(cleanupCalled).toBe(true)
39+
})
40+
})

‎src/dom/__tests__/errorHook.test.ts

Lines changed: 0 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -142,51 +142,4 @@ 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-
})
192145
})
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// This verifies that if process.env is unavailable
2+
// then we still auto-wire up the afterEach for folks
3+
describe('error output suppression (no process.env) tests', () => {
4+
const originalEnv = process.env
5+
const originalConsoleError = console.error
6+
7+
beforeAll(() => {
8+
process.env = {
9+
...process.env,
10+
get RHTL_DISABLE_ERROR_FILTERING(): string | undefined {
11+
throw new Error('expected')
12+
}
13+
}
14+
require('..')
15+
})
16+
17+
afterAll(() => {
18+
process.env = originalEnv
19+
})
20+
21+
test('should not patch console.error', () => {
22+
expect(console.error).not.toBe(originalConsoleError)
23+
})
24+
})
25+
26+
export {}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { useEffect } from 'react'
2+
3+
import { ReactHooksRenderer } from '../../types/react'
4+
5+
describe('error output suppression tests', () => {
6+
test('should not suppress relevant errors', () => {
7+
const consoleError = console.error
8+
console.error = jest.fn()
9+
10+
const { suppressErrorOutput } = require('..') as ReactHooksRenderer
11+
12+
try {
13+
const restoreConsole = suppressErrorOutput()
14+
15+
console.error('expected')
16+
console.error(new Error('expected'))
17+
console.error('expected with args', new Error('expected'))
18+
19+
restoreConsole()
20+
21+
expect(console.error).toBeCalledWith('expected')
22+
expect(console.error).toBeCalledWith(new Error('expected'))
23+
expect(console.error).toBeCalledWith('expected with args', new Error('expected'))
24+
expect(console.error).toBeCalledTimes(3)
25+
} finally {
26+
console.error = consoleError
27+
}
28+
})
29+
30+
test('should allow console.error to be mocked', async () => {
31+
const { renderHook, act } = require('..') as ReactHooksRenderer
32+
const consoleError = console.error
33+
console.error = jest.fn()
34+
35+
try {
36+
const { rerender, unmount } = renderHook(
37+
(stage) => {
38+
useEffect(() => {
39+
console.error(`expected in effect`)
40+
return () => {
41+
console.error(`expected in unmount`)
42+
}
43+
}, [])
44+
console.error(`expected in ${stage}`)
45+
},
46+
{
47+
initialProps: 'render'
48+
}
49+
)
50+
51+
act(() => {
52+
console.error('expected in act')
53+
})
54+
55+
await act(async () => {
56+
await new Promise((resolve) => setTimeout(resolve, 100))
57+
console.error('expected in async act')
58+
})
59+
60+
rerender('rerender')
61+
62+
unmount()
63+
64+
expect(console.error).toBeCalledWith('expected in render')
65+
expect(console.error).toBeCalledWith('expected in effect')
66+
expect(console.error).toBeCalledWith('expected in act')
67+
expect(console.error).toBeCalledWith('expected in async act')
68+
expect(console.error).toBeCalledWith('expected in rerender')
69+
expect(console.error).toBeCalledWith('expected in unmount')
70+
expect(console.error).toBeCalledTimes(6)
71+
} finally {
72+
console.error = consoleError
73+
}
74+
})
75+
})

‎src/dom/pure.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import ReactDOM from 'react-dom'
1+
import * as ReactDOM from 'react-dom'
22
import { act } from 'react-dom/test-utils'
33

44
import { RendererProps, RendererOptions } from '../types/react'
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { useEffect } from 'react'
2+
3+
import { ReactHooksRenderer } from '../../types/react'
4+
5+
// This verifies that if process.env is unavailable
6+
// then we still auto-wire up the afterEach for folks
7+
describe('skip auto cleanup (no process.env) tests', () => {
8+
const originalEnv = process.env
9+
let cleanupCalled = false
10+
let renderHook: ReactHooksRenderer['renderHook']
11+
12+
beforeAll(() => {
13+
process.env = {
14+
...process.env,
15+
get RHTL_SKIP_AUTO_CLEANUP(): string | undefined {
16+
throw new Error('expected')
17+
}
18+
}
19+
renderHook = (require('..') as ReactHooksRenderer).renderHook
20+
})
21+
22+
afterAll(() => {
23+
process.env = originalEnv
24+
})
25+
26+
test('first', () => {
27+
const hookWithCleanup = () => {
28+
useEffect(() => {
29+
return () => {
30+
cleanupCalled = true
31+
}
32+
})
33+
}
34+
renderHook(() => hookWithCleanup())
35+
})
36+
37+
test('second', () => {
38+
expect(cleanupCalled).toBe(true)
39+
})
40+
})

‎src/native/__tests__/errorHook.test.ts

Lines changed: 0 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -142,51 +142,4 @@ 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-
})
192145
})
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// This verifies that if process.env is unavailable
2+
// then we still auto-wire up the afterEach for folks
3+
describe('error output suppression (no process.env) tests', () => {
4+
const originalEnv = process.env
5+
const originalConsoleError = console.error
6+
7+
beforeAll(() => {
8+
process.env = {
9+
...process.env,
10+
get RHTL_DISABLE_ERROR_FILTERING(): string | undefined {
11+
throw new Error('expected')
12+
}
13+
}
14+
require('..')
15+
})
16+
17+
afterAll(() => {
18+
process.env = originalEnv
19+
})
20+
21+
test('should not patch console.error', () => {
22+
expect(console.error).not.toBe(originalConsoleError)
23+
})
24+
})
25+
26+
export {}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { useEffect } from 'react'
2+
3+
import { ReactHooksRenderer } from '../../types/react'
4+
5+
describe('error output suppression tests', () => {
6+
test('should not suppress relevant errors', () => {
7+
const consoleError = console.error
8+
console.error = jest.fn()
9+
10+
const { suppressErrorOutput } = require('..') as ReactHooksRenderer
11+
12+
try {
13+
const restoreConsole = suppressErrorOutput()
14+
15+
console.error('expected')
16+
console.error(new Error('expected'))
17+
console.error('expected with args', new Error('expected'))
18+
19+
restoreConsole()
20+
21+
expect(console.error).toBeCalledWith('expected')
22+
expect(console.error).toBeCalledWith(new Error('expected'))
23+
expect(console.error).toBeCalledWith('expected with args', new Error('expected'))
24+
expect(console.error).toBeCalledTimes(3)
25+
} finally {
26+
console.error = consoleError
27+
}
28+
})
29+
30+
test('should allow console.error to be mocked', async () => {
31+
const { renderHook, act } = require('..') as ReactHooksRenderer
32+
const consoleError = console.error
33+
console.error = jest.fn()
34+
35+
try {
36+
const { rerender, unmount } = renderHook(
37+
(stage) => {
38+
useEffect(() => {
39+
console.error(`expected in effect`)
40+
return () => {
41+
console.error(`expected in unmount`)
42+
}
43+
}, [])
44+
console.error(`expected in ${stage}`)
45+
},
46+
{
47+
initialProps: 'render'
48+
}
49+
)
50+
51+
act(() => {
52+
console.error('expected in act')
53+
})
54+
55+
await act(async () => {
56+
await new Promise((resolve) => setTimeout(resolve, 100))
57+
console.error('expected in async act')
58+
})
59+
60+
rerender('rerender')
61+
62+
unmount()
63+
64+
expect(console.error).toBeCalledWith('expected in render')
65+
expect(console.error).toBeCalledWith('expected in effect')
66+
expect(console.error).toBeCalledWith('expected in act')
67+
expect(console.error).toBeCalledWith('expected in async act')
68+
expect(console.error).toBeCalledWith('expected in rerender')
69+
expect(console.error).toBeCalledWith('expected in unmount')
70+
expect(console.error).toBeCalledTimes(6)
71+
} finally {
72+
console.error = consoleError
73+
}
74+
})
75+
})

‎src/pure.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ function hasDependency(name: string) {
99
try {
1010
require(name)
1111
return true
12-
} catch (error) {
12+
} catch {
1313
return false
1414
}
1515
}
Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,38 @@
11
import { useEffect } from 'react'
22

3-
import { ReactHooksRenderer } from '../../types/react'
3+
import { ReactHooksServerRenderer } from '../../types/react'
44

55
// This verifies that if RHTL_SKIP_AUTO_CLEANUP is set
66
// then we DON'T auto-wire up the afterEach for folks
77
describe('skip auto cleanup (disabled) tests', () => {
8-
let cleanupCalled = false
9-
let renderHook: ReactHooksRenderer['renderHook']
8+
const cleanups: Record<string, boolean> = {
9+
ssr: false,
10+
hydrated: false
11+
}
12+
let renderHook: ReactHooksServerRenderer['renderHook']
1013

1114
beforeAll(() => {
1215
process.env.RHTL_SKIP_AUTO_CLEANUP = 'true'
13-
renderHook = (require('..') as ReactHooksRenderer).renderHook
16+
renderHook = (require('..') as ReactHooksServerRenderer).renderHook
1417
})
1518

1619
test('first', () => {
17-
const hookWithCleanup = () => {
20+
const hookWithCleanup = (name: string) => {
1821
useEffect(() => {
1922
return () => {
20-
cleanupCalled = true
23+
cleanups[name] = true
2124
}
2225
})
2326
}
24-
renderHook(() => hookWithCleanup())
27+
28+
renderHook(() => hookWithCleanup('ssr'))
29+
30+
const { hydrate } = renderHook(() => hookWithCleanup('hydrated'))
31+
hydrate()
2532
})
2633

2734
test('second', () => {
28-
expect(cleanupCalled).toBe(false)
35+
expect(cleanups.ssr).toBe(false)
36+
expect(cleanups.hydrated).toBe(false)
2937
})
3038
})
Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,39 @@
11
import { useEffect } from 'react'
22

3-
import { ReactHooksRenderer } from '../../types/react'
3+
import { ReactHooksServerRenderer } from '../../types/react'
44

55
// 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', () => {
8-
let cleanupCalled = false
9-
let renderHook: ReactHooksRenderer['renderHook']
8+
const cleanups: Record<string, boolean> = {
9+
ssr: false,
10+
hydrated: false
11+
}
12+
let renderHook: ReactHooksServerRenderer['renderHook']
1013

1114
beforeAll(() => {
1215
// @ts-expect-error Turning off AfterEach -- ignore Jest LifeCycle Type
1316
afterEach = false
14-
renderHook = (require('..') as ReactHooksRenderer).renderHook
17+
renderHook = (require('..') as ReactHooksServerRenderer).renderHook
1518
})
1619

1720
test('first', () => {
18-
const hookWithCleanup = () => {
21+
const hookWithCleanup = (name: string) => {
1922
useEffect(() => {
2023
return () => {
21-
cleanupCalled = true
24+
cleanups[name] = true
2225
}
2326
})
2427
}
25-
renderHook(() => hookWithCleanup())
28+
29+
renderHook(() => hookWithCleanup('ssr'))
30+
31+
const { hydrate } = renderHook(() => hookWithCleanup('hydrated'))
32+
hydrate()
2633
})
2734

2835
test('second', () => {
29-
expect(cleanupCalled).toBe(false)
36+
expect(cleanups.ssr).toBe(false)
37+
expect(cleanups.hydrated).toBe(false)
3038
})
3139
})
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { useEffect } from 'react'
2+
3+
import { ReactHooksServerRenderer } from '../../types/react'
4+
5+
// This verifies that if process.env is unavailable
6+
// then we still auto-wire up the afterEach for folks
7+
describe('skip auto cleanup (no process.env) tests', () => {
8+
const originalEnv = process.env
9+
const cleanups: Record<string, boolean> = {
10+
ssr: false,
11+
hydrated: false
12+
}
13+
let renderHook: ReactHooksServerRenderer['renderHook']
14+
15+
beforeAll(() => {
16+
process.env = {
17+
...process.env,
18+
get RHTL_SKIP_AUTO_CLEANUP(): string | undefined {
19+
throw new Error('expected')
20+
}
21+
}
22+
renderHook = (require('..') as ReactHooksServerRenderer).renderHook
23+
})
24+
25+
afterAll(() => {
26+
process.env = originalEnv
27+
})
28+
29+
test('first', () => {
30+
const hookWithCleanup = (name: string) => {
31+
useEffect(() => {
32+
return () => {
33+
cleanups[name] = true
34+
}
35+
})
36+
}
37+
38+
renderHook(() => hookWithCleanup('ssr'))
39+
40+
const { hydrate } = renderHook(() => hookWithCleanup('hydrated'))
41+
hydrate()
42+
})
43+
44+
test('second', () => {
45+
expect(cleanups.ssr).toBe(false)
46+
expect(cleanups.hydrated).toBe(true)
47+
})
48+
})
Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,37 @@
11
import { useEffect } from 'react'
22

3-
import { ReactHooksRenderer } from '../../types/react'
3+
import { ReactHooksServerRenderer } from '../../types/react'
44

55
// This verifies that if pure imports are used
66
// then we DON'T auto-wire up the afterEach for folks
77
describe('skip auto cleanup (pure) tests', () => {
8-
let cleanupCalled = false
9-
let renderHook: ReactHooksRenderer['renderHook']
8+
const cleanups: Record<string, boolean> = {
9+
ssr: false,
10+
hydrated: false
11+
}
12+
let renderHook: ReactHooksServerRenderer['renderHook']
1013

1114
beforeAll(() => {
12-
renderHook = (require('../pure') as ReactHooksRenderer).renderHook
15+
renderHook = (require('../pure') as ReactHooksServerRenderer).renderHook
1316
})
1417

1518
test('first', () => {
16-
const hookWithCleanup = () => {
19+
const hookWithCleanup = (name: string) => {
1720
useEffect(() => {
1821
return () => {
19-
cleanupCalled = true
22+
cleanups[name] = true
2023
}
2124
})
2225
}
23-
renderHook(() => hookWithCleanup())
26+
27+
renderHook(() => hookWithCleanup('ssr'))
28+
29+
const { hydrate } = renderHook(() => hookWithCleanup('hydrated'))
30+
hydrate()
2431
})
2532

2633
test('second', () => {
27-
expect(cleanupCalled).toBe(false)
34+
expect(cleanups.ssr).toBe(false)
35+
expect(cleanups.hydrated).toBe(false)
2836
})
2937
})

‎src/server/__tests__/errorHook.test.ts

Lines changed: 0 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -163,53 +163,4 @@ 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-
})
215166
})
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// This verifies that if process.env is unavailable
2+
// then we still auto-wire up the afterEach for folks
3+
describe('error output suppression (no process.env) tests', () => {
4+
const originalEnv = process.env
5+
const originalConsoleError = console.error
6+
7+
beforeAll(() => {
8+
process.env = {
9+
...process.env,
10+
get RHTL_DISABLE_ERROR_FILTERING(): string | undefined {
11+
throw new Error('expected')
12+
}
13+
}
14+
require('..')
15+
})
16+
17+
afterAll(() => {
18+
process.env = originalEnv
19+
})
20+
21+
test('should not patch console.error', () => {
22+
expect(console.error).not.toBe(originalConsoleError)
23+
})
24+
})
25+
26+
export {}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { useEffect } from 'react'
2+
3+
import { ReactHooksServerRenderer } from '../../types/react'
4+
5+
describe('error output suppression tests', () => {
6+
test('should not suppress relevant errors', () => {
7+
const consoleError = console.error
8+
console.error = jest.fn()
9+
10+
const { suppressErrorOutput } = require('..') as ReactHooksServerRenderer
11+
12+
try {
13+
const restoreConsole = suppressErrorOutput()
14+
15+
console.error('expected')
16+
console.error(new Error('expected'))
17+
console.error('expected with args', new Error('expected'))
18+
19+
restoreConsole()
20+
21+
expect(console.error).toBeCalledWith('expected')
22+
expect(console.error).toBeCalledWith(new Error('expected'))
23+
expect(console.error).toBeCalledWith('expected with args', new Error('expected'))
24+
expect(console.error).toBeCalledTimes(3)
25+
} finally {
26+
console.error = consoleError
27+
}
28+
})
29+
30+
test('should allow console.error to be mocked', async () => {
31+
const { renderHook, act } = require('..') as ReactHooksServerRenderer
32+
const consoleError = console.error
33+
console.error = jest.fn()
34+
35+
try {
36+
const { hydrate, rerender, unmount } = renderHook(
37+
(stage) => {
38+
useEffect(() => {
39+
console.error(`expected in effect`)
40+
return () => {
41+
console.error(`expected in unmount`)
42+
}
43+
}, [])
44+
console.error(`expected in ${stage}`)
45+
},
46+
{
47+
initialProps: 'render'
48+
}
49+
)
50+
51+
hydrate()
52+
53+
act(() => {
54+
console.error('expected in act')
55+
})
56+
57+
await act(async () => {
58+
await new Promise((resolve) => setTimeout(resolve, 100))
59+
console.error('expected in async act')
60+
})
61+
62+
rerender('rerender')
63+
64+
unmount()
65+
66+
expect(console.error).toBeCalledWith('expected in render') // twice render/hydrate
67+
expect(console.error).toBeCalledWith('expected in effect')
68+
expect(console.error).toBeCalledWith('expected in act')
69+
expect(console.error).toBeCalledWith('expected in async act')
70+
expect(console.error).toBeCalledWith('expected in rerender')
71+
expect(console.error).toBeCalledWith('expected in unmount')
72+
expect(console.error).toBeCalledTimes(7)
73+
} finally {
74+
console.error = consoleError
75+
}
76+
})
77+
})

‎src/server/pure.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import ReactDOMServer from 'react-dom/server'
2-
import ReactDOM from 'react-dom'
1+
import * as ReactDOMServer from 'react-dom/server'
2+
import * as ReactDOM from 'react-dom'
33
import { act } from 'react-dom/test-utils'
44

55
import { RendererOptions, RendererProps } from '../types/react'

‎src/types/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ export type Renderer<TProps> = {
55
act: Act
66
}
77

8+
export type ServerRenderer<TProps> = Renderer<TProps> & {
9+
hydrate: () => void
10+
}
11+
812
export type RendererProps<TProps, TResult> = {
913
callback: (props: TProps) => TResult
1014
setError: (error: Error) => void
@@ -59,6 +63,12 @@ export type RenderHookResult<
5963
Omit<TRenderer, keyof Renderer<TProps>> &
6064
AsyncUtils
6165

66+
export type ServerRenderHookResult<
67+
TProps,
68+
TValue,
69+
TRenderer extends ServerRenderer<TProps> = ServerRenderer<TProps>
70+
> = RenderHookResult<TProps, TValue, TRenderer>
71+
6272
export type RenderHookOptions<TProps> = {
6373
initialProps?: TProps
6474
}

‎src/types/react.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ComponentType } from 'react'
33
import {
44
RenderHookOptions as BaseRenderHookOptions,
55
RenderHookResult,
6+
ServerRenderHookResult,
67
Act,
78
CleanupCallback
89
} from '.'
@@ -29,4 +30,11 @@ export type ReactHooksRenderer = {
2930
suppressErrorOutput: () => () => void
3031
}
3132

33+
export type ReactHooksServerRenderer = Omit<ReactHooksRenderer, 'renderHook'> & {
34+
renderHook: <TProps, TResult>(
35+
callback: (props: TProps) => TResult,
36+
options?: RenderHookOptions<TProps>
37+
) => ServerRenderHookResult<TProps, TResult>
38+
}
39+
3240
export * from '.'

0 commit comments

Comments
 (0)
Please sign in to comment.