UNPKG

arkit

Version:

Visualises JavaScript, TypeScript and Flow codebases as meaningful and committable architecture diagrams

1,539 lines (1,441 loc) 72.9 kB
/** * Copyright (c) Facebook, Inc. and its 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 */ 'use strict'; let PropTypes; let React; let ReactDOM; let ReactFeatureFlags; describe('ReactErrorBoundaries', () => { let log; 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; beforeEach(() => { jest.useFakeTimers(); jest.resetModules(); PropTypes = require('prop-types'); ReactFeatureFlags = require('shared/ReactFeatureFlags'); ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false; ReactDOM = require('react-dom'); React = require('react'); log = []; BrokenConstructor = class extends React.Component { constructor(props) { super(props); log.push('BrokenConstructor constructor [!]'); throw new Error('Hello'); } render() { log.push('BrokenConstructor render'); return <div>{this.props.children}</div>; } UNSAFE_componentWillMount() { log.push('BrokenConstructor componentWillMount'); } componentDidMount() { log.push('BrokenConstructor componentDidMount'); } UNSAFE_componentWillReceiveProps() { log.push('BrokenConstructor componentWillReceiveProps'); } UNSAFE_componentWillUpdate() { log.push('BrokenConstructor componentWillUpdate'); } componentDidUpdate() { log.push('BrokenConstructor componentDidUpdate'); } componentWillUnmount() { log.push('BrokenConstructor componentWillUnmount'); } }; BrokenComponentWillMount = class extends React.Component { constructor(props) { super(props); log.push('BrokenComponentWillMount constructor'); } render() { log.push('BrokenComponentWillMount render'); return <div>{this.props.children}</div>; } UNSAFE_componentWillMount() { log.push('BrokenComponentWillMount componentWillMount [!]'); throw new Error('Hello'); } componentDidMount() { log.push('BrokenComponentWillMount componentDidMount'); } UNSAFE_componentWillReceiveProps() { log.push('BrokenComponentWillMount componentWillReceiveProps'); } UNSAFE_componentWillUpdate() { log.push('BrokenComponentWillMount componentWillUpdate'); } componentDidUpdate() { log.push('BrokenComponentWillMount componentDidUpdate'); } componentWillUnmount() { log.push('BrokenComponentWillMount componentWillUnmount'); } }; BrokenComponentDidMount = class extends React.Component { constructor(props) { super(props); log.push('BrokenComponentDidMount constructor'); } render() { log.push('BrokenComponentDidMount render'); return <div>{this.props.children}</div>; } UNSAFE_componentWillMount() { log.push('BrokenComponentDidMount componentWillMount'); } componentDidMount() { log.push('BrokenComponentDidMount componentDidMount [!]'); throw new Error('Hello'); } UNSAFE_componentWillReceiveProps() { log.push('BrokenComponentDidMount componentWillReceiveProps'); } UNSAFE_componentWillUpdate() { log.push('BrokenComponentDidMount componentWillUpdate'); } componentDidUpdate() { log.push('BrokenComponentDidMount componentDidUpdate'); } componentWillUnmount() { log.push('BrokenComponentDidMount componentWillUnmount'); } }; BrokenComponentWillReceiveProps = class extends React.Component { constructor(props) { super(props); log.push('BrokenComponentWillReceiveProps constructor'); } render() { log.push('BrokenComponentWillReceiveProps render'); return <div>{this.props.children}</div>; } UNSAFE_componentWillMount() { log.push('BrokenComponentWillReceiveProps componentWillMount'); } componentDidMount() { log.push('BrokenComponentWillReceiveProps componentDidMount'); } UNSAFE_componentWillReceiveProps() { log.push( 'BrokenComponentWillReceiveProps componentWillReceiveProps [!]', ); throw new Error('Hello'); } UNSAFE_componentWillUpdate() { log.push('BrokenComponentWillReceiveProps componentWillUpdate'); } componentDidUpdate() { log.push('BrokenComponentWillReceiveProps componentDidUpdate'); } componentWillUnmount() { log.push('BrokenComponentWillReceiveProps componentWillUnmount'); } }; BrokenComponentWillUpdate = class extends React.Component { constructor(props) { super(props); log.push('BrokenComponentWillUpdate constructor'); } render() { log.push('BrokenComponentWillUpdate render'); return <div>{this.props.children}</div>; } UNSAFE_componentWillMount() { log.push('BrokenComponentWillUpdate componentWillMount'); } componentDidMount() { log.push('BrokenComponentWillUpdate componentDidMount'); } UNSAFE_componentWillReceiveProps() { log.push('BrokenComponentWillUpdate componentWillReceiveProps'); } UNSAFE_componentWillUpdate() { log.push('BrokenComponentWillUpdate componentWillUpdate [!]'); throw new Error('Hello'); } componentDidUpdate() { log.push('BrokenComponentWillUpdate componentDidUpdate'); } componentWillUnmount() { log.push('BrokenComponentWillUpdate componentWillUnmount'); } }; BrokenComponentDidUpdate = class extends React.Component { static defaultProps = { errorText: 'Hello', }; constructor(props) { super(props); log.push('BrokenComponentDidUpdate constructor'); } render() { log.push('BrokenComponentDidUpdate render'); return <div>{this.props.children}</div>; } UNSAFE_componentWillMount() { log.push('BrokenComponentDidUpdate componentWillMount'); } componentDidMount() { log.push('BrokenComponentDidUpdate componentDidMount'); } UNSAFE_componentWillReceiveProps() { log.push('BrokenComponentDidUpdate componentWillReceiveProps'); } UNSAFE_componentWillUpdate() { log.push('BrokenComponentDidUpdate componentWillUpdate'); } componentDidUpdate() { log.push('BrokenComponentDidUpdate componentDidUpdate [!]'); throw new Error(this.props.errorText); } componentWillUnmount() { log.push('BrokenComponentDidUpdate componentWillUnmount'); } }; BrokenComponentWillUnmount = class extends React.Component { static defaultProps = { errorText: 'Hello', }; constructor(props) { super(props); log.push('BrokenComponentWillUnmount constructor'); } render() { log.push('BrokenComponentWillUnmount render'); return <div>{this.props.children}</div>; } UNSAFE_componentWillMount() { log.push('BrokenComponentWillUnmount componentWillMount'); } componentDidMount() { log.push('BrokenComponentWillUnmount componentDidMount'); } UNSAFE_componentWillReceiveProps() { log.push('BrokenComponentWillUnmount componentWillReceiveProps'); } UNSAFE_componentWillUpdate() { log.push('BrokenComponentWillUnmount componentWillUpdate'); } componentDidUpdate() { log.push('BrokenComponentWillUnmount componentDidUpdate'); } componentWillUnmount() { log.push('BrokenComponentWillUnmount componentWillUnmount [!]'); throw new Error(this.props.errorText); } }; BrokenComponentWillMountErrorBoundary = class extends React.Component { constructor(props) { super(props); this.state = {error: null}; log.push('BrokenComponentWillMountErrorBoundary constructor'); } render() { if (this.state.error) { log.push('BrokenComponentWillMountErrorBoundary render error'); return <div>Caught an error: {this.state.error.message}.</div>; } log.push('BrokenComponentWillMountErrorBoundary render success'); return <div>{this.props.children}</div>; } UNSAFE_componentWillMount() { log.push( 'BrokenComponentWillMountErrorBoundary componentWillMount [!]', ); throw new Error('Hello'); } componentDidMount() { log.push('BrokenComponentWillMountErrorBoundary componentDidMount'); } componentWillUnmount() { log.push('BrokenComponentWillMountErrorBoundary componentWillUnmount'); } static getDerivedStateFromError(error) { log.push( 'BrokenComponentWillMountErrorBoundary static getDerivedStateFromError', ); return {error}; } }; BrokenComponentDidMountErrorBoundary = class extends React.Component { constructor(props) { super(props); this.state = {error: null}; log.push('BrokenComponentDidMountErrorBoundary constructor'); } render() { if (this.state.error) { log.push('BrokenComponentDidMountErrorBoundary render error'); return <div>Caught an error: {this.state.error.message}.</div>; } log.push('BrokenComponentDidMountErrorBoundary render success'); return <div>{this.props.children}</div>; } UNSAFE_componentWillMount() { log.push('BrokenComponentDidMountErrorBoundary componentWillMount'); } componentDidMount() { log.push('BrokenComponentDidMountErrorBoundary componentDidMount [!]'); throw new Error('Hello'); } componentWillUnmount() { log.push('BrokenComponentDidMountErrorBoundary componentWillUnmount'); } static getDerivedStateFromError(error) { log.push( 'BrokenComponentDidMountErrorBoundary static getDerivedStateFromError', ); return {error}; } }; BrokenRenderErrorBoundary = class extends React.Component { constructor(props) { super(props); this.state = {error: null}; log.push('BrokenRenderErrorBoundary constructor'); } render() { if (this.state.error) { log.push('BrokenRenderErrorBoundary render error [!]'); throw new Error('Hello'); } log.push('BrokenRenderErrorBoundary render success'); return <div>{this.props.children}</div>; } UNSAFE_componentWillMount() { log.push('BrokenRenderErrorBoundary componentWillMount'); } componentDidMount() { log.push('BrokenRenderErrorBoundary componentDidMount'); } componentWillUnmount() { log.push('BrokenRenderErrorBoundary componentWillUnmount'); } static getDerivedStateFromError(error) { log.push('BrokenRenderErrorBoundary static getDerivedStateFromError'); return {error}; } }; BrokenRender = class extends React.Component { constructor(props) { super(props); log.push('BrokenRender constructor'); } render() { log.push('BrokenRender render [!]'); throw new Error('Hello'); } UNSAFE_componentWillMount() { log.push('BrokenRender componentWillMount'); } componentDidMount() { log.push('BrokenRender componentDidMount'); } UNSAFE_componentWillReceiveProps() { log.push('BrokenRender componentWillReceiveProps'); } UNSAFE_componentWillUpdate() { log.push('BrokenRender componentWillUpdate'); } componentDidUpdate() { log.push('BrokenRender componentDidUpdate'); } componentWillUnmount() { log.push('BrokenRender componentWillUnmount'); } }; BrokenUseEffect = props => { log.push('BrokenUseEffect render'); React.useEffect(() => { log.push('BrokenUseEffect useEffect [!]'); throw new Error('Hello'); }); return props.children; }; BrokenUseLayoutEffect = props => { log.push('BrokenUseLayoutEffect render'); React.useLayoutEffect(() => { log.push('BrokenUseLayoutEffect useLayoutEffect [!]'); throw new Error('Hello'); }); return props.children; }; NoopErrorBoundary = class extends React.Component { constructor(props) { super(props); log.push('NoopErrorBoundary constructor'); } render() { log.push('NoopErrorBoundary render'); return <BrokenRender />; } UNSAFE_componentWillMount() { log.push('NoopErrorBoundary componentWillMount'); } componentDidMount() { log.push('NoopErrorBoundary componentDidMount'); } componentWillUnmount() { log.push('NoopErrorBoundary componentWillUnmount'); } static getDerivedStateFromError() { log.push('NoopErrorBoundary static getDerivedStateFromError'); } }; Normal = class extends React.Component { static defaultProps = { logName: 'Normal', }; constructor(props) { super(props); log.push(`${this.props.logName} constructor`); } render() { log.push(`${this.props.logName} render`); return <div>{this.props.children}</div>; } UNSAFE_componentWillMount() { log.push(`${this.props.logName} componentWillMount`); } componentDidMount() { log.push(`${this.props.logName} componentDidMount`); } UNSAFE_componentWillReceiveProps() { log.push(`${this.props.logName} componentWillReceiveProps`); } UNSAFE_componentWillUpdate() { log.push(`${this.props.logName} componentWillUpdate`); } componentDidUpdate() { log.push(`${this.props.logName} componentDidUpdate`); } componentWillUnmount() { log.push(`${this.props.logName} componentWillUnmount`); } }; ErrorBoundary = class extends React.Component { constructor(props) { super(props); this.state = {error: null}; log.push(`${this.props.logName} constructor`); } render() { if (this.state.error && !this.props.forceRetry) { log.push(`${this.props.logName} render error`); return this.props.renderError(this.state.error, this.props); } log.push(`${this.props.logName} render success`); return <div>{this.props.children}</div>; } static getDerivedStateFromError(error) { log.push('ErrorBoundary static getDerivedStateFromError'); return {error}; } UNSAFE_componentWillMount() { log.push(`${this.props.logName} componentWillMount`); } componentDidMount() { log.push(`${this.props.logName} componentDidMount`); } UNSAFE_componentWillReceiveProps() { log.push(`${this.props.logName} componentWillReceiveProps`); } UNSAFE_componentWillUpdate() { log.push(`${this.props.logName} componentWillUpdate`); } componentDidUpdate() { log.push(`${this.props.logName} componentDidUpdate`); } componentWillUnmount() { log.push(`${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); log.push('RetryErrorBoundary constructor'); } render() { log.push('RetryErrorBoundary render'); return <BrokenRender />; } UNSAFE_componentWillMount() { log.push('RetryErrorBoundary componentWillMount'); } componentDidMount() { log.push('RetryErrorBoundary componentDidMount'); } componentWillUnmount() { log.push('RetryErrorBoundary componentWillUnmount'); } static getDerivedStateFromError(error) { log.push('RetryErrorBoundary static getDerivedStateFromError [!]'); // In Fiber, calling setState() (and failing) is treated as a rethrow. return {}; } }; ErrorMessage = class extends React.Component { constructor(props) { super(props); log.push('ErrorMessage constructor'); } UNSAFE_componentWillMount() { log.push('ErrorMessage componentWillMount'); } componentDidMount() { log.push('ErrorMessage componentDidMount'); } componentWillUnmount() { log.push('ErrorMessage componentWillUnmount'); } render() { log.push('ErrorMessage render'); return <div>Caught an error: {this.props.message}.</div>; } }; }); it('does not swallow exceptions on mounting without boundaries', () => { let container = document.createElement('div'); expect(() => { ReactDOM.render(<BrokenRender />, container); }).toThrow('Hello'); container = document.createElement('div'); expect(() => { ReactDOM.render(<BrokenComponentWillMount />, container); }).toThrow('Hello'); container = document.createElement('div'); expect(() => { ReactDOM.render(<BrokenComponentDidMount />, container); }).toThrow('Hello'); }); it('does not swallow exceptions on updating without boundaries', () => { let container = document.createElement('div'); ReactDOM.render(<BrokenComponentWillUpdate />, container); expect(() => { ReactDOM.render(<BrokenComponentWillUpdate />, container); }).toThrow('Hello'); container = document.createElement('div'); ReactDOM.render(<BrokenComponentWillReceiveProps />, container); expect(() => { ReactDOM.render(<BrokenComponentWillReceiveProps />, container); }).toThrow('Hello'); container = document.createElement('div'); ReactDOM.render(<BrokenComponentDidUpdate />, container); expect(() => { ReactDOM.render(<BrokenComponentDidUpdate />, container); }).toThrow('Hello'); }); it('does not swallow exceptions on unmounting without boundaries', () => { const container = document.createElement('div'); ReactDOM.render(<BrokenComponentWillUnmount />, container); expect(() => { ReactDOM.unmountComponentAtNode(container); }).toThrow('Hello'); }); it('prevents errors from leaking into other roots', () => { const container1 = document.createElement('div'); const container2 = document.createElement('div'); const container3 = document.createElement('div'); ReactDOM.render(<span>Before 1</span>, container1); expect(() => { ReactDOM.render(<BrokenRender />, container2); }).toThrow('Hello'); ReactDOM.render( <ErrorBoundary> <BrokenRender /> </ErrorBoundary>, container3, ); expect(container1.firstChild.textContent).toBe('Before 1'); expect(container2.firstChild).toBe(null); expect(container3.firstChild.textContent).toBe('Caught an error: Hello.'); ReactDOM.render(<span>After 1</span>, container1); ReactDOM.render(<span>After 2</span>, container2); ReactDOM.render( <ErrorBoundary forceRetry={true}>After 3</ErrorBoundary>, container3, ); expect(container1.firstChild.textContent).toBe('After 1'); expect(container2.firstChild.textContent).toBe('After 2'); expect(container3.firstChild.textContent).toBe('After 3'); ReactDOM.unmountComponentAtNode(container1); ReactDOM.unmountComponentAtNode(container2); ReactDOM.unmountComponentAtNode(container3); expect(container1.firstChild).toBe(null); expect(container2.firstChild).toBe(null); expect(container3.firstChild).toBe(null); }); it('renders an error state if child throws in render', () => { const container = document.createElement('div'); ReactDOM.render( <ErrorBoundary> <BrokenRender /> </ErrorBoundary>, container, ); expect(container.firstChild.textContent).toBe('Caught an error: Hello.'); expect(log).toEqual([ 'ErrorBoundary constructor', 'ErrorBoundary componentWillMount', 'ErrorBoundary render success', 'BrokenRender constructor', 'BrokenRender componentWillMount', 'BrokenRender render [!]', // Catch and render an error message 'ErrorBoundary static getDerivedStateFromError', 'ErrorBoundary componentWillMount', 'ErrorBoundary render error', 'ErrorBoundary componentDidMount', ]); log.length = 0; ReactDOM.unmountComponentAtNode(container); expect(log).toEqual(['ErrorBoundary componentWillUnmount']); }); it('renders an error state if child throws in constructor', () => { const container = document.createElement('div'); ReactDOM.render( <ErrorBoundary> <BrokenConstructor /> </ErrorBoundary>, container, ); expect(container.firstChild.textContent).toBe('Caught an error: Hello.'); expect(log).toEqual([ 'ErrorBoundary constructor', 'ErrorBoundary componentWillMount', 'ErrorBoundary render success', 'BrokenConstructor constructor [!]', // Catch and render an error message 'ErrorBoundary static getDerivedStateFromError', 'ErrorBoundary componentWillMount', 'ErrorBoundary render error', 'ErrorBoundary componentDidMount', ]); log.length = 0; ReactDOM.unmountComponentAtNode(container); expect(log).toEqual(['ErrorBoundary componentWillUnmount']); }); it('renders an error state if child throws in componentWillMount', () => { const container = document.createElement('div'); ReactDOM.render( <ErrorBoundary> <BrokenComponentWillMount /> </ErrorBoundary>, container, ); expect(container.firstChild.textContent).toBe('Caught an error: Hello.'); expect(log).toEqual([ 'ErrorBoundary constructor', 'ErrorBoundary componentWillMount', 'ErrorBoundary render success', 'BrokenComponentWillMount constructor', 'BrokenComponentWillMount componentWillMount [!]', // Catch and render an error message 'ErrorBoundary static getDerivedStateFromError', 'ErrorBoundary componentWillMount', 'ErrorBoundary render error', 'ErrorBoundary componentDidMount', ]); log.length = 0; ReactDOM.unmountComponentAtNode(container); expect(log).toEqual(['ErrorBoundary componentWillUnmount']); }); it('renders an error state if context provider throws in componentWillMount', () => { 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'); ReactDOM.render( <ErrorBoundary> <BrokenComponentWillMountWithContext /> </ErrorBoundary>, container, ); expect(container.firstChild.textContent).toBe('Caught an error: Hello.'); }); it('renders an error state if module-style context provider throws in componentWillMount', () => { function BrokenComponentWillMountWithContext() { return { getChildContext() { return {foo: 42}; }, render() { return <div>{this.props.children}</div>; }, UNSAFE_componentWillMount() { throw new Error('Hello'); }, }; } BrokenComponentWillMountWithContext.childContextTypes = { foo: PropTypes.number, }; const container = document.createElement('div'); ReactDOM.render( <ErrorBoundary> <BrokenComponentWillMountWithContext /> </ErrorBoundary>, container, ); expect(container.firstChild.textContent).toBe('Caught an error: Hello.'); }); it('mounts the error message if mounting fails', () => { function renderError(error) { return <ErrorMessage message={error.message} />; } const container = document.createElement('div'); ReactDOM.render( <ErrorBoundary renderError={renderError}> <BrokenRender /> </ErrorBoundary>, container, ); expect(log).toEqual([ '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', ]); log.length = 0; ReactDOM.unmountComponentAtNode(container); expect(log).toEqual([ 'ErrorBoundary componentWillUnmount', 'ErrorMessage componentWillUnmount', ]); }); it('propagates errors on retry on mounting', () => { const container = document.createElement('div'); ReactDOM.render( <ErrorBoundary> <RetryErrorBoundary> <BrokenRender /> </RetryErrorBoundary> </ErrorBoundary>, container, ); expect(container.firstChild.textContent).toBe('Caught an error: Hello.'); expect(log).toEqual([ 'ErrorBoundary constructor', 'ErrorBoundary componentWillMount', 'ErrorBoundary render success', 'RetryErrorBoundary constructor', 'RetryErrorBoundary componentWillMount', 'RetryErrorBoundary render', 'BrokenRender constructor', 'BrokenRender componentWillMount', 'BrokenRender render [!]', // Retry 'RetryErrorBoundary static getDerivedStateFromError [!]', 'RetryErrorBoundary componentWillMount', 'RetryErrorBoundary render', 'BrokenRender constructor', 'BrokenRender componentWillMount', 'BrokenRender render [!]', // This time, the error propagates to the higher boundary 'ErrorBoundary static getDerivedStateFromError', 'ErrorBoundary componentWillMount', 'ErrorBoundary render error', 'ErrorBoundary componentDidMount', ]); log.length = 0; ReactDOM.unmountComponentAtNode(container); expect(log).toEqual(['ErrorBoundary componentWillUnmount']); }); it('propagates errors inside boundary during componentWillMount', () => { const container = document.createElement('div'); ReactDOM.render( <ErrorBoundary> <BrokenComponentWillMountErrorBoundary /> </ErrorBoundary>, container, ); expect(container.firstChild.textContent).toBe('Caught an error: Hello.'); expect(log).toEqual([ 'ErrorBoundary constructor', 'ErrorBoundary componentWillMount', 'ErrorBoundary render success', 'BrokenComponentWillMountErrorBoundary constructor', 'BrokenComponentWillMountErrorBoundary componentWillMount [!]', // The error propagates to the higher boundary 'ErrorBoundary static getDerivedStateFromError', 'ErrorBoundary componentWillMount', 'ErrorBoundary render error', 'ErrorBoundary componentDidMount', ]); log.length = 0; ReactDOM.unmountComponentAtNode(container); expect(log).toEqual(['ErrorBoundary componentWillUnmount']); }); it('propagates errors inside boundary while rendering error state', () => { const container = document.createElement('div'); ReactDOM.render( <ErrorBoundary> <BrokenRenderErrorBoundary> <BrokenRender /> </BrokenRenderErrorBoundary> </ErrorBoundary>, container, ); expect(container.firstChild.textContent).toBe('Caught an error: Hello.'); expect(log).toEqual([ 'ErrorBoundary constructor', 'ErrorBoundary componentWillMount', 'ErrorBoundary render success', 'BrokenRenderErrorBoundary constructor', 'BrokenRenderErrorBoundary componentWillMount', 'BrokenRenderErrorBoundary render success', 'BrokenRender constructor', 'BrokenRender componentWillMount', 'BrokenRender render [!]', // Attempt to handle the error 'BrokenRenderErrorBoundary static getDerivedStateFromError', 'BrokenRenderErrorBoundary componentWillMount', 'BrokenRenderErrorBoundary render error [!]', // Attempt to handle the error again 'ErrorBoundary static getDerivedStateFromError', 'ErrorBoundary componentWillMount', 'ErrorBoundary render error', 'ErrorBoundary componentDidMount', ]); log.length = 0; ReactDOM.unmountComponentAtNode(container); expect(log).toEqual(['ErrorBoundary componentWillUnmount']); }); it('does not call componentWillUnmount when aborting initial mount', () => { const container = document.createElement('div'); ReactDOM.render( <ErrorBoundary> <Normal /> <BrokenRender /> <Normal /> </ErrorBoundary>, container, ); expect(container.firstChild.textContent).toBe('Caught an error: Hello.'); expect(log).toEqual([ 'ErrorBoundary constructor', 'ErrorBoundary componentWillMount', 'ErrorBoundary render success', // Render first child 'Normal constructor', 'Normal componentWillMount', 'Normal render', // Render second child (it throws) 'BrokenRender constructor', 'BrokenRender componentWillMount', 'BrokenRender render [!]', // Render third child, even though an earlier sibling threw. 'Normal constructor', 'Normal componentWillMount', 'Normal render', // Handle the error 'ErrorBoundary static getDerivedStateFromError', 'ErrorBoundary componentWillMount', 'ErrorBoundary render error', 'ErrorBoundary componentDidMount', ]); log.length = 0; ReactDOM.unmountComponentAtNode(container); expect(log).toEqual(['ErrorBoundary componentWillUnmount']); }); it('resets callback refs if mounting aborts', () => { function childRef(x) { log.push('Child ref is set to ' + x); } function errorMessageRef(x) { log.push('Error message ref is set to ' + x); } const container = document.createElement('div'); ReactDOM.render( <ErrorBoundary errorMessageRef={errorMessageRef}> <div ref={childRef} /> <BrokenRender /> </ErrorBoundary>, container, ); expect(container.textContent).toBe('Caught an error: Hello.'); expect(log).toEqual([ 'ErrorBoundary constructor', 'ErrorBoundary componentWillMount', 'ErrorBoundary render success', 'BrokenRender constructor', 'BrokenRender componentWillMount', 'BrokenRender render [!]', // Handle the error 'ErrorBoundary static getDerivedStateFromError', 'ErrorBoundary componentWillMount', 'ErrorBoundary render error', 'Error message ref is set to [object HTMLDivElement]', 'ErrorBoundary componentDidMount', ]); log.length = 0; ReactDOM.unmountComponentAtNode(container); expect(log).toEqual([ 'ErrorBoundary componentWillUnmount', 'Error message ref is set to null', ]); }); it('resets object refs if mounting aborts', () => { let childRef = React.createRef(); let errorMessageRef = React.createRef(); const container = document.createElement('div'); ReactDOM.render( <ErrorBoundary errorMessageRef={errorMessageRef}> <div ref={childRef} /> <BrokenRender /> </ErrorBoundary>, container, ); expect(container.textContent).toBe('Caught an error: Hello.'); expect(log).toEqual([ 'ErrorBoundary constructor', 'ErrorBoundary componentWillMount', 'ErrorBoundary render success', 'BrokenRender constructor', 'BrokenRender componentWillMount', 'BrokenRender render [!]', // Handle the error 'ErrorBoundary static getDerivedStateFromError', 'ErrorBoundary componentWillMount', 'ErrorBoundary render error', 'ErrorBoundary componentDidMount', ]); expect(errorMessageRef.current.toString()).toEqual( '[object HTMLDivElement]', ); log.length = 0; ReactDOM.unmountComponentAtNode(container); expect(log).toEqual(['ErrorBoundary componentWillUnmount']); expect(errorMessageRef.current).toEqual(null); }); it('successfully mounts if no error occurs', () => { const container = document.createElement('div'); ReactDOM.render( <ErrorBoundary> <div>Mounted successfully.</div> </ErrorBoundary>, container, ); expect(container.firstChild.textContent).toBe('Mounted successfully.'); expect(log).toEqual([ 'ErrorBoundary constructor', 'ErrorBoundary componentWillMount', 'ErrorBoundary render success', 'ErrorBoundary componentDidMount', ]); log.length = 0; ReactDOM.unmountComponentAtNode(container); expect(log).toEqual(['ErrorBoundary componentWillUnmount']); }); it('catches if child throws in constructor during update', () => { const container = document.createElement('div'); ReactDOM.render( <ErrorBoundary> <Normal /> </ErrorBoundary>, container, ); log.length = 0; ReactDOM.render( <ErrorBoundary> <Normal /> <Normal logName="Normal2" /> <BrokenConstructor /> </ErrorBoundary>, container, ); expect(container.textContent).toBe('Caught an error: Hello.'); expect(log).toEqual([ 'ErrorBoundary componentWillReceiveProps', 'ErrorBoundary componentWillUpdate', 'ErrorBoundary render success', 'Normal componentWillReceiveProps', 'Normal componentWillUpdate', 'Normal render', // Normal2 will attempt to mount: 'Normal2 constructor', 'Normal2 componentWillMount', 'Normal2 render', // BrokenConstructor will abort rendering: 'BrokenConstructor constructor [!]', // Handle the error 'ErrorBoundary static getDerivedStateFromError', // Render the error message 'ErrorBoundary componentWillUpdate', 'ErrorBoundary render error', 'Normal componentWillUnmount', 'ErrorBoundary componentDidUpdate', ]); log.length = 0; ReactDOM.unmountComponentAtNode(container); expect(log).toEqual(['ErrorBoundary componentWillUnmount']); }); it('catches if child throws in componentWillMount during update', () => { const container = document.createElement('div'); ReactDOM.render( <ErrorBoundary> <Normal /> </ErrorBoundary>, container, ); log.length = 0; ReactDOM.render( <ErrorBoundary> <Normal /> <Normal logName="Normal2" /> <BrokenComponentWillMount /> </ErrorBoundary>, container, ); expect(container.textContent).toBe('Caught an error: Hello.'); expect(log).toEqual([ 'ErrorBoundary componentWillReceiveProps', 'ErrorBoundary componentWillUpdate', 'ErrorBoundary render success', 'Normal componentWillReceiveProps', 'Normal componentWillUpdate', 'Normal render', // Normal2 will attempt to mount: 'Normal2 constructor', 'Normal2 componentWillMount', 'Normal2 render', // BrokenComponentWillMount will abort rendering: 'BrokenComponentWillMount constructor', 'BrokenComponentWillMount componentWillMount [!]', // Handle the error 'ErrorBoundary static getDerivedStateFromError', // Render the error message 'ErrorBoundary componentWillUpdate', 'ErrorBoundary render error', 'Normal componentWillUnmount', 'ErrorBoundary componentDidUpdate', ]); log.length = 0; ReactDOM.unmountComponentAtNode(container); expect(log).toEqual(['ErrorBoundary componentWillUnmount']); }); it('catches if child throws in componentWillReceiveProps during update', () => { const container = document.createElement('div'); ReactDOM.render( <ErrorBoundary> <Normal /> <BrokenComponentWillReceiveProps /> </ErrorBoundary>, container, ); log.length = 0; ReactDOM.render( <ErrorBoundary> <Normal /> <BrokenComponentWillReceiveProps /> </ErrorBoundary>, container, ); expect(container.textContent).toBe('Caught an error: Hello.'); expect(log).toEqual([ 'ErrorBoundary componentWillReceiveProps', 'ErrorBoundary componentWillUpdate', 'ErrorBoundary render success', 'Normal componentWillReceiveProps', 'Normal componentWillUpdate', 'Normal render', // BrokenComponentWillReceiveProps will abort rendering: 'BrokenComponentWillReceiveProps componentWillReceiveProps [!]', // Handle the error 'ErrorBoundary static getDerivedStateFromError', // Render the error message 'ErrorBoundary componentWillUpdate', 'ErrorBoundary render error', 'Normal componentWillUnmount', 'BrokenComponentWillReceiveProps componentWillUnmount', 'ErrorBoundary componentDidUpdate', ]); log.length = 0; ReactDOM.unmountComponentAtNode(container); expect(log).toEqual(['ErrorBoundary componentWillUnmount']); }); it('catches if child throws in componentWillUpdate during update', () => { const container = document.createElement('div'); ReactDOM.render( <ErrorBoundary> <Normal /> <BrokenComponentWillUpdate /> </ErrorBoundary>, container, ); log.length = 0; ReactDOM.render( <ErrorBoundary> <Normal /> <BrokenComponentWillUpdate /> </ErrorBoundary>, container, ); expect(container.textContent).toBe('Caught an error: Hello.'); expect(log).toEqual([ 'ErrorBoundary componentWillReceiveProps', 'ErrorBoundary componentWillUpdate', 'ErrorBoundary render success', 'Normal componentWillReceiveProps', 'Normal componentWillUpdate', 'Normal render', // BrokenComponentWillUpdate will abort rendering: 'BrokenComponentWillUpdate componentWillReceiveProps', 'BrokenComponentWillUpdate componentWillUpdate [!]', // Handle the error 'ErrorBoundary static getDerivedStateFromError', 'ErrorBoundary componentWillUpdate', 'ErrorBoundary render error', 'Normal componentWillUnmount', 'BrokenComponentWillUpdate componentWillUnmount', 'ErrorBoundary componentDidUpdate', ]); log.length = 0; ReactDOM.unmountComponentAtNode(container); expect(log).toEqual(['ErrorBoundary componentWillUnmount']); }); it('catches if child throws in render during update', () => { const container = document.createElement('div'); ReactDOM.render( <ErrorBoundary> <Normal /> </ErrorBoundary>, container, ); log.length = 0; ReactDOM.render( <ErrorBoundary> <Normal /> <Normal logName="Normal2" /> <BrokenRender /> </ErrorBoundary>, container, ); expect(container.textContent).toBe('Caught an error: Hello.'); expect(log).toEqual([ 'ErrorBoundary componentWillReceiveProps', 'ErrorBoundary componentWillUpdate', 'ErrorBoundary render success', 'Normal componentWillReceiveProps', 'Normal componentWillUpdate', 'Normal render', // Normal2 will attempt to mount: 'Normal2 constructor', 'Normal2 componentWillMount', 'Normal2 render', // BrokenRender will abort rendering: 'BrokenRender constructor', 'BrokenRender componentWillMount', 'BrokenRender render [!]', // Handle the error 'ErrorBoundary static getDerivedStateFromError', 'ErrorBoundary componentWillUpdate', 'ErrorBoundary render error', 'Normal componentWillUnmount', 'ErrorBoundary componentDidUpdate', ]); log.length = 0; ReactDOM.unmountComponentAtNode(container); expect(log).toEqual(['ErrorBoundary componentWillUnmount']); }); it('keeps refs up-to-date during updates', () => { function child1Ref(x) { log.push('Child1 ref is set to ' + x); } function child2Ref(x) { log.push('Child2 ref is set to ' + x); } function errorMessageRef(x) { log.push('Error message ref is set to ' + x); } const container = document.createElement('div'); ReactDOM.render( <ErrorBoundary errorMessageRef={errorMessageRef}> <div ref={child1Ref} /> </ErrorBoundary>, container, ); expect(log).toEqual([ 'ErrorBoundary constructor', 'ErrorBoundary componentWillMount', 'ErrorBoundary render success', 'Child1 ref is set to [object HTMLDivElement]', 'ErrorBoundary componentDidMount', ]); log.length = 0; ReactDOM.render( <ErrorBoundary errorMessageRef={errorMessageRef}> <div ref={child1Ref} /> <div ref={child2Ref} /> <BrokenRender /> </ErrorBoundary>, container, ); expect(container.textContent).toBe('Caught an error: Hello.'); expect(log).toEqual([ 'ErrorBoundary componentWillReceiveProps', 'ErrorBoundary componentWillUpdate', 'ErrorBoundary render success', // BrokenRender will abort rendering: 'BrokenRender constructor', 'BrokenRender componentWillMount', 'BrokenRender render [!]', // Handle the error 'ErrorBoundary static getDerivedStateFromError', 'ErrorBoundary componentWillUpdate', 'ErrorBoundary render error', // Update Child1 ref since Child1 has been unmounted // Child2 ref is never set because its mounting aborted 'Child1 ref is set to null', 'Error message ref is set to [object HTMLDivElement]', 'ErrorBoundary componentDidUpdate', ]); log.length = 0; ReactDOM.unmountComponentAtNode(container); expect(log).toEqual([ 'ErrorBoundary componentWillUnmount', 'Error message ref is set to null', ]); }); it('recovers from componentWillUnmount errors on update', () => { const container = document.createElement('div'); ReactDOM.render( <ErrorBoundary> <BrokenComponentWillUnmount /> <BrokenComponentWillUnmount /> <Normal /> </ErrorBoundary>, container, ); log.length = 0; ReactDOM.render( <ErrorBoundary> <BrokenComponentWillUnmount /> </ErrorBoundary>, container, ); expect(container.textContent).toBe('Caught an error: Hello.'); expect(log).toEqual([ 'ErrorBoundary componentWillReceiveProps', 'ErrorBoundary componentWillUpdate', 'ErrorBoundary render success', // Update existing child: 'BrokenComponentWillUnmount componentWillReceiveProps', 'BrokenComponentWillUnmount componentWillUpdate', 'BrokenComponentWillUnmount render', // Unmounting throws: 'BrokenComponentWillUnmount componentWillUnmount [!]', // Fiber proceeds with lifecycles despite errors 'Normal componentWillUnmount', // The components have updated in this phase 'BrokenComponentWillUnmount componentDidUpdate', 'ErrorBoundary componentDidUpdate', // The initial render was aborted, so // Fiber retries from the root. 'ErrorBoundary static getDerivedStateFromError', 'ErrorBoundary componentWillUpdate', 'ErrorBoundary render error', 'BrokenComponentWillUnmount componentWillUnmount [!]', 'ErrorBoundary componentDidUpdate', // The second willUnmount error should be captured and logged, too. 'ErrorBoundary static getDerivedStateFromError', 'ErrorBoundary componentWillUpdate', // Render an error now (stack will do it later) 'ErrorBoundary render error', // Attempt to unmount previous child: // Done 'ErrorBoundary componentDidUpdate', ]); log.length = 0; ReactDOM.unmountComponentAtNode(container); expect(log).toEqual(['ErrorBoundary componentWillUnmount']); }); it('recovers from nested componentWillUnmount errors on update', () => { const container = document.createElement('div'); ReactDOM.render( <ErrorBoundary> <Normal> <BrokenComponentWillUnmount /> </Normal> <BrokenComponentWillUnmount /> </ErrorBoundary>, container, ); log.length = 0; ReactDOM.render( <ErrorBoundary> <Normal> <BrokenComponentWillUnmount /> </Normal> </ErrorBoundary>, container, ); expect(container.textContent).toBe('Caught an error: Hello.'); expect(log).toEqual([ 'ErrorBoundary componentWillReceiveProps', 'ErrorBoundary componentWillUpdate', 'ErrorBoundary render success', // Update existing children: 'Normal componentWillReceiveProps', 'Normal componentWillUpdate', 'Normal render', 'BrokenComponentWillUnmount componentWillReceiveProps', 'BrokenComponentWillUnmount componentWillUpdate', 'BrokenComponentWillUnmount render', // Unmounting throws: 'BrokenComponentWillUnmount componentWillUnmount [!]', // Fiber proceeds with lifecycles despite errors 'BrokenComponentWillUnmount componentDidUpdate', 'Normal componentDidUpdate', 'ErrorBoundary componentDidUpdate', // Now that commit phase is done, Fiber handles errors 'ErrorBoundary static getDerivedStateFromError', 'ErrorBoundary componentWillUpdate', 'ErrorBoundary render error', 'Normal componentWillUnmount', 'BrokenComponentWillUnmount componentWillUnmount [!]', 'ErrorBoundary componentDidUpdate', // The second willUnmount error should be captured and logged, too. 'ErrorBoundary static getDerivedStateFromError', 'ErrorBoundary componentWillUpdate', // Render an error now (stack will do it later) 'ErrorBoundary render error', // Done 'ErrorBoundary componentDidUpdate', ]); log.length = 0; ReactDOM.unmountComponentAtNode(container); expect(log).toEqual(['ErrorBoundary componentWillUnmount']); }); it('picks the right boundary when handling unmounting errors', () => { 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'); ReactDOM.render( <ErrorBoundary logName="OuterErrorBoundary" renderError={renderOuterError}> <ErrorBoundary logName="InnerErrorBoundary" renderError={renderInnerError}> <BrokenComponentWillUnmount /> </ErrorBoundary> </ErrorBoundary>, container, ); log.length = 0; ReactDOM.render( <ErrorBoundary logName="OuterErrorBoundary" renderError={renderOuterError}> <ErrorBoundary logName="InnerErrorBoundary" renderError={renderInnerError} /> </ErrorBoundary>, container, ); expect(container.textContent).toBe('Caught an inner error: Hello.'); expect(log).toEqual([ // Update outer boundary 'OuterErrorBoundary componentWillReceiveProps', 'OuterErrorBoundary componentWillUpdate', 'OuterErrorBoundary render success', // Update inner boundary 'InnerErrorBoundary componentWillReceiveProps', 'InnerErrorBoundary componentWillUpdate', 'InnerErrorBoundary render success', // Try unmounting child 'BrokenComponentWillUnmount componentWillUnmount [!]', // Now that commit phase is done, Fiber handles errors // Only inner boundary receives the error: 'InnerErrorBoundary componentDidUpdate', 'OuterErrorBoundary componentDidUpdate', 'ErrorBoundary static getDerivedStateFromError', 'InnerErrorBoundary componentWillUpdate', // Render an error now 'InnerErrorBoundary render error', // In Fiber, this was a local update to the // inner boundary so only its hook fires 'InnerErrorBoundary componentDidUpdate', ]); log.length = 0; ReactDOM.unmountComponentAtNode(container); expect(log).toEqual([ 'OuterErrorBoundary componentWillUnmount', 'InnerErrorBoundary componentWillUnmount', ]); }); it('can recover from error state', () => { const container = document.createElement('div'); ReactDOM.render( <ErrorBoundary> <BrokenRender /> </ErrorBoundary>, container, ); ReactDOM.render( <ErrorBoundary> <Normal /> </ErrorBoundary>, contain