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!
');
});
});
});