import { setupRerender } from 'preact/test-utils'; import { createElement, render, Component, Fragment } from 'preact'; import { setupScratch, teardown } from '../../_util/helpers'; /** @jsx createElement */ describe('Lifecycle methods', () => { /* eslint-disable react/display-name */ /** @type {HTMLDivElement} */ let scratch; /** @type {() => void} */ let rerender; beforeEach(() => { scratch = setupScratch(); rerender = setupRerender(); }); afterEach(() => { teardown(scratch); }); describe('#componentDidCatch', () => { /** @type {Error} */ let expectedError; /** @type {typeof import('../../../').Component} */ let ThrowErr; class Receiver extends Component { componentDidCatch(error) { this.setState({ error }); } render() { return this.state.error ? String(this.state.error) : this.props.children; } } let thrower; sinon.spy(Receiver.prototype, 'componentDidCatch'); sinon.spy(Receiver.prototype, 'render'); function throwExpectedError() { throw (expectedError = new Error('Error!')); } beforeEach(() => { ThrowErr = class ThrowErr extends Component { constructor(props) { super(props); thrower = this; } componentDidCatch() { expect.fail("Throwing component should not catch it's own error."); } render() { return
ThrowErr: componentDidCatch
; } }; sinon.spy(ThrowErr.prototype, 'componentDidCatch'); expectedError = undefined; Receiver.prototype.componentDidCatch.resetHistory(); Receiver.prototype.render.resetHistory(); }); afterEach(() => { expect( ThrowErr.prototype.componentDidCatch, "Throwing component should not catch it's own error." ).to.not.be.called; thrower = undefined; }); it('should be called when child fails in constructor', () => { class ThrowErr extends Component { constructor(props, context) { super(props, context); throwExpectedError(); } componentDidCatch() { expect.fail("Throwing component should not catch it's own error"); } render() { return
; } } render( , scratch ); rerender(); expect(Receiver.prototype.componentDidCatch).to.have.been.calledWith( expectedError ); }); // https://github.com/preactjs/preact/issues/1570 it('should handle double child throws', () => { const Child = ({ i }) => { throw new Error(`error! ${i}`); }; const fn = () => render( {[1, 2].map(i => ( ))} , scratch ); expect(fn).to.not.throw(); rerender(); expect(scratch.innerHTML).to.equal('Error: error! 2'); }); it('should be called when child fails in componentWillMount', () => { ThrowErr.prototype.componentWillMount = throwExpectedError; render( , scratch ); expect(Receiver.prototype.componentDidCatch).to.have.been.calledWith( expectedError ); }); it('should be called when child fails in render', () => { ThrowErr.prototype.render = throwExpectedError; render( , scratch ); expect(Receiver.prototype.componentDidCatch).to.have.been.calledWith( expectedError ); }); it('should be called when child fails in componentDidMount', () => { ThrowErr.prototype.componentDidMount = throwExpectedError; render( , scratch ); expect(Receiver.prototype.componentDidCatch).to.have.been.calledWith( expectedError ); }); it('should be called when child fails in getDerivedStateFromProps', () => { ThrowErr.getDerivedStateFromProps = throwExpectedError; sinon.spy(ThrowErr.prototype, 'render'); render( , scratch ); expect(Receiver.prototype.componentDidCatch).to.have.been.calledWith( expectedError ); expect(ThrowErr.prototype.render).not.to.have.been.called; }); it('should be called when child fails in getSnapshotBeforeUpdate', () => { ThrowErr.prototype.getSnapshotBeforeUpdate = throwExpectedError; render( , scratch ); thrower.forceUpdate(); rerender(); expect(Receiver.prototype.componentDidCatch).to.have.been.calledWith( expectedError ); }); it('should be called when child fails in componentDidUpdate', () => { ThrowErr.prototype.componentDidUpdate = throwExpectedError; render( , scratch ); thrower.forceUpdate(); rerender(); expect(Receiver.prototype.componentDidCatch).to.have.been.calledWith( expectedError ); }); it('should be called when child fails in componentWillUpdate', () => { ThrowErr.prototype.componentWillUpdate = throwExpectedError; render( , scratch ); thrower.forceUpdate(); rerender(); expect(Receiver.prototype.componentDidCatch).to.have.been.calledWith( expectedError ); }); it('should be called when child fails in componentWillReceiveProps', () => { ThrowErr.prototype.componentWillReceiveProps = throwExpectedError; let receiver; class Receiver extends Component { constructor() { super(); this.state = { foo: 'bar' }; receiver = this; } componentDidCatch(error) { this.setState({ error }); } render() { return this.state.error ? ( String(this.state.error) ) : ( ); } } sinon.spy(Receiver.prototype, 'componentDidCatch'); render(, scratch); receiver.setState({ foo: 'baz' }); rerender(); expect(Receiver.prototype.componentDidCatch).to.have.been.calledWith( expectedError ); }); it('should be called when child fails in shouldComponentUpdate', () => { ThrowErr.prototype.shouldComponentUpdate = throwExpectedError; let receiver; class Receiver extends Component { constructor() { super(); this.state = { foo: 'bar' }; receiver = this; } componentDidCatch(error) { this.setState({ error }); } render() { return this.state.error ? ( String(this.state.error) ) : ( ); } } sinon.spy(Receiver.prototype, 'componentDidCatch'); render(, scratch); receiver.setState({ foo: 'baz' }); rerender(); expect(Receiver.prototype.componentDidCatch).to.have.been.calledWith( expectedError ); }); it('should be called when child fails in componentWillUnmount', () => { ThrowErr.prototype.componentWillUnmount = throwExpectedError; render( , scratch ); render(
, scratch ); expect(Receiver.prototype.componentDidCatch).to.have.been.calledWith( expectedError ); }); it('should be called when applying a Component ref', () => { const Foo = () =>
; const ref = value => { if (value) { throwExpectedError(); } }; // In React, an error boundary handles it's own refs: // https://codesandbox.io/s/react-throwing-refs-lk958 class Receiver extends Component { componentDidCatch(error) { this.setState({ error }); } render() { return this.state.error ? ( String(this.state.error) ) : ( ); } } sinon.spy(Receiver.prototype, 'componentDidCatch'); render(, scratch); expect(Receiver.prototype.componentDidCatch).to.have.been.calledWith( expectedError ); }); it('should be called when applying a DOM ref', () => { const ref = value => { if (value) { throwExpectedError(); } }; // In React, an error boundary handles it's own refs: // https://codesandbox.io/s/react-throwing-refs-lk958 class Receiver extends Component { componentDidCatch(error) { this.setState({ error }); } render() { return this.state.error ? ( String(this.state.error) ) : (
); } } sinon.spy(Receiver.prototype, 'componentDidCatch'); render(, scratch); expect(Receiver.prototype.componentDidCatch).to.have.been.calledWith( expectedError ); }); it('should be called when unmounting a ref', () => { const ref = value => { if (value == null) { throwExpectedError(); } }; ThrowErr.prototype.render = () =>
; render( , scratch ); render(
, scratch ); expect(Receiver.prototype.componentDidCatch).to.have.been.calledOnceWith( expectedError ); }); it('should be called when functional child fails', () => { function ThrowErr() { throwExpectedError(); } render( , scratch ); expect(Receiver.prototype.componentDidCatch).to.have.been.calledWith( expectedError ); }); it('should be called when child inside a Fragment fails', () => { function ThrowErr() { throwExpectedError(); } render( , scratch ); expect(Receiver.prototype.componentDidCatch).to.have.been.calledWith( expectedError ); }); it('should re-render with new content', () => { class ThrowErr extends Component { componentWillMount() { throw new Error('Error contents'); } render() { return 'No error!?!?'; } } render( , scratch ); rerender(); expect(scratch).to.have.property('textContent', 'Error: Error contents'); }); it('should be able to adapt and rethrow errors', () => { let adaptedError; class Adapter extends Component { componentDidCatch(error) { throw (adaptedError = new Error( 'Adapted ' + String(error && 'message' in error ? error.message : error) )); } render() { return
{this.props.children}
; } } function ThrowErr() { throwExpectedError(); } sinon.spy(Adapter.prototype, 'componentDidCatch'); render( , scratch ); expect(Adapter.prototype.componentDidCatch).to.have.been.calledWith( expectedError ); expect(Receiver.prototype.componentDidCatch).to.have.been.calledWith( adaptedError ); rerender(); expect(scratch).to.have.property('textContent', 'Error: Adapted Error!'); }); it('should bubble on repeated errors', () => { class Adapter extends Component { componentDidCatch(error) { // Try to handle the error this.setState({ error }); } render() { // But fail at doing so if (this.state.error) { throw this.state.error; } return
{this.props.children}
; } } function ThrowErr() { throwExpectedError(); } sinon.spy(Adapter.prototype, 'componentDidCatch'); render( , scratch ); rerender(); expect(Adapter.prototype.componentDidCatch).to.have.been.calledWith( expectedError ); expect(Receiver.prototype.componentDidCatch).to.have.been.calledWith( expectedError ); expect(scratch).to.have.property('textContent', 'Error: Error!'); }); it('should bubble on ignored errors', () => { class Adapter extends Component { componentDidCatch() { // Ignore the error } render() { return
{this.props.children}
; } } function ThrowErr() { throw new Error('Error!'); } sinon.spy(Adapter.prototype, 'componentDidCatch'); render( , scratch ); rerender(); expect(Adapter.prototype.componentDidCatch, 'Adapter').to.have.been .called; expect(Receiver.prototype.componentDidCatch, 'Receiver').to.have.been .called; expect(scratch).to.have.property('textContent', 'Error: Error!'); }); it('should not bubble on caught errors', () => { class TopReceiver extends Component { componentDidCatch(error) { this.setState({ error }); } render() { return (
{this.state.error ? String(this.state.error) : this.props.children}
); } } function ThrowErr() { throwExpectedError(); } sinon.spy(TopReceiver.prototype, 'componentDidCatch'); render( , scratch ); rerender(); expect(TopReceiver.prototype.componentDidCatch).not.to.have.been.called; expect(Receiver.prototype.componentDidCatch).to.have.been.calledWith( expectedError ); expect(scratch).to.have.property('textContent', 'Error: Error!'); }); it('should be called through non-component parent elements', () => { ThrowErr.prototype.render = throwExpectedError; render(
, scratch ); expect(Receiver.prototype.componentDidCatch).to.have.been.calledWith( expectedError ); }); it('should bubble up when ref throws on component that is not an error boundary', () => { const ref = value => { if (value) { throwExpectedError(); } }; function ThrowErr() { return
; } render( , scratch ); expect(Receiver.prototype.componentDidCatch).to.have.been.calledWith( expectedError ); }); it.skip('should successfully unmount constantly throwing ref', () => { const buggyRef = throwExpectedError; function ThrowErr() { return
ThrowErr
; } render( , scratch ); rerender(); expect(scratch.innerHTML).to.equal('
Error: Error!
'); }); }); });