/*** Copyright (c) Meta Platforms, Inc. and affiliates.** This source code is licensed under the MIT license found in the* LICENSE file in the root directory of this source tree.** @emails react-core* @jest-environment node*/'use strict';
let React;
let ReactNoop;
let Scheduler;
let waitForAll;
let waitForThrow;
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;
waitForThrow = InternalTestUtils.waitForThrow;
});// Note: in this test file we won't be using toErrorDev() matchers
// because they filter out precisely the messages we want to test for.
let oldConsoleError;
beforeEach(() => {
oldConsoleError = console.error;
console.error = jest.fn();
});afterEach(() => {
console.error = oldConsoleError;
oldConsoleError = 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 />;
}}ReactNoop.render(
<div>
<span>
<ErrorThrowingComponent />
</span>
</div>,
);await waitForThrow('constructor error');
expect(console.error).toHaveBeenCalledTimes(1);
expect(console.error).toHaveBeenCalledWith(
__DEV__
? expect.stringMatching(
new RegExp(
'The above error occurred in the <ErrorThrowingComponent> component:\n' +
'\\s+(in|at) ErrorThrowingComponent (.*)\n' +
'\\s+(in|at) span(.*)\n' +
'\\s+(in|at) div(.*)\n\n' +
'Consider adding an error boundary to your tree ' +
'to customize error handling behavior\\.',
),): expect.objectContaining({
message: 'constructor error',
}),);});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 />;
}}ReactNoop.render(
<div>
<span>
<ErrorThrowingComponent />
</span>
</div>,
);await waitForThrow('componentDidMount error');
expect(console.error).toHaveBeenCalledTimes(1);
expect(console.error).toHaveBeenCalledWith(
__DEV__
? expect.stringMatching(
new RegExp(
'The above error occurred in the <ErrorThrowingComponent> component:\n' +
'\\s+(in|at) ErrorThrowingComponent (.*)\n' +
'\\s+(in|at) span(.*)\n' +
'\\s+(in|at) div(.*)\n\n' +
'Consider adding an error boundary to your tree ' +
'to customize error handling behavior\\.',
),): expect.objectContaining({
message: 'componentDidMount error',
}),);});it('should ignore errors thrown in log method to prevent cycle', async () => {
const logCapturedErrorCalls = [];
console.error.mockImplementation(error => {
// Test what happens when logging itself is buggy.
logCapturedErrorCalls.push(error);
throw new Error('logCapturedError error');
});class ErrorThrowingComponent extends React.Component {
render() {
throw new Error('render error');
}}ReactNoop.render(
<div>
<span>
<ErrorThrowingComponent />
</span>
</div>,
);await waitForThrow('render error');
expect(logCapturedErrorCalls.length).toBe(1);
expect(logCapturedErrorCalls[0]).toEqual(
__DEV__
? expect.stringMatching(
new RegExp(
'The above error occurred in the <ErrorThrowingComponent> component:\n' +
'\\s+(in|at) ErrorThrowingComponent (.*)\n' +
'\\s+(in|at) span(.*)\n' +
'\\s+(in|at) div(.*)\n\n' +
'Consider adding an error boundary to your tree ' +
'to customize error handling behavior\\.',
),): expect.objectContaining({
message: 'render error',
}),);// The error thrown in logCapturedError should be rethrown with a clean stack
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',__DEV__ && 'render: 1', // replay due to invokeGuardedCallback// Retry one more time before handling error'render: 1',__DEV__ && 'render: 1', // replay due to invokeGuardedCallback'componentWillUnmount: 0',].filter(Boolean),
);expect(console.error).toHaveBeenCalledTimes(1);
expect(console.error).toHaveBeenCalledWith(
__DEV__? expect.stringMatching(
new RegExp('The above error occurred in the <Foo> component:\n' +
'\\s+(in|at) Foo (.*)\n' +
'\\s+(in|at) ErrorBoundary (.*)\n\n' +
'React will try to recreate this component tree from scratch ' +
'using the error boundary you provided, ErrorBoundary.',
),): expect.objectContaining({
message: 'oops',}),);});});