Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: only suppress console.error for non-pure imports #549

Merged
merged 12 commits into from
Jan 22, 2021
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
52 changes: 49 additions & 3 deletions 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)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just reading what this list of links is for, I don't this this one should be added here after all (it will appear in the side menu still though).


---

Expand Down Expand Up @@ -153,9 +154,8 @@ 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
[Rendering](/installation#being-specific).
Alternatively, you can change your test to import from `@testing-library/react-hooks/pure` (or any
of the [other non-pure imports](/installation#pure-imports)) instead of the regular imports.

```diff
- import { renderHook, cleanup, act } from '@testing-library/react-hooks'
Expand Down Expand Up @@ -270,3 +270,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 importing from
`@testing-library/react-hooks` (or any of the [other non-pure imports](/installation#pure-imports))
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` (or any
of the [other non-pure imports](/installation#pure-imports)) instead of the regular imports.

```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.
28 changes: 26 additions & 2 deletions docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ npm install --save-dev @testing-library/react-hooks
yarn add --dev @testing-library/react-hooks
```

### Peer Dependencies
### Peer dependencies

`react-hooks-testing-library` does not come bundled with a version of
[`react`](https://www.npmjs.com/package/react) to allow you to install the specific version you want
Expand Down Expand Up @@ -92,7 +92,31 @@ import { renderHook, act } from '@testing-library/react-hooks/native' // will us
import { renderHook, act } from '@testing-library/react-hooks/server' // will use react-dom/server
```

## Testing Framework
## Pure imports

Importing from any of the previously mentioned imports will cause some side effects in the test
environment:

1. `cleanup` is automatically called in an `afterEach` block
2. `console.error` is patched to hide some React errors

The specifics of these side effects are discussed in more detail in the
[API reference](/reference/api).

If you want to ensure the imports are free of side-effects, you can use the `pure` imports instead,
which can be accessed by appending `/pure` to the end of any of the other imports:

```ts
import { renderHook, act } from '@testing-library/react-hooks/pure'

import { renderHook, act } from '@testing-library/react-hooks/dom/pure'

import { renderHook, act } from '@testing-library/react-hooks/native/pure'

import { renderHook, act } from '@testing-library/react-hooks/server/pure'
```

## Testing framework

In order to run tests, you will probably want to be using a test framework. If you have not already
got one, we recommend using [Jest](https://jestjs.io/), but this library should work without issues
Expand Down
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
}
})
})
})
14 changes: 14 additions & 0 deletions src/dom/__tests__/errorSuppression.disabled.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
describe('error output suppression (disabled) tests', () => {
const originalConsoleError = console.error

beforeAll(() => {
process.env.RHTL_DISABLE_ERROR_FILTERING = 'true'
})

test('should not patch console.error', () => {
require('..')
expect(console.error).toBe(originalConsoleError)
})
})

export {}
24 changes: 24 additions & 0 deletions src/dom/__tests__/errorSuppression.noAfterAll.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
describe('error output suppression (noAfterAll) tests', () => {
const originalConsoleError = console.error

beforeAll(() => {
// @ts-expect-error Turning off AfterAll -- ignore Jest LifeCycle Type
// eslint-disable-next-line no-global-assign
afterAll = false
})

describe('first', () => {
test('should patch console.error', () => {
require('..')
expect(console.error).not.toBe(originalConsoleError)
})
})

describe('second', () => {
test('should still used patched console.error', () => {
expect(console.error).not.toBe(originalConsoleError)
})
})
})

export {}
10 changes: 10 additions & 0 deletions src/dom/__tests__/errorSuppression.pure.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
describe('error output suppression (pure) tests', () => {
const originalConsoleError = console.error

test('should not patch console.error', () => {
require('../pure')
expect(console.error).toBe(originalConsoleError)
})
})

export {}
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
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
}
})
})
})