'use strict';
let React;
let ReactNoop;
let Scheduler;
let waitForAll;
let uncaughtExceptionMock;
async function fakeAct(cb) {
await cb();
Scheduler.unstable_flushAll();
}
describe('ReactIncrementalErrorLogging', () => {
beforeEach(() => {
jest.resetModules();
React = require('react');
ReactNoop = require('react-noop-renderer');
Scheduler = require('scheduler');
const InternalTestUtils = require('internal-test-utils');
waitForAll = InternalTestUtils.waitForAll;
});
let oldConsoleWarn;
let oldConsoleError;
beforeEach(() => {
oldConsoleWarn = console.warn;
oldConsoleError = console.error;
console.warn = jest.fn();
console.error = jest.fn();
uncaughtExceptionMock = jest.fn();
process.on('uncaughtException', uncaughtExceptionMock);
});
afterEach(() => {
console.warn = oldConsoleWarn;
console.error = oldConsoleError;
process.off('uncaughtException', uncaughtExceptionMock);
oldConsoleWarn = null;
oldConsoleError = null;
uncaughtExceptionMock = null;
});
it('should log errors that occur during the begin phase', async () => {
class ErrorThrowingComponent extends React.Component {
constructor(props) {
super(props);
throw new Error('constructor error');
}
render() {
return <div />;
}
}
await fakeAct(() => {
ReactNoop.render(
<div>
<span>
<ErrorThrowingComponent />
</span>
</div>,
);
});
expect(uncaughtExceptionMock).toHaveBeenCalledTimes(1);
expect(uncaughtExceptionMock).toHaveBeenCalledWith(
expect.objectContaining({
message: 'constructor error',
}),
);
if (__DEV__) {
expect(console.warn).toHaveBeenCalledTimes(1);
expect(console.warn).toHaveBeenCalledWith(
expect.stringContaining('%s'),
expect.stringContaining(
'An error occurred in the <ErrorThrowingComponent> component.',
),
expect.stringContaining(
'Consider adding an error boundary to your tree ' +
'to customize error handling behavior.',
),
);
}
});
it('should log errors that occur during the commit phase', async () => {
class ErrorThrowingComponent extends React.Component {
componentDidMount() {
throw new Error('componentDidMount error');
}
render() {
return <div />;
}
}
await fakeAct(() => {
ReactNoop.render(
<div>
<span>
<ErrorThrowingComponent />
</span>
</div>,
);
});
expect(uncaughtExceptionMock).toHaveBeenCalledTimes(1);
expect(uncaughtExceptionMock).toHaveBeenCalledWith(
expect.objectContaining({
message: 'componentDidMount error',
}),
);
if (__DEV__) {
expect(console.warn).toHaveBeenCalledTimes(1);
expect(console.warn).toHaveBeenCalledWith(
expect.stringContaining('%s'),
expect.stringContaining(
'An error occurred in the <ErrorThrowingComponent> component.',
),
expect.stringContaining(
'Consider adding an error boundary to your tree ' +
'to customize error handling behavior.',
),
);
}
});
it('should ignore errors thrown in log method to prevent cycle', async () => {
const logCapturedErrorCalls = [];
console.error.mockImplementation(error => {
logCapturedErrorCalls.push(error);
throw new Error('logCapturedError error');
});
class ErrorBoundary extends React.Component {
state = {error: null};
componentDidCatch(error) {
this.setState({error});
}
render() {
return this.state.error ? null : this.props.children;
}
}
class ErrorThrowingComponent extends React.Component {
render() {
throw new Error('render error');
}
}
await fakeAct(() => {
ReactNoop.render(
<div>
<ErrorBoundary>
<span>
<ErrorThrowingComponent />
</span>
</ErrorBoundary>
</div>,
);
});
expect(logCapturedErrorCalls.length).toBe(1);
if (__DEV__) {
expect(console.error).toHaveBeenCalledWith(
expect.stringContaining('%o'),
expect.objectContaining({
message: 'render error',
}),
expect.stringContaining(
'The above error occurred in the <ErrorThrowingComponent> component.',
),
expect.stringContaining(
'React will try to recreate this component tree from scratch ' +
'using the error boundary you provided, ErrorBoundary.',
),
);
} else {
expect(logCapturedErrorCalls[0]).toEqual(
expect.objectContaining({
message: 'render error',
}),
);
}
expect(() => {
jest.runAllTimers();
}).toThrow('logCapturedError error');
});
it('resets instance variables before unmounting failed node', async () => {
class ErrorBoundary extends React.Component {
state = {error: null};
componentDidCatch(error) {
this.setState({error});
}
render() {
return this.state.error ? null : this.props.children;
}
}
class Foo extends React.Component {
state = {step: 0};
componentDidMount() {
this.setState({step: 1});
}
componentWillUnmount() {
Scheduler.log('componentWillUnmount: ' + this.state.step);
}
render() {
Scheduler.log('render: ' + this.state.step);
if (this.state.step > 0) {
throw new Error('oops');
}
return null;
}
}
ReactNoop.render(
<ErrorBoundary>
<Foo />
</ErrorBoundary>,
);
await waitForAll(
[
'render: 0',
'render: 1',
'render: 1',
'componentWillUnmount: 0',
].filter(Boolean),
);
expect(console.error).toHaveBeenCalledTimes(1);
if (__DEV__) {
expect(console.error).toHaveBeenCalledWith(
expect.stringContaining('%o'),
expect.objectContaining({
message: 'oops',
}),
expect.stringContaining(
'The above error occurred in the <Foo> component.',
),
expect.stringContaining(
'React will try to recreate this component tree from scratch ' +
'using the error boundary you provided, ErrorBoundary.',
),
);
} else {
expect(console.error).toHaveBeenCalledWith(
expect.objectContaining({
message: 'oops',
}),
);
}
});
});