diff --git a/disable-error-filtering.js b/disable-error-filtering.js index 25e71c79..cde13e17 100644 --- a/disable-error-filtering.js +++ b/disable-error-filtering.js @@ -1 +1,8 @@ -process.env.RHTL_DISABLE_ERROR_FILTERING = true +try { + process.env.RHTL_DISABLE_ERROR_FILTERING = true +} catch { + // falling back in the case that process.env.RHTL_DISABLE_ERROR_FILTERING cannot be accessed (e.g. browser environment) + console.warn( + 'Could not disable error filtering as process.env.RHTL_DISABLE_ERROR_FILTERING could not be accessed.' + ) +} diff --git a/dont-cleanup-after-each.js b/dont-cleanup-after-each.js index 969e6e2a..4e849555 100644 --- a/dont-cleanup-after-each.js +++ b/dont-cleanup-after-each.js @@ -1 +1,8 @@ -process.env.RHTL_SKIP_AUTO_CLEANUP = true +try { + process.env.RHTL_SKIP_AUTO_CLEANUP = true +} catch { + // falling back in the case that process.env.RHTL_SKIP_AUTO_CLEANUP cannot be accessed (e.g. browser environment) + console.warn( + 'Could not skip auto cleanup as process.env.RHTL_SKIP_AUTO_CLEANUP could not be accessed.' + ) +} diff --git a/package.json b/package.json index d7a127aa..2a889644 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,6 @@ "@types/react": ">=16.9.0", "@types/react-dom": ">=16.9.0", "@types/react-test-renderer": ">=16.9.0", - "filter-console": "^0.1.1", "react-error-boundary": "^3.1.0" }, "devDependencies": { diff --git a/src/core/cleanup.ts b/src/core/cleanup.ts index e521b1fb..42918d2d 100644 --- a/src/core/cleanup.ts +++ b/src/core/cleanup.ts @@ -18,9 +18,18 @@ function removeCleanup(callback: CleanupCallback) { cleanupCallbacks = cleanupCallbacks.filter((cb) => cb !== callback) } +function skipAutoCleanup() { + try { + return !!process.env.RHTL_SKIP_AUTO_CLEANUP + } catch { + // falling back in the case that process.env.RHTL_SKIP_AUTO_CLEANUP cannot be accessed (e.g. browser environment) + return false + } +} + function autoRegisterCleanup() { // Automatically registers cleanup in supported testing frameworks - if (typeof afterEach === 'function' && !process.env.RHTL_SKIP_AUTO_CLEANUP) { + if (typeof afterEach === 'function' && !skipAutoCleanup()) { afterEach(async () => { await cleanup() }) diff --git a/src/core/console.ts b/src/core/console.ts index 579dcf54..681a7a61 100644 --- a/src/core/console.ts +++ b/src/core/console.ts @@ -1,24 +1,41 @@ -import filterConsole from 'filter-console' +const consoleFilters = [ + /^The above error occurred in the <.*?> component:/, // error boundary output + /^Error: Uncaught .+/ // jsdom output +] function suppressErrorOutput() { - if (process.env.RHTL_DISABLE_ERROR_FILTERING) { - return () => {} - } + const originalError = console.error - return filterConsole( - [ - /^The above error occurred in the component:/, // error boundary output - /^Error: Uncaught .+/ // jsdom output - ], - { - methods: ['error'] + const error = (...args: Parameters) => { + const message = typeof args[0] === 'string' ? args[0] : null + if (!message || !consoleFilters.some((filter) => filter.test(message))) { + originalError(...args) } - ) + } + + console.error = error + + return () => { + console.error = originalError + } +} + +function errorFilteringDisabled() { + try { + return !!process.env.RHTL_DISABLE_ERROR_FILTERING + } catch { + // falling back in the case that process.env.RHTL_DISABLE_ERROR_FILTERING cannot be accessed (e.g. browser environment) + return false + } } function enableErrorOutputSuppression() { // Automatically registers console error suppression and restoration in supported testing frameworks - if (typeof beforeEach === 'function' && typeof afterEach === 'function') { + if ( + typeof beforeEach === 'function' && + typeof afterEach === 'function' && + !errorFilteringDisabled() + ) { let restoreConsole!: () => void beforeEach(() => { diff --git a/src/dom/__tests__/autoCleanup.noProcessEnv.test.ts b/src/dom/__tests__/autoCleanup.noProcessEnv.test.ts new file mode 100644 index 00000000..f6adc8ad --- /dev/null +++ b/src/dom/__tests__/autoCleanup.noProcessEnv.test.ts @@ -0,0 +1,40 @@ +import { useEffect } from 'react' + +import { ReactHooksRenderer } from '../../types/react' + +// This verifies that if process.env is unavailable +// then we still auto-wire up the afterEach for folks +describe('skip auto cleanup (no process.env) tests', () => { + const originalEnv = process.env + let cleanupCalled = false + let renderHook: ReactHooksRenderer['renderHook'] + + beforeAll(() => { + process.env = { + ...process.env, + get RHTL_SKIP_AUTO_CLEANUP(): string | undefined { + throw new Error('expected') + } + } + renderHook = (require('..') as ReactHooksRenderer).renderHook + }) + + afterAll(() => { + process.env = originalEnv + }) + + test('first', () => { + const hookWithCleanup = () => { + useEffect(() => { + return () => { + cleanupCalled = true + } + }) + } + renderHook(() => hookWithCleanup()) + }) + + test('second', () => { + expect(cleanupCalled).toBe(true) + }) +}) diff --git a/src/dom/__tests__/errorHook.test.ts b/src/dom/__tests__/errorHook.test.ts index 8b3760b9..6e6c0a38 100644 --- a/src/dom/__tests__/errorHook.test.ts +++ b/src/dom/__tests__/errorHook.test.ts @@ -142,51 +142,4 @@ 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.noProcessEnv.test.ts b/src/dom/__tests__/errorSuppression.noProcessEnv.test.ts new file mode 100644 index 00000000..24a50f21 --- /dev/null +++ b/src/dom/__tests__/errorSuppression.noProcessEnv.test.ts @@ -0,0 +1,26 @@ +// This verifies that if process.env is unavailable +// then we still auto-wire up the afterEach for folks +describe('error output suppression (no process.env) tests', () => { + const originalEnv = process.env + const originalConsoleError = console.error + + beforeAll(() => { + process.env = { + ...process.env, + get RHTL_DISABLE_ERROR_FILTERING(): string | undefined { + throw new Error('expected') + } + } + require('..') + }) + + afterAll(() => { + process.env = originalEnv + }) + + test('should not patch console.error', () => { + expect(console.error).not.toBe(originalConsoleError) + }) +}) + +export {} diff --git a/src/dom/__tests__/errorSuppression.test.ts b/src/dom/__tests__/errorSuppression.test.ts new file mode 100644 index 00000000..69250f47 --- /dev/null +++ b/src/dom/__tests__/errorSuppression.test.ts @@ -0,0 +1,75 @@ +import { useEffect } from 'react' + +import { ReactHooksRenderer } from '../../types/react' + +describe('error output suppression tests', () => { + test('should not suppress relevant errors', () => { + const consoleError = console.error + console.error = jest.fn() + + const { suppressErrorOutput } = require('..') as ReactHooksRenderer + + try { + const restoreConsole = suppressErrorOutput() + + console.error('expected') + console.error(new Error('expected')) + console.error('expected with args', new Error('expected')) + + restoreConsole() + + expect(console.error).toBeCalledWith('expected') + expect(console.error).toBeCalledWith(new Error('expected')) + expect(console.error).toBeCalledWith('expected with args', new Error('expected')) + expect(console.error).toBeCalledTimes(3) + } finally { + console.error = consoleError + } + }) + + test('should allow console.error to be mocked', async () => { + const { renderHook, act } = require('..') as ReactHooksRenderer + 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/pure.ts b/src/dom/pure.ts index 89b13c65..42d66072 100644 --- a/src/dom/pure.ts +++ b/src/dom/pure.ts @@ -1,4 +1,4 @@ -import ReactDOM from 'react-dom' +import * as ReactDOM from 'react-dom' import { act } from 'react-dom/test-utils' import { RendererProps, RendererOptions } from '../types/react' diff --git a/src/native/__tests__/autoCleanup.noProcessEnv.test.ts b/src/native/__tests__/autoCleanup.noProcessEnv.test.ts new file mode 100644 index 00000000..f6adc8ad --- /dev/null +++ b/src/native/__tests__/autoCleanup.noProcessEnv.test.ts @@ -0,0 +1,40 @@ +import { useEffect } from 'react' + +import { ReactHooksRenderer } from '../../types/react' + +// This verifies that if process.env is unavailable +// then we still auto-wire up the afterEach for folks +describe('skip auto cleanup (no process.env) tests', () => { + const originalEnv = process.env + let cleanupCalled = false + let renderHook: ReactHooksRenderer['renderHook'] + + beforeAll(() => { + process.env = { + ...process.env, + get RHTL_SKIP_AUTO_CLEANUP(): string | undefined { + throw new Error('expected') + } + } + renderHook = (require('..') as ReactHooksRenderer).renderHook + }) + + afterAll(() => { + process.env = originalEnv + }) + + test('first', () => { + const hookWithCleanup = () => { + useEffect(() => { + return () => { + cleanupCalled = true + } + }) + } + renderHook(() => hookWithCleanup()) + }) + + test('second', () => { + expect(cleanupCalled).toBe(true) + }) +}) diff --git a/src/native/__tests__/errorHook.test.ts b/src/native/__tests__/errorHook.test.ts index 69e54270..8399a50b 100644 --- a/src/native/__tests__/errorHook.test.ts +++ b/src/native/__tests__/errorHook.test.ts @@ -142,51 +142,4 @@ 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.noProcessEnv.test.ts b/src/native/__tests__/errorSuppression.noProcessEnv.test.ts new file mode 100644 index 00000000..24a50f21 --- /dev/null +++ b/src/native/__tests__/errorSuppression.noProcessEnv.test.ts @@ -0,0 +1,26 @@ +// This verifies that if process.env is unavailable +// then we still auto-wire up the afterEach for folks +describe('error output suppression (no process.env) tests', () => { + const originalEnv = process.env + const originalConsoleError = console.error + + beforeAll(() => { + process.env = { + ...process.env, + get RHTL_DISABLE_ERROR_FILTERING(): string | undefined { + throw new Error('expected') + } + } + require('..') + }) + + afterAll(() => { + process.env = originalEnv + }) + + test('should not patch console.error', () => { + expect(console.error).not.toBe(originalConsoleError) + }) +}) + +export {} diff --git a/src/native/__tests__/errorSuppression.test.ts b/src/native/__tests__/errorSuppression.test.ts new file mode 100644 index 00000000..69250f47 --- /dev/null +++ b/src/native/__tests__/errorSuppression.test.ts @@ -0,0 +1,75 @@ +import { useEffect } from 'react' + +import { ReactHooksRenderer } from '../../types/react' + +describe('error output suppression tests', () => { + test('should not suppress relevant errors', () => { + const consoleError = console.error + console.error = jest.fn() + + const { suppressErrorOutput } = require('..') as ReactHooksRenderer + + try { + const restoreConsole = suppressErrorOutput() + + console.error('expected') + console.error(new Error('expected')) + console.error('expected with args', new Error('expected')) + + restoreConsole() + + expect(console.error).toBeCalledWith('expected') + expect(console.error).toBeCalledWith(new Error('expected')) + expect(console.error).toBeCalledWith('expected with args', new Error('expected')) + expect(console.error).toBeCalledTimes(3) + } finally { + console.error = consoleError + } + }) + + test('should allow console.error to be mocked', async () => { + const { renderHook, act } = require('..') as ReactHooksRenderer + 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/pure.ts b/src/pure.ts index 33da7895..ec72c6fe 100644 --- a/src/pure.ts +++ b/src/pure.ts @@ -9,7 +9,7 @@ function hasDependency(name: string) { try { require(name) return true - } catch (error) { + } catch { return false } } diff --git a/src/server/__tests__/autoCleanup.disabled.test.ts b/src/server/__tests__/autoCleanup.disabled.test.ts index 2c574b83..a39e4e72 100644 --- a/src/server/__tests__/autoCleanup.disabled.test.ts +++ b/src/server/__tests__/autoCleanup.disabled.test.ts @@ -1,30 +1,38 @@ import { useEffect } from 'react' -import { ReactHooksRenderer } from '../../types/react' +import { ReactHooksServerRenderer } from '../../types/react' // This verifies that if RHTL_SKIP_AUTO_CLEANUP is set // then we DON'T auto-wire up the afterEach for folks describe('skip auto cleanup (disabled) tests', () => { - let cleanupCalled = false - let renderHook: ReactHooksRenderer['renderHook'] + const cleanups: Record = { + ssr: false, + hydrated: false + } + let renderHook: ReactHooksServerRenderer['renderHook'] beforeAll(() => { process.env.RHTL_SKIP_AUTO_CLEANUP = 'true' - renderHook = (require('..') as ReactHooksRenderer).renderHook + renderHook = (require('..') as ReactHooksServerRenderer).renderHook }) test('first', () => { - const hookWithCleanup = () => { + const hookWithCleanup = (name: string) => { useEffect(() => { return () => { - cleanupCalled = true + cleanups[name] = true } }) } - renderHook(() => hookWithCleanup()) + + renderHook(() => hookWithCleanup('ssr')) + + const { hydrate } = renderHook(() => hookWithCleanup('hydrated')) + hydrate() }) test('second', () => { - expect(cleanupCalled).toBe(false) + expect(cleanups.ssr).toBe(false) + expect(cleanups.hydrated).toBe(false) }) }) diff --git a/src/server/__tests__/autoCleanup.noAfterEach.test.ts b/src/server/__tests__/autoCleanup.noAfterEach.test.ts index 40b33f16..6468296b 100644 --- a/src/server/__tests__/autoCleanup.noAfterEach.test.ts +++ b/src/server/__tests__/autoCleanup.noAfterEach.test.ts @@ -1,31 +1,39 @@ import { useEffect } from 'react' -import { ReactHooksRenderer } from '../../types/react' +import { ReactHooksServerRenderer } from '../../types/react' // 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: ReactHooksRenderer['renderHook'] + const cleanups: Record = { + ssr: false, + hydrated: false + } + let renderHook: ReactHooksServerRenderer['renderHook'] beforeAll(() => { // @ts-expect-error Turning off AfterEach -- ignore Jest LifeCycle Type afterEach = false - renderHook = (require('..') as ReactHooksRenderer).renderHook + renderHook = (require('..') as ReactHooksServerRenderer).renderHook }) test('first', () => { - const hookWithCleanup = () => { + const hookWithCleanup = (name: string) => { useEffect(() => { return () => { - cleanupCalled = true + cleanups[name] = true } }) } - renderHook(() => hookWithCleanup()) + + renderHook(() => hookWithCleanup('ssr')) + + const { hydrate } = renderHook(() => hookWithCleanup('hydrated')) + hydrate() }) test('second', () => { - expect(cleanupCalled).toBe(false) + expect(cleanups.ssr).toBe(false) + expect(cleanups.hydrated).toBe(false) }) }) diff --git a/src/server/__tests__/autoCleanup.noProcessEnv.test.ts b/src/server/__tests__/autoCleanup.noProcessEnv.test.ts new file mode 100644 index 00000000..f734d73e --- /dev/null +++ b/src/server/__tests__/autoCleanup.noProcessEnv.test.ts @@ -0,0 +1,48 @@ +import { useEffect } from 'react' + +import { ReactHooksServerRenderer } from '../../types/react' + +// This verifies that if process.env is unavailable +// then we still auto-wire up the afterEach for folks +describe('skip auto cleanup (no process.env) tests', () => { + const originalEnv = process.env + const cleanups: Record = { + ssr: false, + hydrated: false + } + let renderHook: ReactHooksServerRenderer['renderHook'] + + beforeAll(() => { + process.env = { + ...process.env, + get RHTL_SKIP_AUTO_CLEANUP(): string | undefined { + throw new Error('expected') + } + } + renderHook = (require('..') as ReactHooksServerRenderer).renderHook + }) + + afterAll(() => { + process.env = originalEnv + }) + + test('first', () => { + const hookWithCleanup = (name: string) => { + useEffect(() => { + return () => { + cleanups[name] = true + } + }) + } + + renderHook(() => hookWithCleanup('ssr')) + + const { hydrate } = renderHook(() => hookWithCleanup('hydrated')) + hydrate() + }) + + test('second', () => { + expect(cleanups.ssr).toBe(false) + expect(cleanups.hydrated).toBe(true) + }) +}) diff --git a/src/server/__tests__/autoCleanup.pure.test.ts b/src/server/__tests__/autoCleanup.pure.test.ts index 1f84b87c..0044e17f 100644 --- a/src/server/__tests__/autoCleanup.pure.test.ts +++ b/src/server/__tests__/autoCleanup.pure.test.ts @@ -1,29 +1,37 @@ import { useEffect } from 'react' -import { ReactHooksRenderer } from '../../types/react' +import { ReactHooksServerRenderer } 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'] + const cleanups: Record = { + ssr: false, + hydrated: false + } + let renderHook: ReactHooksServerRenderer['renderHook'] beforeAll(() => { - renderHook = (require('../pure') as ReactHooksRenderer).renderHook + renderHook = (require('../pure') as ReactHooksServerRenderer).renderHook }) test('first', () => { - const hookWithCleanup = () => { + const hookWithCleanup = (name: string) => { useEffect(() => { return () => { - cleanupCalled = true + cleanups[name] = true } }) } - renderHook(() => hookWithCleanup()) + + renderHook(() => hookWithCleanup('ssr')) + + const { hydrate } = renderHook(() => hookWithCleanup('hydrated')) + hydrate() }) test('second', () => { - expect(cleanupCalled).toBe(false) + expect(cleanups.ssr).toBe(false) + expect(cleanups.hydrated).toBe(false) }) }) diff --git a/src/server/__tests__/errorHook.test.ts b/src/server/__tests__/errorHook.test.ts index f3ce0442..75925a98 100644 --- a/src/server/__tests__/errorHook.test.ts +++ b/src/server/__tests__/errorHook.test.ts @@ -163,53 +163,4 @@ 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.noProcessEnv.test.ts b/src/server/__tests__/errorSuppression.noProcessEnv.test.ts new file mode 100644 index 00000000..24a50f21 --- /dev/null +++ b/src/server/__tests__/errorSuppression.noProcessEnv.test.ts @@ -0,0 +1,26 @@ +// This verifies that if process.env is unavailable +// then we still auto-wire up the afterEach for folks +describe('error output suppression (no process.env) tests', () => { + const originalEnv = process.env + const originalConsoleError = console.error + + beforeAll(() => { + process.env = { + ...process.env, + get RHTL_DISABLE_ERROR_FILTERING(): string | undefined { + throw new Error('expected') + } + } + require('..') + }) + + afterAll(() => { + process.env = originalEnv + }) + + test('should not patch console.error', () => { + expect(console.error).not.toBe(originalConsoleError) + }) +}) + +export {} diff --git a/src/server/__tests__/errorSuppression.test.ts b/src/server/__tests__/errorSuppression.test.ts new file mode 100644 index 00000000..e4492756 --- /dev/null +++ b/src/server/__tests__/errorSuppression.test.ts @@ -0,0 +1,77 @@ +import { useEffect } from 'react' + +import { ReactHooksServerRenderer } from '../../types/react' + +describe('error output suppression tests', () => { + test('should not suppress relevant errors', () => { + const consoleError = console.error + console.error = jest.fn() + + const { suppressErrorOutput } = require('..') as ReactHooksServerRenderer + + try { + const restoreConsole = suppressErrorOutput() + + console.error('expected') + console.error(new Error('expected')) + console.error('expected with args', new Error('expected')) + + restoreConsole() + + expect(console.error).toBeCalledWith('expected') + expect(console.error).toBeCalledWith(new Error('expected')) + expect(console.error).toBeCalledWith('expected with args', new Error('expected')) + expect(console.error).toBeCalledTimes(3) + } finally { + console.error = consoleError + } + }) + + test('should allow console.error to be mocked', async () => { + const { renderHook, act } = require('..') as ReactHooksServerRenderer + 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/pure.ts b/src/server/pure.ts index b37b6df5..1978f2d0 100644 --- a/src/server/pure.ts +++ b/src/server/pure.ts @@ -1,5 +1,5 @@ -import ReactDOMServer from 'react-dom/server' -import ReactDOM from 'react-dom' +import * as ReactDOMServer from 'react-dom/server' +import * as ReactDOM from 'react-dom' import { act } from 'react-dom/test-utils' import { RendererOptions, RendererProps } from '../types/react' diff --git a/src/types/index.ts b/src/types/index.ts index fa0035f5..994e024e 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -5,6 +5,10 @@ export type Renderer = { act: Act } +export type ServerRenderer = Renderer & { + hydrate: () => void +} + export type RendererProps = { callback: (props: TProps) => TResult setError: (error: Error) => void @@ -59,6 +63,12 @@ export type RenderHookResult< Omit> & AsyncUtils +export type ServerRenderHookResult< + TProps, + TValue, + TRenderer extends ServerRenderer = ServerRenderer +> = RenderHookResult + export type RenderHookOptions = { initialProps?: TProps } diff --git a/src/types/react.ts b/src/types/react.ts index 348eb5af..d7091776 100644 --- a/src/types/react.ts +++ b/src/types/react.ts @@ -3,6 +3,7 @@ import { ComponentType } from 'react' import { RenderHookOptions as BaseRenderHookOptions, RenderHookResult, + ServerRenderHookResult, Act, CleanupCallback } from '.' @@ -29,4 +30,11 @@ export type ReactHooksRenderer = { suppressErrorOutput: () => () => void } +export type ReactHooksServerRenderer = Omit & { + renderHook: ( + callback: (props: TProps) => TResult, + options?: RenderHookOptions + ) => ServerRenderHookResult +} + export * from '.'