'use strict';
let PropTypes;
let React;
let ReactDOM;
let ReactDOMClient;
let act;
let Scheduler;
describe('ReactErrorBoundaries', () => {
let BrokenConstructor;
let BrokenComponentWillMount;
let BrokenComponentDidMount;
let BrokenComponentWillReceiveProps;
let BrokenComponentWillUpdate;
let BrokenComponentDidUpdate;
let BrokenComponentWillUnmount;
let BrokenRenderErrorBoundary;
let BrokenComponentWillMountErrorBoundary;
let BrokenComponentDidMountErrorBoundary;
let BrokenRender;
let BrokenUseEffect;
let BrokenUseLayoutEffect;
let ErrorBoundary;
let ErrorMessage;
let NoopErrorBoundary;
let RetryErrorBoundary;
let Normal;
let assertLog;
let assertConsoleErrorDev;
beforeEach(() => {
jest.useFakeTimers();
jest.resetModules();
PropTypes = require('prop-types');
ReactDOM = require('react-dom');
ReactDOMClient = require('react-dom/client');
React = require('react');
act = require('internal-test-utils').act;
Scheduler = require('scheduler');
({assertLog, assertConsoleErrorDev} = require('internal-test-utils'));
BrokenConstructor = class extends React.Component {
constructor(props) {
super(props);
Scheduler.log('BrokenConstructor constructor [!]');
throw new Error('Hello');
}
render() {
Scheduler.log('BrokenConstructor render');
return <div>{this.props.children}</div>;
}
UNSAFE_componentWillMount() {
Scheduler.log('BrokenConstructor componentWillMount');
}
componentDidMount() {
Scheduler.log('BrokenConstructor componentDidMount');
}
UNSAFE_componentWillReceiveProps() {
Scheduler.log('BrokenConstructor componentWillReceiveProps');
}
UNSAFE_componentWillUpdate() {
Scheduler.log('BrokenConstructor componentWillUpdate');
}
componentDidUpdate() {
Scheduler.log('BrokenConstructor componentDidUpdate');
}
componentWillUnmount() {
Scheduler.log('BrokenConstructor componentWillUnmount');
}
};
BrokenComponentWillMount = class extends React.Component {
constructor(props) {
super(props);
Scheduler.log('BrokenComponentWillMount constructor');
}
render() {
Scheduler.log('BrokenComponentWillMount render');
return <div>{this.props.children}</div>;
}
UNSAFE_componentWillMount() {
Scheduler.log('BrokenComponentWillMount componentWillMount [!]');
throw new Error('Hello');
}
componentDidMount() {
Scheduler.log('BrokenComponentWillMount componentDidMount');
}
UNSAFE_componentWillReceiveProps() {
Scheduler.log('BrokenComponentWillMount componentWillReceiveProps');
}
UNSAFE_componentWillUpdate() {
Scheduler.log('BrokenComponentWillMount componentWillUpdate');
}
componentDidUpdate() {
Scheduler.log('BrokenComponentWillMount componentDidUpdate');
}
componentWillUnmount() {
Scheduler.log('BrokenComponentWillMount componentWillUnmount');
}
};
BrokenComponentDidMount = class extends React.Component {
constructor(props) {
super(props);
Scheduler.log('BrokenComponentDidMount constructor');
}
render() {
Scheduler.log('BrokenComponentDidMount render');
return <div>{this.props.children}</div>;
}
UNSAFE_componentWillMount() {
Scheduler.log('BrokenComponentDidMount componentWillMount');
}
componentDidMount() {
Scheduler.log('BrokenComponentDidMount componentDidMount [!]');
throw new Error('Hello');
}
UNSAFE_componentWillReceiveProps() {
Scheduler.log('BrokenComponentDidMount componentWillReceiveProps');
}
UNSAFE_componentWillUpdate() {
Scheduler.log('BrokenComponentDidMount componentWillUpdate');
}
componentDidUpdate() {
Scheduler.log('BrokenComponentDidMount componentDidUpdate');
}
componentWillUnmount() {
Scheduler.log('BrokenComponentDidMount componentWillUnmount');
}
};
BrokenComponentWillReceiveProps = class extends React.Component {
constructor(props) {
super(props);
Scheduler.log('BrokenComponentWillReceiveProps constructor');
}
render() {
Scheduler.log('BrokenComponentWillReceiveProps render');
return <div>{this.props.children}</div>;
}
UNSAFE_componentWillMount() {
Scheduler.log('BrokenComponentWillReceiveProps componentWillMount');
}
componentDidMount() {
Scheduler.log('BrokenComponentWillReceiveProps componentDidMount');
}
UNSAFE_componentWillReceiveProps() {
Scheduler.log(
'BrokenComponentWillReceiveProps componentWillReceiveProps [!]',
);
throw new Error('Hello');
}
UNSAFE_componentWillUpdate() {
Scheduler.log('BrokenComponentWillReceiveProps componentWillUpdate');
}
componentDidUpdate() {
Scheduler.log('BrokenComponentWillReceiveProps componentDidUpdate');
}
componentWillUnmount() {
Scheduler.log('BrokenComponentWillReceiveProps componentWillUnmount');
}
};
BrokenComponentWillUpdate = class extends React.Component {
constructor(props) {
super(props);
Scheduler.log('BrokenComponentWillUpdate constructor');
}
render() {
Scheduler.log('BrokenComponentWillUpdate render');
return <div>{this.props.children}</div>;
}
UNSAFE_componentWillMount() {
Scheduler.log('BrokenComponentWillUpdate componentWillMount');
}
componentDidMount() {
Scheduler.log('BrokenComponentWillUpdate componentDidMount');
}
UNSAFE_componentWillReceiveProps() {
Scheduler.log('BrokenComponentWillUpdate componentWillReceiveProps');
}
UNSAFE_componentWillUpdate() {
Scheduler.log('BrokenComponentWillUpdate componentWillUpdate [!]');
throw new Error('Hello');
}
componentDidUpdate() {
Scheduler.log('BrokenComponentWillUpdate componentDidUpdate');
}
componentWillUnmount() {
Scheduler.log('BrokenComponentWillUpdate componentWillUnmount');
}
};
BrokenComponentDidUpdate = class extends React.Component {
static defaultProps = {
errorText: 'Hello',
};
constructor(props) {
super(props);
Scheduler.log('BrokenComponentDidUpdate constructor');
}
render() {
Scheduler.log('BrokenComponentDidUpdate render');
return <div>{this.props.children}</div>;
}
UNSAFE_componentWillMount() {
Scheduler.log('BrokenComponentDidUpdate componentWillMount');
}
componentDidMount() {
Scheduler.log('BrokenComponentDidUpdate componentDidMount');
}
UNSAFE_componentWillReceiveProps() {
Scheduler.log('BrokenComponentDidUpdate componentWillReceiveProps');
}
UNSAFE_componentWillUpdate() {
Scheduler.log('BrokenComponentDidUpdate componentWillUpdate');
}
componentDidUpdate() {
Scheduler.log('BrokenComponentDidUpdate componentDidUpdate [!]');
throw new Error(this.props.errorText);
}
componentWillUnmount() {
Scheduler.log('BrokenComponentDidUpdate componentWillUnmount');
}
};
BrokenComponentWillUnmount = class extends React.Component {
static defaultProps = {
errorText: 'Hello',
};
constructor(props) {
super(props);
Scheduler.log('BrokenComponentWillUnmount constructor');
}
render() {
Scheduler.log('BrokenComponentWillUnmount render');
return <div>{this.props.children}</div>;
}
UNSAFE_componentWillMount() {
Scheduler.log('BrokenComponentWillUnmount componentWillMount');
}
componentDidMount() {
Scheduler.log('BrokenComponentWillUnmount componentDidMount');
}
UNSAFE_componentWillReceiveProps() {
Scheduler.log('BrokenComponentWillUnmount componentWillReceiveProps');
}
UNSAFE_componentWillUpdate() {
Scheduler.log('BrokenComponentWillUnmount componentWillUpdate');
}
componentDidUpdate() {
Scheduler.log('BrokenComponentWillUnmount componentDidUpdate');
}
componentWillUnmount() {
Scheduler.log('BrokenComponentWillUnmount componentWillUnmount [!]');
throw new Error(this.props.errorText);
}
};
BrokenComponentWillMountErrorBoundary = class extends React.Component {
constructor(props) {
super(props);
this.state = {error: null};
Scheduler.log('BrokenComponentWillMountErrorBoundary constructor');
}
render() {
if (this.state.error) {
Scheduler.log('BrokenComponentWillMountErrorBoundary render error');
return <div>Caught an error: {this.state.error.message}.</div>;
}
Scheduler.log('BrokenComponentWillMountErrorBoundary render success');
return <div>{this.props.children}</div>;
}
UNSAFE_componentWillMount() {
Scheduler.log(
'BrokenComponentWillMountErrorBoundary componentWillMount [!]',
);
throw new Error('Hello');
}
componentDidMount() {
Scheduler.log(
'BrokenComponentWillMountErrorBoundary componentDidMount',
);
}
componentWillUnmount() {
Scheduler.log(
'BrokenComponentWillMountErrorBoundary componentWillUnmount',
);
}
static getDerivedStateFromError(error) {
Scheduler.log(
'BrokenComponentWillMountErrorBoundary static getDerivedStateFromError',
);
return {error};
}
};
BrokenComponentDidMountErrorBoundary = class extends React.Component {
constructor(props) {
super(props);
this.state = {error: null};
Scheduler.log('BrokenComponentDidMountErrorBoundary constructor');
}
render() {
if (this.state.error) {
Scheduler.log('BrokenComponentDidMountErrorBoundary render error');
return <div>Caught an error: {this.state.error.message}.</div>;
}
Scheduler.log('BrokenComponentDidMountErrorBoundary render success');
return <div>{this.props.children}</div>;
}
UNSAFE_componentWillMount() {
Scheduler.log(
'BrokenComponentDidMountErrorBoundary componentWillMount',
);
}
componentDidMount() {
Scheduler.log(
'BrokenComponentDidMountErrorBoundary componentDidMount [!]',
);
throw new Error('Hello');
}
componentWillUnmount() {
Scheduler.log(
'BrokenComponentDidMountErrorBoundary componentWillUnmount',
);
}
static getDerivedStateFromError(error) {
Scheduler.log(
'BrokenComponentDidMountErrorBoundary static getDerivedStateFromError',
);
return {error};
}
};
BrokenRenderErrorBoundary = class extends React.Component {
constructor(props) {
super(props);
this.state = {error: null};
Scheduler.log('BrokenRenderErrorBoundary constructor');
}
render() {
if (this.state.error) {
Scheduler.log('BrokenRenderErrorBoundary render error [!]');
throw new Error('Hello');
}
Scheduler.log('BrokenRenderErrorBoundary render success');
return <div>{this.props.children}</div>;
}
UNSAFE_componentWillMount() {
Scheduler.log('BrokenRenderErrorBoundary componentWillMount');
}
componentDidMount() {
Scheduler.log('BrokenRenderErrorBoundary componentDidMount');
}
componentWillUnmount() {
Scheduler.log('BrokenRenderErrorBoundary componentWillUnmount');
}
static getDerivedStateFromError(error) {
Scheduler.log(
'BrokenRenderErrorBoundary static getDerivedStateFromError',
);
return {error};
}
};
BrokenRender = class extends React.Component {
constructor(props) {
super(props);
Scheduler.log('BrokenRender constructor');
}
render() {
Scheduler.log('BrokenRender render [!]');
throw new Error('Hello');
}
UNSAFE_componentWillMount() {
Scheduler.log('BrokenRender componentWillMount');
}
componentDidMount() {
Scheduler.log('BrokenRender componentDidMount');
}
UNSAFE_componentWillReceiveProps() {
Scheduler.log('BrokenRender componentWillReceiveProps');
}
UNSAFE_componentWillUpdate() {
Scheduler.log('BrokenRender componentWillUpdate');
}
componentDidUpdate() {
Scheduler.log('BrokenRender componentDidUpdate');
}
componentWillUnmount() {
Scheduler.log('BrokenRender componentWillUnmount');
}
};
BrokenUseEffect = ({children}) => {
Scheduler.log('BrokenUseEffect render');
React.useEffect(() => {
Scheduler.log('BrokenUseEffect useEffect [!]');
throw new Error('Hello');
});
return children;
};
BrokenUseLayoutEffect = ({children}) => {
Scheduler.log('BrokenUseLayoutEffect render');
React.useLayoutEffect(() => {
Scheduler.log('BrokenUseLayoutEffect useLayoutEffect [!]');
throw new Error('Hello');
});
return children;
};
NoopErrorBoundary = class extends React.Component {
constructor(props) {
super(props);
Scheduler.log('NoopErrorBoundary constructor');
}
render() {
Scheduler.log('NoopErrorBoundary render');
return <BrokenRender />;
}
UNSAFE_componentWillMount() {
Scheduler.log('NoopErrorBoundary componentWillMount');
}
componentDidMount() {
Scheduler.log('NoopErrorBoundary componentDidMount');
}
componentWillUnmount() {
Scheduler.log('NoopErrorBoundary componentWillUnmount');
}
static getDerivedStateFromError() {
Scheduler.log('NoopErrorBoundary static getDerivedStateFromError');
}
};
Normal = class extends React.Component {
static defaultProps = {
logName: 'Normal',
};
constructor(props) {
super(props);
Scheduler.log(`${this.props.logName} constructor`);
}
render() {
Scheduler.log(`${this.props.logName} render`);
return <div>{this.props.children}</div>;
}
UNSAFE_componentWillMount() {
Scheduler.log(`${this.props.logName} componentWillMount`);
}
componentDidMount() {
Scheduler.log(`${this.props.logName} componentDidMount`);
}
UNSAFE_componentWillReceiveProps() {
Scheduler.log(`${this.props.logName} componentWillReceiveProps`);
}
UNSAFE_componentWillUpdate() {
Scheduler.log(`${this.props.logName} componentWillUpdate`);
}
componentDidUpdate() {
Scheduler.log(`${this.props.logName} componentDidUpdate`);
}
componentWillUnmount() {
Scheduler.log(`${this.props.logName} componentWillUnmount`);
}
};
ErrorBoundary = class extends React.Component {
constructor(props) {
super(props);
this.state = {error: null};
Scheduler.log(`${this.props.logName} constructor`);
}
render() {
if (this.state.error && !this.props.forceRetry) {
Scheduler.log(`${this.props.logName} render error`);
return this.props.renderError(this.state.error, this.props);
}
Scheduler.log(`${this.props.logName} render success`);
return <div>{this.props.children}</div>;
}
static getDerivedStateFromError(error) {
Scheduler.log('ErrorBoundary static getDerivedStateFromError');
return {error};
}
UNSAFE_componentWillMount() {
Scheduler.log(`${this.props.logName} componentWillMount`);
}
componentDidMount() {
Scheduler.log(`${this.props.logName} componentDidMount`);
}
UNSAFE_componentWillReceiveProps() {
Scheduler.log(`${this.props.logName} componentWillReceiveProps`);
}
UNSAFE_componentWillUpdate() {
Scheduler.log(`${this.props.logName} componentWillUpdate`);
}
componentDidUpdate() {
Scheduler.log(`${this.props.logName} componentDidUpdate`);
}
componentWillUnmount() {
Scheduler.log(`${this.props.logName} componentWillUnmount`);
}
};
ErrorBoundary.defaultProps = {
logName: 'ErrorBoundary',
renderError(error, props) {
return (
<div ref={props.errorMessageRef}>
Caught an error: {error.message}.
</div>
);
},
};
RetryErrorBoundary = class extends React.Component {
constructor(props) {
super(props);
Scheduler.log('RetryErrorBoundary constructor');
}
render() {
Scheduler.log('RetryErrorBoundary render');
return <BrokenRender />;
}
UNSAFE_componentWillMount() {
Scheduler.log('RetryErrorBoundary componentWillMount');
}
componentDidMount() {
Scheduler.log('RetryErrorBoundary componentDidMount');
}
componentWillUnmount() {
Scheduler.log('RetryErrorBoundary componentWillUnmount');
}
static getDerivedStateFromError(error) {
Scheduler.log('RetryErrorBoundary static getDerivedStateFromError [!]');
return {};
}
};
ErrorMessage = class extends React.Component {
constructor(props) {
super(props);
Scheduler.log('ErrorMessage constructor');
}
UNSAFE_componentWillMount() {
Scheduler.log('ErrorMessage componentWillMount');
}
componentDidMount() {
Scheduler.log('ErrorMessage componentDidMount');
}
componentWillUnmount() {
Scheduler.log('ErrorMessage componentWillUnmount');
}
render() {
Scheduler.log('ErrorMessage render');
return <div>Caught an error: {this.props.message}.</div>;
}
};
});
it('does not swallow exceptions on mounting without boundaries', async () => {
let container = document.createElement('div');
let root = ReactDOMClient.createRoot(container);
await expect(async () => {
await act(async () => {
root.render(<BrokenRender />);
});
}).rejects.toThrow('Hello');
Scheduler.unstable_clearLog();
container = document.createElement('div');
root = ReactDOMClient.createRoot(container);
await expect(async () => {
await act(async () => {
root.render(<BrokenComponentWillMount />);
});
}).rejects.toThrow('Hello');
Scheduler.unstable_clearLog();
container = document.createElement('div');
root = ReactDOMClient.createRoot(container);
await expect(async () => {
await act(async () => {
root.render(<BrokenComponentDidMount />);
});
}).rejects.toThrow('Hello');
});
it('does not swallow exceptions on updating without boundaries', async () => {
let container = document.createElement('div');
let root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(<BrokenComponentWillUpdate />);
});
Scheduler.unstable_clearLog();
await expect(async () => {
await act(async () => {
root.render(<BrokenComponentWillUpdate />);
});
}).rejects.toThrow('Hello');
Scheduler.unstable_clearLog();
container = document.createElement('div');
root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(<BrokenComponentWillReceiveProps />);
});
Scheduler.unstable_clearLog();
await expect(async () => {
await act(async () => {
root.render(<BrokenComponentWillReceiveProps />);
});
}).rejects.toThrow('Hello');
Scheduler.unstable_clearLog();
container = document.createElement('div');
root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(<BrokenComponentDidUpdate />);
});
Scheduler.unstable_clearLog();
await expect(async () => {
await act(async () => {
root.render(<BrokenComponentDidUpdate />);
});
}).rejects.toThrow('Hello');
});
it('does not swallow exceptions on unmounting without boundaries', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(<BrokenComponentWillUnmount />);
});
Scheduler.unstable_clearLog();
await expect(async () => {
await act(() => root.unmount());
}).rejects.toThrow('Hello');
});
it('prevents errors from leaking into other roots', async () => {
const container1 = document.createElement('div');
const root1 = ReactDOMClient.createRoot(container1);
const container2 = document.createElement('div');
const root2 = ReactDOMClient.createRoot(container2);
const container3 = document.createElement('div');
const root3 = ReactDOMClient.createRoot(container3);
await act(async () => {
root1.render(<span>Before 1</span>);
});
await expect(async () => {
await act(async () => {
root2.render(<BrokenRender />);
});
}).rejects.toThrow('Hello');
assertLog([
'BrokenRender constructor',
'BrokenRender componentWillMount',
'BrokenRender render [!]',
'BrokenRender constructor',
'BrokenRender componentWillMount',
'BrokenRender render [!]',
]);
await act(async () => {
root3.render(
<ErrorBoundary>
<BrokenRender />
</ErrorBoundary>,
);
});
expect(container1.firstChild.textContent).toBe('Before 1');
expect(container2.firstChild).toBe(null);
expect(container3.firstChild.textContent).toBe('Caught an error: Hello.');
Scheduler.unstable_clearLog();
await act(async () => {
root1.render(<span>After 1</span>);
});
Scheduler.unstable_clearLog();
await act(async () => {
root2.render(<span>After 2</span>);
});
Scheduler.unstable_clearLog();
await act(async () => {
root3.render(<ErrorBoundary forceRetry={true}>After 3</ErrorBoundary>);
});
expect(container1.firstChild.textContent).toBe('After 1');
expect(container2.firstChild.textContent).toBe('After 2');
expect(container3.firstChild.textContent).toBe('After 3');
root1.unmount();
root2.unmount();
root3.unmount();
expect(container1.firstChild).toBe(null);
expect(container2.firstChild).toBe(null);
expect(container3.firstChild).toBe(null);
});
it('logs a single error when using error boundary', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
spyOnDev(console, 'error');
await act(async () => {
root.render(
<ErrorBoundary>
<BrokenRender />
</ErrorBoundary>,
);
});
if (__DEV__) {
expect(console.error).toHaveBeenCalledTimes(1);
expect(console.error.mock.calls[0][2]).toContain(
'The above error occurred in the <BrokenRender> component',
);
}
expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
assertLog([
'ErrorBoundary constructor',
'ErrorBoundary componentWillMount',
'ErrorBoundary render success',
'BrokenRender constructor',
'BrokenRender componentWillMount',
'BrokenRender render [!]',
'ErrorBoundary static getDerivedStateFromError',
'ErrorBoundary componentWillMount',
'ErrorBoundary render error',
'ErrorBoundary constructor',
'ErrorBoundary componentWillMount',
'ErrorBoundary render success',
'BrokenRender constructor',
'BrokenRender componentWillMount',
'BrokenRender render [!]',
'ErrorBoundary static getDerivedStateFromError',
'ErrorBoundary componentWillMount',
'ErrorBoundary render error',
'ErrorBoundary componentDidMount',
]);
root.unmount();
assertLog(['ErrorBoundary componentWillUnmount']);
});
it('renders an error state if child throws in render', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(
<ErrorBoundary>
<BrokenRender />
</ErrorBoundary>,
);
});
expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
assertLog([
'ErrorBoundary constructor',
'ErrorBoundary componentWillMount',
'ErrorBoundary render success',
'BrokenRender constructor',
'BrokenRender componentWillMount',
'BrokenRender render [!]',
'ErrorBoundary static getDerivedStateFromError',
'ErrorBoundary componentWillMount',
'ErrorBoundary render error',
'ErrorBoundary constructor',
'ErrorBoundary componentWillMount',
'ErrorBoundary render success',
'BrokenRender constructor',
'BrokenRender componentWillMount',
'BrokenRender render [!]',
'ErrorBoundary static getDerivedStateFromError',
'ErrorBoundary componentWillMount',
'ErrorBoundary render error',
'ErrorBoundary componentDidMount',
]);
root.unmount();
assertLog(['ErrorBoundary componentWillUnmount']);
});
it('renders an error state if child throws in constructor', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(
<ErrorBoundary>
<BrokenConstructor />
</ErrorBoundary>,
);
});
expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
assertLog([
'ErrorBoundary constructor',
'ErrorBoundary componentWillMount',
'ErrorBoundary render success',
'BrokenConstructor constructor [!]',
'ErrorBoundary static getDerivedStateFromError',
'ErrorBoundary componentWillMount',
'ErrorBoundary render error',
'ErrorBoundary constructor',
'ErrorBoundary componentWillMount',
'ErrorBoundary render success',
'BrokenConstructor constructor [!]',
'ErrorBoundary static getDerivedStateFromError',
'ErrorBoundary componentWillMount',
'ErrorBoundary render error',
'ErrorBoundary componentDidMount',
]);
root.unmount();
assertLog(['ErrorBoundary componentWillUnmount']);
});
it('renders an error state if child throws in componentWillMount', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(
<ErrorBoundary>
<BrokenComponentWillMount />
</ErrorBoundary>,
);
});
expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
assertLog([
'ErrorBoundary constructor',
'ErrorBoundary componentWillMount',
'ErrorBoundary render success',
'BrokenComponentWillMount constructor',
'BrokenComponentWillMount componentWillMount [!]',
'ErrorBoundary static getDerivedStateFromError',
'ErrorBoundary componentWillMount',
'ErrorBoundary render error',
'ErrorBoundary constructor',
'ErrorBoundary componentWillMount',
'ErrorBoundary render success',
'BrokenComponentWillMount constructor',
'BrokenComponentWillMount componentWillMount [!]',
'ErrorBoundary static getDerivedStateFromError',
'ErrorBoundary componentWillMount',
'ErrorBoundary render error',
'ErrorBoundary componentDidMount',
]);
root.unmount();
assertLog(['ErrorBoundary componentWillUnmount']);
});
it('renders an error state if context provider throws in componentWillMount', async () => {
class BrokenComponentWillMountWithContext extends React.Component {
static childContextTypes = {foo: PropTypes.number};
getChildContext() {
return {foo: 42};
}
render() {
return <div>{this.props.children}</div>;
}
UNSAFE_componentWillMount() {
throw new Error('Hello');
}
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(
<ErrorBoundary>
<BrokenComponentWillMountWithContext />
</ErrorBoundary>,
);
});
assertConsoleErrorDev([
'BrokenComponentWillMountWithContext uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
]);
expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
});
it('mounts the error message if mounting fails', async () => {
function renderError(error) {
return <ErrorMessage message={error.message} />;
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(
<ErrorBoundary renderError={renderError}>
<BrokenRender />
</ErrorBoundary>,
);
});
assertLog([
'ErrorBoundary constructor',
'ErrorBoundary componentWillMount',
'ErrorBoundary render success',
'BrokenRender constructor',
'BrokenRender componentWillMount',
'BrokenRender render [!]',
'ErrorBoundary static getDerivedStateFromError',
'ErrorBoundary componentWillMount',
'ErrorBoundary render error',
'ErrorMessage constructor',
'ErrorMessage componentWillMount',
'ErrorMessage render',
'ErrorBoundary constructor',
'ErrorBoundary componentWillMount',
'ErrorBoundary render success',
'BrokenRender constructor',
'BrokenRender componentWillMount',
'BrokenRender render [!]',
'ErrorBoundary static getDerivedStateFromError',
'ErrorBoundary componentWillMount',
'ErrorBoundary render error',
'ErrorMessage constructor',
'ErrorMessage componentWillMount',
'ErrorMessage render',
'ErrorMessage componentDidMount',
'ErrorBoundary componentDidMount',
]);
root.unmount();
assertLog([
'ErrorBoundary componentWillUnmount',
'ErrorMessage componentWillUnmount',
]);
});
it('propagates errors on retry on mounting', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(
<ErrorBoundary>
<RetryErrorBoundary>
<BrokenRender />
</RetryErrorBoundary>
</ErrorBoundary>,
);
});
expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
assertLog([
'ErrorBoundary constructor',
'ErrorBoundary componentWillMount',
'ErrorBoundary render success',
'RetryErrorBoundary constructor',
'RetryErrorBoundary componentWillMount',
'RetryErrorBoundary render',
'BrokenRender constructor',
'BrokenRender componentWillMount',
'BrokenRender render [!]',
'RetryErrorBoundary static getDerivedStateFromError [!]',
'RetryErrorBoundary componentWillMount',
'RetryErrorBoundary render',
'BrokenRender constructor',
'BrokenRender componentWillMount',
'BrokenRender render [!]',
'ErrorBoundary static getDerivedStateFromError',
'ErrorBoundary componentWillMount',
'ErrorBoundary render error',
'ErrorBoundary constructor',
'ErrorBoundary componentWillMount',
'ErrorBoundary render success',
'RetryErrorBoundary constructor',
'RetryErrorBoundary componentWillMount',
'RetryErrorBoundary render',
'BrokenRender constructor',
'BrokenRender componentWillMount',
'BrokenRender render [!]',
'RetryErrorBoundary static getDerivedStateFromError [!]',
'RetryErrorBoundary componentWillMount',
'RetryErrorBoundary render',
'BrokenRender constructor',
'BrokenRender componentWillMount',
'BrokenRender render [!]',
'ErrorBoundary static getDerivedStateFromError',
'ErrorBoundary componentWillMount',
'ErrorBoundary render error',
'ErrorBoundary componentDidMount',
]);
root.unmount();
assertLog(['ErrorBoundary componentWillUnmount']);
});
it('propagates errors inside boundary during componentWillMount', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(
<ErrorBoundary>
<BrokenComponentWillMountErrorBoundary />
</ErrorBoundary>,
);
});
expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
assertLog([
'ErrorBoundary constructor',
'ErrorBoundary componentWillMount',
'ErrorBoundary render success',
'BrokenComponentWillMountErrorBoundary constructor',
'BrokenComponentWillMountErrorBoundary componentWillMount [!]',
'ErrorBoundary static getDerivedStateFromError',
'ErrorBoundary componentWillMount',
'ErrorBoundary render error',
'ErrorBoundary constructor',
'ErrorBoundary componentWillMount',
'ErrorBoundary render success',
'BrokenComponentWillMountErrorBoundary constructor',
'BrokenComponentWillMountErrorBoundary componentWillMount [!]',
'ErrorBoundary static getDerivedStateFromError',
'ErrorBoundary componentWillMount',
'ErrorBoundary render error',
'ErrorBoundary componentDidMount',
]);
root.unmount();
assertLog(['ErrorBoundary componentWillUnmount']);
});
it('propagates errors inside boundary while rendering error state', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(
<ErrorBoundary>
<BrokenRenderErrorBoundary>
<BrokenRender />
</BrokenRenderErrorBoundary>
</ErrorBoundary>,
);
});
expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
assertLog([
'ErrorBoundary constructor',
'ErrorBoundary componentWillMount',
'ErrorBoundary render success',
'BrokenRenderErrorBoundary constructor',
'BrokenRenderErrorBoundary componentWillMount',
'BrokenRenderErrorBoundary render success',
'BrokenRender constructor',
'BrokenRender componentWillMount',
'BrokenRender render [!]',
'BrokenRenderErrorBoundary static getDerivedStateFromError',
'BrokenRenderErrorBoundary componentWillMount',
'BrokenRenderErrorBoundary render error [!]',
'ErrorBoundary static getDerivedStateFromError',
'ErrorBoundary componentWillMount',
'ErrorBoundary render error',
'ErrorBoundary constructor',
'ErrorBoundary componentWillMount',
'ErrorBoundary render success',
'BrokenRenderErrorBoundary constructor',
'BrokenRenderErrorBoundary componentWillMount',
'BrokenRenderErrorBoundary render success',
'BrokenRender constructor',
'BrokenRender componentWillMount',
'BrokenRender render [!]',
'BrokenRenderErrorBoundary static getDerivedStateFromError',
'BrokenRenderErrorBoundary componentWillMount',
'BrokenRenderErrorBoundary render error [!]',
'ErrorBoundary static getDerivedStateFromError',
'ErrorBoundary componentWillMount',
'ErrorBoundary render error',
'ErrorBoundary componentDidMount',
]);
root.unmount();
assertLog(['ErrorBoundary componentWillUnmount']);
});
it('does not call componentWillUnmount when aborting initial mount', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(
<ErrorBoundary>
<Normal />
<BrokenRender />
<Normal />
</ErrorBoundary>,
);
});
expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
assertLog([
'ErrorBoundary constructor',
'ErrorBoundary componentWillMount',
'ErrorBoundary render success',
'Normal constructor',
'Normal componentWillMount',
'Normal render',
'BrokenRender constructor',
'BrokenRender componentWillMount',
'BrokenRender render [!]',
'ErrorBoundary static getDerivedStateFromError',
'ErrorBoundary componentWillMount',
'ErrorBoundary render error',
'ErrorBoundary constructor',
'ErrorBoundary componentWillMount',
'ErrorBoundary render success',
'Normal constructor',
'Normal componentWillMount',
'Normal render',
'BrokenRender constructor',
'BrokenRender componentWillMount',
'BrokenRender render [!]',
'ErrorBoundary static getDerivedStateFromError',
'ErrorBoundary componentWillMount',
'ErrorBoundary render error',
'ErrorBoundary componentDidMount',
]);
root.unmount();
assertLog(['ErrorBoundary componentWillUnmount']);
});
it('resets callback refs if mounting aborts', async () => {
function childRef(x) {
Scheduler.log('Child ref is set to ' + x);
}
function errorMessageRef(x) {
Scheduler.log('Error message ref is set to ' + x);
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(
<ErrorBoundary errorMessageRef={errorMessageRef}>
<div ref={childRef} />
<BrokenRender />
</ErrorBoundary>,
);
});
expect(container.textContent).toBe('Caught an error: Hello.');
assertLog([
'ErrorBoundary constructor',
'ErrorBoundary componentWillMount',
'ErrorBoundary render success',
'BrokenRender constructor',
'BrokenRender componentWillMount',
'BrokenRender render [!]',
'ErrorBoundary static getDerivedStateFromError',
'ErrorBoundary componentWillMount',
'ErrorBoundary render error',
'ErrorBoundary constructor',
'ErrorBoundary componentWillMount',
'ErrorBoundary render success',
'BrokenRender constructor',
'BrokenRender componentWillMount',
'BrokenRender render [!]',
'ErrorBoundary static getDerivedStateFromError',
'ErrorBoundary componentWillMount',
'ErrorBoundary render error',
'Error message ref is set to [object HTMLDivElement]',
'ErrorBoundary componentDidMount',
]);
root.unmount();
assertLog([
'ErrorBoundary componentWillUnmount',
'Error message ref is set to null',
]);
});
it('resets object refs if mounting aborts', async () => {
const childRef = React.createRef();
const errorMessageRef = React.createRef();
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(
<ErrorBoundary errorMessageRef={errorMessageRef}>
<div ref={childRef} />
<BrokenRender />
</ErrorBoundary>,
);
});
expect(container.textContent).toBe('Caught an error: Hello.');
assertLog([
'ErrorBoundary constructor',
'ErrorBoundary componentWillMount',
'ErrorBoundary render success',
'BrokenRender constructor',
'BrokenRender componentWillMount',
'BrokenRender render [!]',
'ErrorBoundary static getDerivedStateFromError',
'ErrorBoundary componentWillMount',
'ErrorBoundary render error',
'ErrorBoundary constructor',
'ErrorBoundary componentWillMount',
'ErrorBoundary render success',
'BrokenRender constructor',
'BrokenRender componentWillMount',
'BrokenRender render [!]',
'ErrorBoundary static getDerivedStateFromError',
'ErrorBoundary componentWillMount',
'ErrorBoundary render error',
'ErrorBoundary componentDidMount',
]);
expect(errorMessageRef.current.toString()).toEqual(
'[object HTMLDivElement]',
);
root.unmount();
assertLog(['ErrorBoundary componentWillUnmount']);
expect(errorMessageRef.current).toEqual(null);
});
it('successfully mounts if no error occurs', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(
<ErrorBoundary>
<div>Mounted successfully.</div>
</ErrorBoundary>,
);
});
expect(container.firstChild.textContent).toBe('Mounted successfully.');
assertLog([
'ErrorBoundary constructor',
'ErrorBoundary componentWillMount',
'ErrorBoundary render success',
'ErrorBoundary componentDidMount',
]);
root.unmount();
assertLog(['ErrorBoundary componentWillUnmount']);
});
it('catches if child throws in constructor during update', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(
<ErrorBoundary>
<Normal />
</ErrorBoundary>,
);
});
Scheduler.unstable_clearLog();
await act(async () => {
root.render(
<ErrorBoundary>
<Normal />
<Normal logName="Normal2" />
<BrokenConstructor />
</ErrorBoundary>,
);
});
expect(container.textContent).toBe('Caught an error: Hello.');
assertLog([
'ErrorBoundary componentWillReceiveProps',
'ErrorBoundary componentWillUpdate',
'ErrorBoundary render success',
'Normal componentWillReceiveProps',
'Normal componentWillUpdate',
'Normal render',
'Normal2 constructor',
'Normal2 componentWillMount',
'Normal2 render',
'BrokenConstructor constructor [!]',
'ErrorBoundary static getDerivedStateFromError',
'ErrorBoundary componentWillUpdate',
'ErrorBoundary render error',
'ErrorBoundary componentWillReceiveProps',
'ErrorBoundary componentWillUpdate',
'ErrorBoundary render success',
'Normal componentWillReceiveProps',
'Normal componentWillUpdate',
'Normal render',
'Normal2 constructor',
'Normal2 componentWillMount',
'Normal2 render',
'BrokenConstructor constructor [!]',
'ErrorBoundary static getDerivedStateFromError',
'ErrorBoundary componentWillUpdate',
'ErrorBoundary render error',
'Normal componentWillUnmount',
'ErrorBoundary componentDidUpdate',
]);
root.unmount();
assertLog(['ErrorBoundary componentWillUnmount']);
});
it('catches if child throws in componentWillMount during update', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(
<ErrorBoundary>
<Normal />
</ErrorBoundary>,
);
});
Scheduler.unstable_clearLog();
await act(async () => {
root.render(
<ErrorBoundary>
<Normal />
<Normal logName="Normal2" />
<BrokenComponentWillMount />
</ErrorBoundary>,
);
});
expect(container.textContent).toBe('Caught an error: Hello.');
assertLog([
'ErrorBoundary componentWillReceiveProps',
'ErrorBoundary componentWillUpdate',
'ErrorBoundary render success',
'Normal componentWillReceiveProps',
'Normal componentWillUpdate',
'Normal render',
'Normal2 constructor',
'Normal2 componentWillMount',
'Normal2 render',
'BrokenComponentWillMount constructor',
'BrokenComponentWillMount componentWillMount [!]',
'ErrorBoundary static getDerivedStateFromError',
'ErrorBoundary componentWillUpdate',
'ErrorBoundary render error',
'ErrorBoundary componentWillReceiveProps',
'ErrorBoundary componentWillUpdate',
'ErrorBoundary render success',
'Normal componentWillReceiveProps',
'Normal componentWillUpdate',
'Normal render',
'Normal2 constructor',
'Normal2 componentWillMount',
'Normal2 render',
'BrokenComponentWillMount constructor',
'BrokenComponentWillMount componentWillMount [!]',
'ErrorBoundary static getDerivedStateFromError',
'ErrorBoundary componentWillUpdate',
'ErrorBoundary render error',
'Normal componentWillUnmount',
'ErrorBoundary componentDidUpdate',
]);
root.unmount();
assertLog(['ErrorBoundary componentWillUnmount']);
});
it('catches if child throws in componentWillReceiveProps during update', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(
<ErrorBoundary>
<Normal />
<BrokenComponentWillReceiveProps />
</ErrorBoundary>,
);
});
Scheduler.unstable_clearLog();
await act(async () => {
root.render(
<ErrorBoundary>
<Normal />
<BrokenComponentWillReceiveProps />
</ErrorBoundary>,
);
});
expect(container.textContent).toBe('Caught an error: Hello.');
assertLog([
'ErrorBoundary componentWillReceiveProps',
'ErrorBoundary componentWillUpdate',
'ErrorBoundary render success',
'Normal componentWillReceiveProps',
'Normal componentWillUpdate',
'Normal render',
'BrokenComponentWillReceiveProps componentWillReceiveProps [!]',
'ErrorBoundary static getDerivedStateFromError',
'ErrorBoundary componentWillUpdate',
'ErrorBoundary render error',
'ErrorBoundary componentWillReceiveProps',
'ErrorBoundary componentWillUpdate',
'ErrorBoundary render success',
'Normal componentWillReceiveProps',
'Normal componentWillUpdate',
'Normal render',
'BrokenComponentWillReceiveProps componentWillReceiveProps [!]',
'ErrorBoundary static getDerivedStateFromError',
'ErrorBoundary componentWillUpdate',
'ErrorBoundary render error',
'Normal componentWillUnmount',
'BrokenComponentWillReceiveProps componentWillUnmount',
'ErrorBoundary componentDidUpdate',
]);
root.unmount();
assertLog(['ErrorBoundary componentWillUnmount']);
});
it('catches if child throws in componentWillUpdate during update', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(
<ErrorBoundary>
<Normal />
<BrokenComponentWillUpdate />
</ErrorBoundary>,
);
});
Scheduler.unstable_clearLog();
await act(async () => {
root.render(
<ErrorBoundary>
<Normal />
<BrokenComponentWillUpdate />
</ErrorBoundary>,
);
});
expect(container.textContent).toBe('Caught an error: Hello.');
assertLog([
'ErrorBoundary componentWillReceiveProps',
'ErrorBoundary componentWillUpdate',
'ErrorBoundary render success',
'Normal componentWillReceiveProps',
'Normal componentWillUpdate',
'Normal render',
'BrokenComponentWillUpdate componentWillReceiveProps',
'BrokenComponentWillUpdate componentWillUpdate [!]',
'ErrorBoundary static getDerivedStateFromError',
'ErrorBoundary componentWillUpdate',
'ErrorBoundary render error',
'ErrorBoundary componentWillReceiveProps',
'ErrorBoundary componentWillUpdate',
'ErrorBoundary render success',
'Normal componentWillReceiveProps',
'Normal componentWillUpdate',
'Normal render',
'BrokenComponentWillUpdate componentWillReceiveProps',
'BrokenComponentWillUpdate componentWillUpdate [!]',
'ErrorBoundary static getDerivedStateFromError',
'ErrorBoundary componentWillUpdate',
'ErrorBoundary render error',
'Normal componentWillUnmount',
'BrokenComponentWillUpdate componentWillUnmount',
'ErrorBoundary componentDidUpdate',
]);
root.unmount();
assertLog(['ErrorBoundary componentWillUnmount']);
});
it('catches if child throws in render during update', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(
<ErrorBoundary>
<Normal />
</ErrorBoundary>,
);
});
Scheduler.unstable_clearLog();
await act(async () => {
root.render(
<ErrorBoundary>
<Normal />
<Normal logName="Normal2" />
<BrokenRender />
</ErrorBoundary>,
);
});
expect(container.textContent).toBe('Caught an error: Hello.');
assertLog([
'ErrorBoundary componentWillReceiveProps',
'ErrorBoundary componentWillUpdate',
'ErrorBoundary render success',
'Normal componentWillReceiveProps',
'Normal componentWillUpdate',
'Normal render',
'Normal2 constructor',
'Normal2 componentWillMount',
'Normal2 render',
'BrokenRender constructor',
'BrokenRender componentWillMount',
'BrokenRender render [!]',
'ErrorBoundary static getDerivedStateFromError',
'ErrorBoundary componentWillUpdate',
'ErrorBoundary render error',
'ErrorBoundary componentWillReceiveProps',
'ErrorBoundary componentWillUpdate',
'ErrorBoundary render success',
'Normal componentWillReceiveProps',
'Normal componentWillUpdate',
'Normal render',
'Normal2 constructor',
'Normal2 componentWillMount',
'Normal2 render',
'BrokenRender constructor',
'BrokenRender componentWillMount',
'BrokenRender render [!]',
'ErrorBoundary static getDerivedStateFromError',
'ErrorBoundary componentWillUpdate',
'ErrorBoundary render error',
'Normal componentWillUnmount',
'ErrorBoundary componentDidUpdate',
]);
root.unmount();
assertLog(['ErrorBoundary componentWillUnmount']);
});
it('keeps refs up-to-date during updates', async () => {
function child1Ref(x) {
Scheduler.log('Child1 ref is set to ' + x);
}
function child2Ref(x) {
Scheduler.log('Child2 ref is set to ' + x);
}
function errorMessageRef(x) {
Scheduler.log('Error message ref is set to ' + x);
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(
<ErrorBoundary errorMessageRef={errorMessageRef}>
<div ref={child1Ref} />
</ErrorBoundary>,
);
});
assertLog([
'ErrorBoundary constructor',
'ErrorBoundary componentWillMount',
'ErrorBoundary render success',
'Child1 ref is set to [object HTMLDivElement]',
'ErrorBoundary componentDidMount',
]);
await act(async () => {
root.render(
<ErrorBoundary errorMessageRef={errorMessageRef}>
<div ref={child1Ref} />
<div ref={child2Ref} />
<BrokenRender />
</ErrorBoundary>,
);
});
expect(container.textContent).toBe('Caught an error: Hello.');
assertLog([
'ErrorBoundary componentWillReceiveProps',
'ErrorBoundary componentWillUpdate',
'ErrorBoundary render success',
'BrokenRender constructor',
'BrokenRender componentWillMount',
'BrokenRender render [!]',
'ErrorBoundary static getDerivedStateFromError',
'ErrorBoundary componentWillUpdate',
'ErrorBoundary render error',
'ErrorBoundary componentWillReceiveProps',
'ErrorBoundary componentWillUpdate',
'ErrorBoundary render success',
'BrokenRender constructor',
'BrokenRender componentWillMount',
'BrokenRender render [!]',
'ErrorBoundary static getDerivedStateFromError',
'ErrorBoundary componentWillUpdate',
'ErrorBoundary render error',
'Child1 ref is set to null',
'Error message ref is set to [object HTMLDivElement]',
'ErrorBoundary componentDidUpdate',
]);
root.unmount();
assertLog([
'ErrorBoundary componentWillUnmount',
'Error message ref is set to null',
]);
});
it('recovers from componentWillUnmount errors on update', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(
<ErrorBoundary>
<BrokenComponentWillUnmount />
<BrokenComponentWillUnmount />
<Normal />
</ErrorBoundary>,
);
});
Scheduler.unstable_clearLog();
await act(async () => {
root.render(
<ErrorBoundary>
<BrokenComponentWillUnmount />
</ErrorBoundary>,
);
});
expect(container.textContent).toBe('Caught an error: Hello.');
assertLog([
'ErrorBoundary componentWillReceiveProps',
'ErrorBoundary componentWillUpdate',
'ErrorBoundary render success',
'BrokenComponentWillUnmount componentWillReceiveProps',
'BrokenComponentWillUnmount componentWillUpdate',
'BrokenComponentWillUnmount render',
'BrokenComponentWillUnmount componentWillUnmount [!]',
'Normal componentWillUnmount',
'BrokenComponentWillUnmount componentDidUpdate',
'ErrorBoundary componentDidUpdate',
'ErrorBoundary static getDerivedStateFromError',
'ErrorBoundary componentWillUpdate',
'ErrorBoundary render error',
'BrokenComponentWillUnmount componentWillUnmount [!]',
'ErrorBoundary componentDidUpdate',
'ErrorBoundary static getDerivedStateFromError',
'ErrorBoundary componentWillUpdate',
'ErrorBoundary render error',
'ErrorBoundary componentDidUpdate',
]);
root.unmount();
assertLog(['ErrorBoundary componentWillUnmount']);
});
it('recovers from nested componentWillUnmount errors on update', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(
<ErrorBoundary>
<Normal>
<BrokenComponentWillUnmount />
</Normal>
<BrokenComponentWillUnmount />
</ErrorBoundary>,
);
});
Scheduler.unstable_clearLog();
await act(async () => {
root.render(
<ErrorBoundary>
<Normal>
<BrokenComponentWillUnmount />
</Normal>
</ErrorBoundary>,
);
});
expect(container.textContent).toBe('Caught an error: Hello.');
assertLog([
'ErrorBoundary componentWillReceiveProps',
'ErrorBoundary componentWillUpdate',
'ErrorBoundary render success',
'Normal componentWillReceiveProps',
'Normal componentWillUpdate',
'Normal render',
'BrokenComponentWillUnmount componentWillReceiveProps',
'BrokenComponentWillUnmount componentWillUpdate',
'BrokenComponentWillUnmount render',
'BrokenComponentWillUnmount componentWillUnmount [!]',
'BrokenComponentWillUnmount componentDidUpdate',
'Normal componentDidUpdate',
'ErrorBoundary componentDidUpdate',
'ErrorBoundary static getDerivedStateFromError',
'ErrorBoundary componentWillUpdate',
'ErrorBoundary render error',
'Normal componentWillUnmount',
'BrokenComponentWillUnmount componentWillUnmount [!]',
'ErrorBoundary componentDidUpdate',
'ErrorBoundary static getDerivedStateFromError',
'ErrorBoundary componentWillUpdate',
'ErrorBoundary render error',
'ErrorBoundary componentDidUpdate',
]);
root.unmount();
assertLog(['ErrorBoundary componentWillUnmount']);
});
it('picks the right boundary when handling unmounting errors', async () => {
function renderInnerError(error) {
return <div>Caught an inner error: {error.message}.</div>;
}
function renderOuterError(error) {
return <div>Caught an outer error: {error.message}.</div>;
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(
<ErrorBoundary
logName="OuterErrorBoundary"
renderError={renderOuterError}>
<ErrorBoundary
logName="InnerErrorBoundary"
renderError={renderInnerError}>
<BrokenComponentWillUnmount />
</ErrorBoundary>
</ErrorBoundary>,
);
});
Scheduler.unstable_clearLog();
await act(async () => {
root.render(
<ErrorBoundary
logName="OuterErrorBoundary"
renderError={renderOuterError}>
<ErrorBoundary
logName="InnerErrorBoundary"
renderError={renderInnerError}
/>
</ErrorBoundary>,
);
});
expect(container.textContent).toBe('Caught an inner error: Hello.');
assertLog([
'OuterErrorBoundary componentWillReceiveProps',
'OuterErrorBoundary componentWillUpdate',
'OuterErrorBoundary render success',
'InnerErrorBoundary componentWillReceiveProps',
'InnerErrorBoundary componentWillUpdate',
'InnerErrorBoundary render success',
'BrokenComponentWillUnmount componentWillUnmount [!]',
'InnerErrorBoundary componentDidUpdate',
'OuterErrorBoundary componentDidUpdate',
'ErrorBoundary static getDerivedStateFromError',
'InnerErrorBoundary componentWillUpdate',
'InnerErrorBoundary render error',
'InnerErrorBoundary componentDidUpdate',
]);
root.unmount();
assertLog([
'OuterErrorBoundary componentWillUnmount',
'InnerErrorBoundary componentWillUnmount',
]);
});
it('can recover from error state', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(
<ErrorBoundary>
<BrokenRender />
</ErrorBoundary>,
);
});
Scheduler.unstable_clearLog();
await act(async () => {
root.render(
<ErrorBoundary>
<Normal />
</ErrorBoundary>,
);
});
expect(container.textContent).toBe('Caught an error: Hello.');
Scheduler.unstable_clearLog();
await act(async () => {
root.render(
<ErrorBoundary forceRetry={true}>
<Normal />
</ErrorBoundary>,
);
});
expect(container.textContent).not.toContain('Caught an error');
assertLog([
'ErrorBoundary componentWillReceiveProps',
'ErrorBoundary componentWillUpdate',
'ErrorBoundary render success',
'Normal constructor',
'Normal componentWillMount',
'Normal render',
'Normal componentDidMount',
'ErrorBoundary componentDidUpdate',
]);
root.unmount();
assertLog([
'ErrorBoundary componentWillUnmount',
'Normal componentWillUnmount',
]);
});
it('can update multiple times in error state', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(
<ErrorBoundary>
<BrokenRender />
</ErrorBoundary>,
);
});
expect(container.textContent).toBe('Caught an error: Hello.');
Scheduler.unstable_clearLog();
await act(async () => {
root.render(
<ErrorBoundary>
<BrokenRender />
</ErrorBoundary>,
);
});
expect(container.textContent).toBe('Caught an error: Hello.');
Scheduler.unstable_clearLog();
await act(async () => {
root.render(<div>Other screen</div>);
});
expect(container.textContent).toBe('Other screen');
root.unmount();
});
it("doesn't get into inconsistent state during removals", async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(
<ErrorBoundary>
<Normal />
<BrokenComponentWillUnmount />
<Normal />
</ErrorBoundary>,
);
});
Scheduler.unstable_clearLog();
await act(async () => {
root.render(<ErrorBoundary />);
});
expect(container.textContent).toBe('Caught an error: Hello.');
Scheduler.unstable_clearLog();
root.unmount();
assertLog(['ErrorBoundary componentWillUnmount']);
});
it("doesn't get into inconsistent state during additions", async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(<ErrorBoundary />);
});
assertLog([
'ErrorBoundary constructor',
'ErrorBoundary componentWillMount',
'ErrorBoundary render success',
'ErrorBoundary componentDidMount',
]);
await act(async () => {
root.render(
<ErrorBoundary>
<Normal />
<BrokenRender />
<Normal />
</ErrorBoundary>,
);
});
expect(container.textContent).toBe('Caught an error: Hello.');
Scheduler.unstable_clearLog();
root.unmount();
assertLog(['ErrorBoundary componentWillUnmount']);
});
it("doesn't get into inconsistent state during reorders", async () => {
function getAMixOfNormalAndBrokenRenderElements() {
const elements = [];
for (let i = 0; i < 100; i++) {
elements.push(<Normal key={i} />);
}
elements.push(<MaybeBrokenRender key={100} />);
let currentIndex = elements.length;
while (0 !== currentIndex) {
const randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex -= 1;
const temporaryValue = elements[currentIndex];
elements[currentIndex] = elements[randomIndex];
elements[randomIndex] = temporaryValue;
}
return elements;
}
class MaybeBrokenRender extends React.Component {
render() {
if (fail) {
throw new Error('Hello');
}
return <div>{this.props.children}</div>;
}
}
let fail = false;
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(
<ErrorBoundary>
{getAMixOfNormalAndBrokenRenderElements()}
</ErrorBoundary>,
);
});
expect(container.textContent).not.toContain('Caught an error');
fail = true;
Scheduler.unstable_clearLog();
await act(async () => {
root.render(
<ErrorBoundary>
{getAMixOfNormalAndBrokenRenderElements()}
</ErrorBoundary>,
);
});
expect(container.textContent).toBe('Caught an error: Hello.');
Scheduler.unstable_clearLog();
root.unmount();
assertLog(['ErrorBoundary componentWillUnmount']);
});
it('catches errors originating downstream', async () => {
let fail = false;
class Stateful extends React.Component {
state = {shouldThrow: false};
render() {
if (fail) {
Scheduler.log('Stateful render [!]');
throw new Error('Hello');
}
return <div>{this.props.children}</div>;
}
}
let statefulInst;
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(
<ErrorBoundary>
<Stateful ref={inst => (statefulInst = inst)} />
</ErrorBoundary>,
);
});
Scheduler.unstable_clearLog();
expect(() => {
fail = true;
statefulInst.forceUpdate();
}).not.toThrow();
root.unmount();
assertLog(['ErrorBoundary componentWillUnmount']);
});
it('catches errors in componentDidMount', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(
<ErrorBoundary>
<BrokenComponentWillUnmount>
<Normal />
</BrokenComponentWillUnmount>
<BrokenComponentDidMount />
<Normal logName="LastChild" />
</ErrorBoundary>,
);
});
assertLog([
'ErrorBoundary constructor',
'ErrorBoundary componentWillMount',
'ErrorBoundary render success',
'BrokenComponentWillUnmount constructor',
'BrokenComponentWillUnmount componentWillMount',
'BrokenComponentWillUnmount render',
'Normal constructor',
'Normal componentWillMount',
'Normal render',
'BrokenComponentDidMount constructor',
'BrokenComponentDidMount componentWillMount',
'BrokenComponentDidMount render',
'LastChild constructor',
'LastChild componentWillMount',
'LastChild render',
'Normal componentDidMount',
'BrokenComponentWillUnmount componentDidMount',
'BrokenComponentDidMount componentDidMount [!]',
'LastChild componentDidMount',
'ErrorBoundary componentDidMount',
'ErrorBoundary static getDerivedStateFromError',
'ErrorBoundary componentWillUpdate',
'ErrorBoundary render error',
'BrokenComponentWillUnmount componentWillUnmount [!]',
'Normal componentWillUnmount',
'BrokenComponentDidMount componentWillUnmount',
'LastChild componentWillUnmount',
'ErrorBoundary componentDidUpdate',
'ErrorBoundary static getDerivedStateFromError',
'ErrorBoundary componentWillUpdate',
'ErrorBoundary render error',
'ErrorBoundary componentDidUpdate',
]);
root.unmount();
assertLog(['ErrorBoundary componentWillUnmount']);
});
it('catches errors in componentDidUpdate', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(
<ErrorBoundary>
<BrokenComponentDidUpdate />
</ErrorBoundary>,
);
});
Scheduler.unstable_clearLog();
await act(async () => {
root.render(
<ErrorBoundary>
<BrokenComponentDidUpdate />
</ErrorBoundary>,
);
});
assertLog([
'ErrorBoundary componentWillReceiveProps',
'ErrorBoundary componentWillUpdate',
'ErrorBoundary render success',
'BrokenComponentDidUpdate componentWillReceiveProps',
'BrokenComponentDidUpdate componentWillUpdate',
'BrokenComponentDidUpdate render',
'BrokenComponentDidUpdate componentDidUpdate [!]',
'ErrorBoundary componentDidUpdate',
'ErrorBoundary static getDerivedStateFromError',
'ErrorBoundary componentWillUpdate',
'ErrorBoundary render error',
'BrokenComponentDidUpdate componentWillUnmount',
'ErrorBoundary componentDidUpdate',
]);
root.unmount();
assertLog(['ErrorBoundary componentWillUnmount']);
});
it('catches errors in useEffect', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(
<ErrorBoundary>
<BrokenUseEffect>Initial value</BrokenUseEffect>
</ErrorBoundary>,
);
});
assertLog([
'ErrorBoundary constructor',
'ErrorBoundary componentWillMount',
'ErrorBoundary render success',
'BrokenUseEffect render',
'ErrorBoundary componentDidMount',
'BrokenUseEffect useEffect [!]',
'ErrorBoundary static getDerivedStateFromError',
'ErrorBoundary componentWillUpdate',
'ErrorBoundary render error',
'ErrorBoundary componentDidUpdate',
]);
expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
});
it('catches errors in useLayoutEffect', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(
<ErrorBoundary>
<BrokenUseLayoutEffect>Initial value</BrokenUseLayoutEffect>
</ErrorBoundary>,
);
});
assertLog([
'ErrorBoundary constructor',
'ErrorBoundary componentWillMount',
'ErrorBoundary render success',
'BrokenUseLayoutEffect render',
'BrokenUseLayoutEffect useLayoutEffect [!]',
'ErrorBoundary componentDidMount',
'ErrorBoundary static getDerivedStateFromError',
'ErrorBoundary componentWillUpdate',
'ErrorBoundary render error',
'ErrorBoundary componentDidUpdate',
]);
expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
});
it('propagates errors inside boundary during componentDidMount', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(
<ErrorBoundary>
<BrokenComponentDidMountErrorBoundary
renderError={error => (
<div>We should never catch our own error: {error.message}.</div>
)}
/>
</ErrorBoundary>,
);
});
expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
assertLog([
'ErrorBoundary constructor',
'ErrorBoundary componentWillMount',
'ErrorBoundary render success',
'BrokenComponentDidMountErrorBoundary constructor',
'BrokenComponentDidMountErrorBoundary componentWillMount',
'BrokenComponentDidMountErrorBoundary render success',
'BrokenComponentDidMountErrorBoundary componentDidMount [!]',
'ErrorBoundary componentDidMount',
'ErrorBoundary static getDerivedStateFromError',
'ErrorBoundary componentWillUpdate',
'ErrorBoundary render error',
'BrokenComponentDidMountErrorBoundary componentWillUnmount',
'ErrorBoundary componentDidUpdate',
]);
root.unmount();
assertLog(['ErrorBoundary componentWillUnmount']);
});
it('calls static getDerivedStateFromError for each error that is captured', async () => {
function renderUnmountError(error) {
return <div>Caught an unmounting error: {error.message}.</div>;
}
function renderUpdateError(error) {
return <div>Caught an updating error: {error.message}.</div>;
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(
<ErrorBoundary logName="OuterErrorBoundary">
<ErrorBoundary
logName="InnerUnmountBoundary"
renderError={renderUnmountError}>
<BrokenComponentWillUnmount errorText="E1" />
<BrokenComponentWillUnmount errorText="E2" />
</ErrorBoundary>
<ErrorBoundary
logName="InnerUpdateBoundary"
renderError={renderUpdateError}>
<BrokenComponentDidUpdate errorText="E3" />
<BrokenComponentDidUpdate errorText="E4" />
</ErrorBoundary>
</ErrorBoundary>,
);
});
Scheduler.unstable_clearLog();
await act(async () => {
root.render(
<ErrorBoundary logName="OuterErrorBoundary">
<ErrorBoundary
logName="InnerUnmountBoundary"
renderError={renderUnmountError}
/>
<ErrorBoundary
logName="InnerUpdateBoundary"
renderError={renderUpdateError}>
<BrokenComponentDidUpdate errorText="E3" />
<BrokenComponentDidUpdate errorText="E4" />
</ErrorBoundary>
</ErrorBoundary>,
);
});
expect(container.firstChild.textContent).toBe(
'Caught an unmounting error: E2.' + 'Caught an updating error: E4.',
);
assertLog([
'OuterErrorBoundary componentWillReceiveProps',
'OuterErrorBoundary componentWillUpdate',
'OuterErrorBoundary render success',
'InnerUnmountBoundary componentWillReceiveProps',
'InnerUnmountBoundary componentWillUpdate',
'InnerUnmountBoundary render success',
'InnerUpdateBoundary componentWillReceiveProps',
'InnerUpdateBoundary componentWillUpdate',
'InnerUpdateBoundary render success',
'BrokenComponentDidUpdate componentWillReceiveProps',
'BrokenComponentDidUpdate componentWillUpdate',
'BrokenComponentDidUpdate render',
'BrokenComponentDidUpdate componentWillReceiveProps',
'BrokenComponentDidUpdate componentWillUpdate',
'BrokenComponentDidUpdate render',
'BrokenComponentWillUnmount componentWillUnmount [!]',
'BrokenComponentWillUnmount componentWillUnmount [!]',
'InnerUnmountBoundary componentDidUpdate',
'BrokenComponentDidUpdate componentDidUpdate [!]',
'BrokenComponentDidUpdate componentDidUpdate [!]',
'InnerUpdateBoundary componentDidUpdate',
'OuterErrorBoundary componentDidUpdate',
'ErrorBoundary static getDerivedStateFromError',
'ErrorBoundary static getDerivedStateFromError',
'InnerUnmountBoundary componentWillUpdate',
'InnerUnmountBoundary render error',
'ErrorBoundary static getDerivedStateFromError',
'ErrorBoundary static getDerivedStateFromError',
'InnerUpdateBoundary componentWillUpdate',
'InnerUpdateBoundary render error',
'BrokenComponentDidUpdate componentWillUnmount',
'BrokenComponentDidUpdate componentWillUnmount',
'InnerUnmountBoundary componentDidUpdate',
'InnerUpdateBoundary componentDidUpdate',
]);
root.unmount();
assertLog([
'OuterErrorBoundary componentWillUnmount',
'InnerUnmountBoundary componentWillUnmount',
'InnerUpdateBoundary componentWillUnmount',
]);
});
it('discards a bad root if the root component fails', async () => {
const X = null;
const Y = undefined;
let err1;
let err2;
try {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await expect(
async () =>
await act(async () => {
root.render(<X />, container);
}),
).toErrorDev(
'React.createElement: type is invalid -- expected a string ' +
'(for built-in components) or a class/function ' +
'(for composite components) but got: null.',
);
} catch (err) {
err1 = err;
}
try {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await expect(
async () =>
await act(async () => {
root.render(<Y />, container);
}),
).toErrorDev(
'React.createElement: type is invalid -- expected a string ' +
'(for built-in components) or a class/function ' +
'(for composite components) but got: undefined.',
);
} catch (err) {
err2 = err;
}
expect(err1.message).toMatch(/got: null/);
expect(err2.message).toMatch(/got: undefined/);
});
it('renders empty output if error boundary does not handle the error', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await expect(async () => {
await act(async () => {
root.render(
<div>
Sibling
<NoopErrorBoundary>
<BrokenRender />
</NoopErrorBoundary>
</div>,
);
});
}).rejects.toThrow('Hello');
expect(container.innerHTML).toBe('');
assertLog([
'NoopErrorBoundary constructor',
'NoopErrorBoundary componentWillMount',
'NoopErrorBoundary render',
'BrokenRender constructor',
'BrokenRender componentWillMount',
'BrokenRender render [!]',
'NoopErrorBoundary static getDerivedStateFromError',
'NoopErrorBoundary render',
'BrokenRender constructor',
'BrokenRender componentWillMount',
'BrokenRender render [!]',
'NoopErrorBoundary constructor',
'NoopErrorBoundary componentWillMount',
'NoopErrorBoundary render',
'BrokenRender constructor',
'BrokenRender componentWillMount',
'BrokenRender render [!]',
'NoopErrorBoundary static getDerivedStateFromError',
'NoopErrorBoundary render',
'BrokenRender constructor',
'BrokenRender componentWillMount',
'BrokenRender render [!]',
]);
});
it('passes an aggregate error when two errors happen in commit', async () => {
const errors = [];
let caughtError;
class Parent extends React.Component {
render() {
return <Child />;
}
componentDidMount() {
errors.push('parent sad');
throw new Error('parent sad');
}
}
class Child extends React.Component {
render() {
return <div />;
}
componentDidMount() {
errors.push('child sad');
throw new Error('child sad');
}
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
try {
await act(async () => {
root.render(<Parent />);
});
} catch (e) {
caughtError = e;
}
expect(errors).toEqual(['child sad', 'parent sad']);
expect(caughtError.errors).toEqual([
expect.objectContaining({message: 'child sad'}),
expect.objectContaining({message: 'parent sad'}),
]);
});
it('propagates uncaught error inside unbatched initial mount', async () => {
function Foo() {
throw new Error('foo error');
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await expect(async () => {
await ReactDOM.unstable_batchedUpdates(async () => {
await act(async () => {
root.render(<Foo />);
});
});
}).rejects.toThrow('foo error');
});
it('handles errors that occur in before-mutation commit hook', async () => {
const errors = [];
let caughtError;
class Parent extends React.Component {
getSnapshotBeforeUpdate() {
errors.push('parent sad');
throw new Error('parent sad');
}
componentDidUpdate() {}
render() {
return <Child {...this.props} />;
}
}
class Child extends React.Component {
getSnapshotBeforeUpdate() {
errors.push('child sad');
throw new Error('child sad');
}
componentDidUpdate() {}
render() {
return <div />;
}
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(<Parent value={1} />);
});
try {
await act(async () => {
root.render(<Parent value={2} />);
});
} catch (e) {
caughtError = e;
}
expect(errors).toEqual(['child sad', 'parent sad']);
expect(caughtError.errors).toEqual([
expect.objectContaining({message: 'child sad'}),
expect.objectContaining({message: 'parent sad'}),
]);
});
it('should warn if an error boundary with only componentDidCatch does not update state', async () => {
class InvalidErrorBoundary extends React.Component {
componentDidCatch(error, info) {
}
render() {
return this.props.children;
}
}
const Throws = () => {
throw new Error('expected');
};
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await expect(async () => {
await act(async () => {
root.render(
<InvalidErrorBoundary>
<Throws />
</InvalidErrorBoundary>,
);
});
}).toErrorDev(
'InvalidErrorBoundary: Error boundaries should implement getDerivedStateFromError(). ' +
'In that method, return a state update to display an error message or fallback UI.',
);
expect(container.textContent).toBe('');
});
it('should call both componentDidCatch and getDerivedStateFromError if both exist on a component', async () => {
let componentDidCatchError, getDerivedStateFromErrorError;
class ErrorBoundaryWithBothMethods extends React.Component {
state = {error: null};
static getDerivedStateFromError(error) {
getDerivedStateFromErrorError = error;
return {error};
}
componentDidCatch(error, info) {
componentDidCatchError = error;
}
render() {
return this.state.error ? 'ErrorBoundary' : this.props.children;
}
}
const thrownError = new Error('expected');
const Throws = () => {
throw thrownError;
};
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(
<ErrorBoundaryWithBothMethods>
<Throws />
</ErrorBoundaryWithBothMethods>,
);
});
expect(container.textContent).toBe('ErrorBoundary');
expect(componentDidCatchError).toBe(thrownError);
expect(getDerivedStateFromErrorError).toBe(thrownError);
});
it('should catch errors from invariants in completion phase', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(
<ErrorBoundary>
<input>
<div />
</input>
</ErrorBoundary>,
);
});
expect(container.textContent).toContain(
'Caught an error: input is a void element tag',
);
});
it('should catch errors from errors in the throw phase from boundaries', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
const thrownError = new Error('original error');
const Throws = () => {
throw thrownError;
};
class EvilErrorBoundary extends React.Component {
get componentDidCatch() {
throw new Error('gotta catch em all');
}
render() {
return this.props.children;
}
}
await act(async () => {
root.render(
<ErrorBoundary>
<EvilErrorBoundary>
<Throws />
</EvilErrorBoundary>
</ErrorBoundary>,
);
});
expect(container.textContent).toContain(
'Caught an error: gotta catch em all',
);
});
it('should protect errors from errors in the stack generation', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
const evilError = {
message: 'gotta catch em all',
get stack() {
throw new Error('gotta catch em all');
},
};
const Throws = () => {
throw evilError;
};
Object.defineProperty(Throws, 'displayName', {
get: function () {
throw new Error('gotta catch em all');
},
});
function Wrapper() {
return <Throws />;
}
await expect(async () => {
await act(async () => {
root.render(
<ErrorBoundary>
<Wrapper />
</ErrorBoundary>,
);
});
}).rejects.toThrow('gotta catch em all');
expect(container.textContent).toContain(
'Caught an error: gotta catch em all.',
);
});
it('catches errors thrown in componentWillUnmount', async () => {
class LocalErrorBoundary extends React.Component {
state = {error: null};
static getDerivedStateFromError(error) {
Scheduler.log(`ErrorBoundary static getDerivedStateFromError`);
return {error};
}
render() {
const {children, id, fallbackID} = this.props;
const {error} = this.state;
if (error) {
Scheduler.log(`${id} render error`);
return <Component id={fallbackID} />;
}
Scheduler.log(`${id} render success`);
return children || null;
}
}
class Component extends React.Component {
render() {
const {id} = this.props;
Scheduler.log('Component render ' + id);
return id;
}
}
class LocalBrokenComponentWillUnmount extends React.Component {
componentWillUnmount() {
Scheduler.log('BrokenComponentWillUnmount componentWillUnmount');
throw Error('Expected');
}
render() {
Scheduler.log('BrokenComponentWillUnmount render');
return 'broken';
}
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(
<LocalErrorBoundary id="OuterBoundary" fallbackID="OuterFallback">
<Component id="sibling" />
<LocalErrorBoundary id="InnerBoundary" fallbackID="InnerFallback">
<LocalBrokenComponentWillUnmount />
</LocalErrorBoundary>
</LocalErrorBoundary>,
);
});
expect(container.firstChild.textContent).toBe('sibling');
expect(container.lastChild.textContent).toBe('broken');
assertLog([
'OuterBoundary render success',
'Component render sibling',
'InnerBoundary render success',
'BrokenComponentWillUnmount render',
]);
await act(async () => {
root.render(
<LocalErrorBoundary id="OuterBoundary" fallbackID="OuterFallback">
<Component id="sibling" />
</LocalErrorBoundary>,
);
});
expect(container.firstChild.textContent).toBe('OuterFallback');
expect(container.lastChild.textContent).toBe('OuterFallback');
assertLog([
'OuterBoundary render success',
'Component render sibling',
'BrokenComponentWillUnmount componentWillUnmount',
'ErrorBoundary static getDerivedStateFromError',
'OuterBoundary render error',
'Component render OuterFallback',
]);
});
it('catches errors thrown while detaching refs', async () => {
class LocalErrorBoundary extends React.Component {
state = {error: null};
static getDerivedStateFromError(error) {
Scheduler.log(`ErrorBoundary static getDerivedStateFromError`);
return {error};
}
render() {
const {children, id, fallbackID} = this.props;
const {error} = this.state;
if (error) {
Scheduler.log(`${id} render error`);
return <Component id={fallbackID} />;
}
Scheduler.log(`${id} render success`);
return children || null;
}
}
class Component extends React.Component {
render() {
const {id} = this.props;
Scheduler.log('Component render ' + id);
return id;
}
}
class LocalBrokenCallbackRef extends React.Component {
_ref = ref => {
Scheduler.log('LocalBrokenCallbackRef ref ' + !!ref);
if (ref === null) {
throw Error('Expected');
}
};
render() {
Scheduler.log('LocalBrokenCallbackRef render');
return <div ref={this._ref}>ref</div>;
}
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(
<LocalErrorBoundary id="OuterBoundary" fallbackID="OuterFallback">
<Component id="sibling" />
<LocalErrorBoundary id="InnerBoundary" fallbackID="InnerFallback">
<LocalBrokenCallbackRef />
</LocalErrorBoundary>
</LocalErrorBoundary>,
);
});
expect(container.firstChild.textContent).toBe('sibling');
expect(container.lastChild.textContent).toBe('ref');
assertLog([
'OuterBoundary render success',
'Component render sibling',
'InnerBoundary render success',
'LocalBrokenCallbackRef render',
'LocalBrokenCallbackRef ref true',
]);
await act(async () => {
root.render(
<LocalErrorBoundary id="OuterBoundary" fallbackID="OuterFallback">
<Component id="sibling" />
</LocalErrorBoundary>,
);
});
expect(container.firstChild.textContent).toBe('OuterFallback');
expect(container.lastChild.textContent).toBe('OuterFallback');
assertLog([
'OuterBoundary render success',
'Component render sibling',
'LocalBrokenCallbackRef ref false',
'ErrorBoundary static getDerivedStateFromError',
'OuterBoundary render error',
'Component render OuterFallback',
]);
});
});