react-native
Version:
A framework for building native apps using React
1,115 lines (982 loc) • 32 kB
JavaScript
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @emails react-core
*/
;
var React;
var ReactNoop;
var ReactFeatureFlags;
describe('ReactIncrementalErrorHandling', () => {
beforeEach(() => {
jest.resetModules();
React = require('React');
ReactNoop = require('ReactNoop');
ReactFeatureFlags = require('ReactFeatureFlags');
ReactFeatureFlags.disableNewFiberFeatures = false;
});
function div(...children) {
children = children.map(c => typeof c === 'string' ? { text: c } : c);
return { type: 'div', children, prop: undefined };
}
function span(prop) {
return { type: 'span', children: [], prop };
}
it('catches render error in a boundary during full deferred mounting', () => {
class ErrorBoundary extends React.Component {
state = {error: null};
unstable_handleError(error) {
this.setState({error});
}
render() {
if (this.state.error) {
return <span prop={`Caught an error: ${this.state.error.message}.`} />;
}
return this.props.children;
}
}
function BrokenRender(props) {
throw new Error('Hello');
}
ReactNoop.render(
<ErrorBoundary>
<BrokenRender />
</ErrorBoundary>
);
ReactNoop.flushDeferredPri();
expect(ReactNoop.getChildren()).toEqual([span('Caught an error: Hello.')]);
});
it('catches render error in a boundary during partial deferred mounting', () => {
var ops = [];
class ErrorBoundary extends React.Component {
state = {error: null};
unstable_handleError(error) {
ops.push('ErrorBoundary unstable_handleError');
this.setState({error});
}
render() {
if (this.state.error) {
ops.push('ErrorBoundary render error');
return <span prop={`Caught an error: ${this.state.error.message}.`} />;
}
ops.push('ErrorBoundary render success');
return this.props.children;
}
}
function BrokenRender(props) {
ops.push('BrokenRender');
throw new Error('Hello');
}
ReactNoop.render(
<ErrorBoundary>
<BrokenRender />
</ErrorBoundary>
);
ReactNoop.flushDeferredPri(15);
expect(ops).toEqual([
'ErrorBoundary render success',
]);
expect(ReactNoop.getChildren()).toEqual([]);
ops.length = 0;
ReactNoop.flushDeferredPri(30);
expect(ops).toEqual([
'BrokenRender',
'ErrorBoundary unstable_handleError',
'ErrorBoundary render error',
]);
expect(ReactNoop.getChildren()).toEqual([span('Caught an error: Hello.')]);
});
it('catches render error in a boundary during animation mounting', () => {
var ops = [];
class ErrorBoundary extends React.Component {
state = {error: null};
unstable_handleError(error) {
ops.push('ErrorBoundary unstable_handleError');
this.setState({error});
}
render() {
if (this.state.error) {
ops.push('ErrorBoundary render error');
return <span prop={`Caught an error: ${this.state.error.message}.`} />;
}
ops.push('ErrorBoundary render success');
return this.props.children;
}
}
function BrokenRender(props) {
ops.push('BrokenRender');
throw new Error('Hello');
}
ReactNoop.performAnimationWork(() => {
ReactNoop.render(
<ErrorBoundary>
<BrokenRender />
</ErrorBoundary>
);
});
ReactNoop.flushAnimationPri();
expect(ops).toEqual([
'ErrorBoundary render success',
'BrokenRender',
'ErrorBoundary unstable_handleError',
'ErrorBoundary render error',
]);
expect(ReactNoop.getChildren()).toEqual([span('Caught an error: Hello.')]);
});
it('catches render error in a boundary during synchronous mounting', () => {
var ops = [];
class ErrorBoundary extends React.Component {
state = {error: null};
unstable_handleError(error) {
ops.push('ErrorBoundary unstable_handleError');
this.setState({error});
}
render() {
if (this.state.error) {
ops.push('ErrorBoundary render error');
return <span prop={`Caught an error: ${this.state.error.message}.`} />;
}
ops.push('ErrorBoundary render success');
return this.props.children;
}
}
function BrokenRender(props) {
ops.push('BrokenRender');
throw new Error('Hello');
}
ReactNoop.syncUpdates(() => {
ReactNoop.render(
<ErrorBoundary>
<BrokenRender />
</ErrorBoundary>
);
});
expect(ops).toEqual([
'ErrorBoundary render success',
'BrokenRender',
'ErrorBoundary unstable_handleError',
'ErrorBoundary render error',
]);
expect(ReactNoop.getChildren()).toEqual([span('Caught an error: Hello.')]);
});
it('catches render error in a boundary during batched mounting', () => {
var ops = [];
class ErrorBoundary extends React.Component {
state = {error: null};
unstable_handleError(error) {
ops.push('ErrorBoundary unstable_handleError');
this.setState({error});
}
render() {
if (this.state.error) {
ops.push('ErrorBoundary render error');
return <span prop={`Caught an error: ${this.state.error.message}.`} />;
}
ops.push('ErrorBoundary render success');
return this.props.children;
}
}
function BrokenRender(props) {
ops.push('BrokenRender');
throw new Error('Hello');
}
ReactNoop.syncUpdates(() => {
ReactNoop.batchedUpdates(() => {
ReactNoop.render(
<ErrorBoundary>
Before the storm.
</ErrorBoundary>
);
ReactNoop.render(
<ErrorBoundary>
<BrokenRender />
</ErrorBoundary>
);
});
});
expect(ops).toEqual([
'ErrorBoundary render success',
'BrokenRender',
'ErrorBoundary unstable_handleError',
'ErrorBoundary render error',
]);
expect(ReactNoop.getChildren()).toEqual([span('Caught an error: Hello.')]);
});
it('propagates an error from a noop error boundary during full deferred mounting', () => {
var ops = [];
class RethrowErrorBoundary extends React.Component {
unstable_handleError(error) {
ops.push('RethrowErrorBoundary unstable_handleError');
throw error;
}
render() {
ops.push('RethrowErrorBoundary render');
return this.props.children;
}
}
function BrokenRender() {
ops.push('BrokenRender');
throw new Error('Hello');
}
ReactNoop.render(
<RethrowErrorBoundary>
<BrokenRender />
</RethrowErrorBoundary>
);
expect(() => {
ReactNoop.flush();
}).toThrow('Hello');
expect(ops).toEqual([
'RethrowErrorBoundary render',
'BrokenRender',
'RethrowErrorBoundary unstable_handleError',
]);
expect(ReactNoop.getChildren()).toEqual([]);
});
it('propagates an error from a noop error boundary during partial deferred mounting', () => {
var ops = [];
class RethrowErrorBoundary extends React.Component {
unstable_handleError(error) {
ops.push('RethrowErrorBoundary unstable_handleError');
throw error;
}
render() {
ops.push('RethrowErrorBoundary render');
return this.props.children;
}
}
function BrokenRender() {
ops.push('BrokenRender');
throw new Error('Hello');
}
ReactNoop.render(
<RethrowErrorBoundary>
<BrokenRender />
</RethrowErrorBoundary>
);
ReactNoop.flushDeferredPri(15);
expect(ops).toEqual([
'RethrowErrorBoundary render',
]);
ops.length = 0;
expect(() => {
ReactNoop.flush();
}).toThrow('Hello');
expect(ops).toEqual([
'BrokenRender',
'RethrowErrorBoundary unstable_handleError',
]);
expect(ReactNoop.getChildren()).toEqual([]);
});
it('propagates an error from a noop error boundary during animation mounting', () => {
var ops = [];
class RethrowErrorBoundary extends React.Component {
unstable_handleError(error) {
ops.push('RethrowErrorBoundary unstable_handleError');
throw error;
}
render() {
ops.push('RethrowErrorBoundary render');
return this.props.children;
}
}
function BrokenRender() {
ops.push('BrokenRender');
throw new Error('Hello');
}
ReactNoop.performAnimationWork(() => {
ReactNoop.render(
<RethrowErrorBoundary>
<BrokenRender />
</RethrowErrorBoundary>
);
});
expect(() => {
ReactNoop.flush();
}).toThrow('Hello');
expect(ops).toEqual([
'RethrowErrorBoundary render',
'BrokenRender',
'RethrowErrorBoundary unstable_handleError',
]);
expect(ReactNoop.getChildren()).toEqual([]);
});
it('propagates an error from a noop error boundary during synchronous mounting', () => {
var ops = [];
class RethrowErrorBoundary extends React.Component {
unstable_handleError(error) {
ops.push('RethrowErrorBoundary unstable_handleError');
throw error;
}
render() {
ops.push('RethrowErrorBoundary render');
return this.props.children;
}
}
function BrokenRender() {
ops.push('BrokenRender');
throw new Error('Hello');
}
expect(() => {
ReactNoop.syncUpdates(() => {
ReactNoop.render(
<RethrowErrorBoundary>
<BrokenRender />
</RethrowErrorBoundary>
);
});
}).toThrow('Hello');
expect(ops).toEqual([
'RethrowErrorBoundary render',
'BrokenRender',
'RethrowErrorBoundary unstable_handleError',
]);
expect(ReactNoop.getChildren()).toEqual([]);
});
it('propagates an error from a noop error boundary during batched mounting', () => {
var ops = [];
class RethrowErrorBoundary extends React.Component {
unstable_handleError(error) {
ops.push('RethrowErrorBoundary unstable_handleError');
throw error;
}
render() {
ops.push('RethrowErrorBoundary render');
return this.props.children;
}
}
function BrokenRender() {
ops.push('BrokenRender');
throw new Error('Hello');
}
expect(() => {
ReactNoop.syncUpdates(() => {
ReactNoop.batchedUpdates(() => {
ReactNoop.render(
<RethrowErrorBoundary>
Before the storm.
</RethrowErrorBoundary>
);
ReactNoop.render(
<RethrowErrorBoundary>
<BrokenRender />
</RethrowErrorBoundary>
);
});
});
}).toThrow('Hello');
expect(ops).toEqual([
'RethrowErrorBoundary render',
'BrokenRender',
'RethrowErrorBoundary unstable_handleError',
]);
expect(ReactNoop.getChildren()).toEqual([]);
});
it('applies batched updates regardless despite errors in scheduling', () => {
ReactNoop.render(<span prop="a:1" />);
expect(() => {
ReactNoop.batchedUpdates(() => {
ReactNoop.render(<span prop="a:2" />);
ReactNoop.render(<span prop="a:3" />);
throw new Error('Hello');
});
}).toThrow('Hello');
ReactNoop.flush();
expect(ReactNoop.getChildren()).toEqual([span('a:3')]);
});
it('applies nested batched updates despite errors in scheduling', () => {
ReactNoop.render(<span prop="a:1" />);
expect(() => {
ReactNoop.batchedUpdates(() => {
ReactNoop.render(<span prop="a:2" />);
ReactNoop.render(<span prop="a:3" />);
ReactNoop.batchedUpdates(() => {
ReactNoop.render(<span prop="a:4" />);
ReactNoop.render(<span prop="a:5" />);
throw new Error('Hello');
});
});
}).toThrow('Hello');
ReactNoop.flush();
expect(ReactNoop.getChildren()).toEqual([span('a:5')]);
});
it('applies sync updates regardless despite errors in scheduling', () => {
ReactNoop.render(<span prop="a:1" />);
expect(() => {
ReactNoop.syncUpdates(() => {
ReactNoop.batchedUpdates(() => {
ReactNoop.render(<span prop="a:2" />);
ReactNoop.render(<span prop="a:3" />);
throw new Error('Hello');
});
});
}).toThrow('Hello');
expect(ReactNoop.getChildren()).toEqual([span('a:3')]);
});
it('can schedule updates after uncaught error in render on mount', () => {
var ops = [];
function BrokenRender() {
ops.push('BrokenRender');
throw new Error('Hello');
}
function Foo() {
ops.push('Foo');
return null;
}
ReactNoop.render(<BrokenRender />);
expect(() => {
ReactNoop.flush();
}).toThrow('Hello');
expect(ops).toEqual(['BrokenRender']);
ops = [];
ReactNoop.render(<Foo />);
ReactNoop.flush();
expect(ops).toEqual(['Foo']);
});
it('can schedule updates after uncaught error in render on update', () => {
var ops = [];
function BrokenRender(props) {
ops.push('BrokenRender');
if (props.throw) {
throw new Error('Hello');
}
return null;
}
function Foo() {
ops.push('Foo');
return null;
}
ReactNoop.render(<BrokenRender throw={false} />);
ReactNoop.flush();
ops = [];
expect(() => {
ReactNoop.render(<BrokenRender throw={true} />);
ReactNoop.flush();
}).toThrow('Hello');
expect(ops).toEqual(['BrokenRender']);
ops = [];
ReactNoop.render(<Foo />);
ReactNoop.flush();
expect(ops).toEqual(['Foo']);
});
it('can schedule updates after uncaught error during umounting', () => {
var ops = [];
class BrokenComponentWillUnmount extends React.Component {
render() {
return <div />;
}
componentWillUnmount() {
throw new Error('Hello');
}
}
function Foo() {
ops.push('Foo');
return null;
}
ReactNoop.render(<BrokenComponentWillUnmount />);
ReactNoop.flush();
expect(() => {
ReactNoop.render(<div />);
ReactNoop.flush();
}).toThrow('Hello');
ops = [];
ReactNoop.render(<Foo />);
ReactNoop.flush();
expect(ops).toEqual(['Foo']);
});
it('continues work on other roots despite caught errors', () => {
class ErrorBoundary extends React.Component {
state = {error: null};
unstable_handleError(error) {
this.setState({error});
}
render() {
if (this.state.error) {
return <span prop={`Caught an error: ${this.state.error.message}.`} />;
}
return this.props.children;
}
}
function BrokenRender(props) {
throw new Error('Hello');
}
ReactNoop.renderToRootWithID(
<ErrorBoundary>
<BrokenRender />
</ErrorBoundary>,
'a'
);
ReactNoop.renderToRootWithID(<span prop="b:1" />, 'b');
ReactNoop.flush();
expect(ReactNoop.getChildren('a')).toEqual([span('Caught an error: Hello.')]);
expect(ReactNoop.getChildren('b')).toEqual([span('b:1')]);
});
it('continues work on other roots despite uncaught errors', () => {
function BrokenRender(props) {
throw new Error('Hello');
}
ReactNoop.renderToRootWithID(<BrokenRender />, 'a');
expect(() => {
ReactNoop.flush();
}).toThrow('Hello');
expect(ReactNoop.getChildren('a')).toEqual([]);
ReactNoop.renderToRootWithID(<BrokenRender />, 'a');
ReactNoop.renderToRootWithID(<span prop="b:2" />, 'b');
expect(() => {
ReactNoop.flush();
}).toThrow('Hello');
expect(ReactNoop.getChildren('a')).toEqual([]);
expect(ReactNoop.getChildren('b')).toEqual([span('b:2')]);
ReactNoop.renderToRootWithID(<span prop="a:3" />, 'a');
ReactNoop.renderToRootWithID(<BrokenRender />, 'b');
expect(() => {
ReactNoop.flush();
}).toThrow('Hello');
expect(ReactNoop.getChildren('a')).toEqual([span('a:3')]);
expect(ReactNoop.getChildren('b')).toEqual([]);
ReactNoop.renderToRootWithID(<span prop="a:4" />, 'a');
ReactNoop.renderToRootWithID(<BrokenRender />, 'b');
ReactNoop.renderToRootWithID(<span prop="c:4" />, 'c');
expect(() => {
ReactNoop.flush();
}).toThrow('Hello');
expect(ReactNoop.getChildren('a')).toEqual([span('a:4')]);
expect(ReactNoop.getChildren('b')).toEqual([]);
expect(ReactNoop.getChildren('c')).toEqual([span('c:4')]);
ReactNoop.renderToRootWithID(<span prop="a:5" />, 'a');
ReactNoop.renderToRootWithID(<span prop="b:5" />, 'b');
ReactNoop.renderToRootWithID(<span prop="c:5" />, 'c');
ReactNoop.renderToRootWithID(<span prop="d:5" />, 'd');
ReactNoop.renderToRootWithID(<BrokenRender />, 'e');
expect(() => {
ReactNoop.flush();
}).toThrow('Hello');
expect(ReactNoop.getChildren('a')).toEqual([span('a:5')]);
expect(ReactNoop.getChildren('b')).toEqual([span('b:5')]);
expect(ReactNoop.getChildren('c')).toEqual([span('c:5')]);
expect(ReactNoop.getChildren('d')).toEqual([span('d:5')]);
expect(ReactNoop.getChildren('e')).toEqual([]);
ReactNoop.renderToRootWithID(<BrokenRender />, 'a');
ReactNoop.renderToRootWithID(<span prop="b:6" />, 'b');
ReactNoop.renderToRootWithID(<BrokenRender />, 'c');
ReactNoop.renderToRootWithID(<span prop="d:6" />, 'd');
ReactNoop.renderToRootWithID(<BrokenRender />, 'e');
ReactNoop.renderToRootWithID(<span prop="f:6" />, 'f');
expect(() => {
ReactNoop.flush();
}).toThrow('Hello');
expect(ReactNoop.getChildren('a')).toEqual([]);
expect(ReactNoop.getChildren('b')).toEqual([span('b:6')]);
expect(ReactNoop.getChildren('c')).toEqual([]);
expect(ReactNoop.getChildren('d')).toEqual([span('d:6')]);
expect(ReactNoop.getChildren('e')).toEqual([]);
expect(ReactNoop.getChildren('f')).toEqual([span('f:6')]);
ReactNoop.unmountRootWithID('a');
ReactNoop.unmountRootWithID('b');
ReactNoop.unmountRootWithID('c');
ReactNoop.unmountRootWithID('d');
ReactNoop.unmountRootWithID('e');
ReactNoop.unmountRootWithID('f');
ReactNoop.flush();
expect(ReactNoop.getChildren('a')).toEqual(null);
expect(ReactNoop.getChildren('b')).toEqual(null);
expect(ReactNoop.getChildren('c')).toEqual(null);
expect(ReactNoop.getChildren('d')).toEqual(null);
expect(ReactNoop.getChildren('e')).toEqual(null);
expect(ReactNoop.getChildren('f')).toEqual(null);
});
it('unwinds the context stack correctly on error', () => {
class Provider extends React.Component {
static childContextTypes = { message: React.PropTypes.string };
static contextTypes = { message: React.PropTypes.string };
getChildContext() {
return {
message: (this.context.message || '') + this.props.message,
};
}
render() {
return this.props.children;
}
}
function Connector(props, context) {
return <span prop={context.message} />;
}
Connector.contextTypes = {
message: React.PropTypes.string,
};
function BadRender() {
throw new Error('render error');
}
class Boundary extends React.Component {
state = { error: null };
unstable_handleError(error) {
this.setState({ error });
}
render() {
return (
<Provider message="b">
<Provider message="c">
<Provider message="d">
<Provider message="e">
{!this.state.error && <BadRender />}
</Provider>
</Provider>
</Provider>
</Provider>
);
}
}
ReactNoop.render(
<Provider message="a">
<Boundary />
<Connector />
</Provider>
);
ReactNoop.flush();
// If the context stack does not unwind, span will get 'abcde'
expect(ReactNoop.getChildren()).toEqual([
span('a'),
]);
});
it('catches reconciler errors in a boundary during mounting', () => {
spyOn(console, 'error');
class ErrorBoundary extends React.Component {
state = {error: null};
unstable_handleError(error) {
this.setState({error});
}
render() {
if (this.state.error) {
return <span prop={this.state.error.message} />;
}
return this.props.children;
}
}
const InvalidType = undefined;
function BrokenRender(props) {
return <InvalidType />;
}
ReactNoop.render(
<ErrorBoundary>
<BrokenRender />
</ErrorBoundary>
);
ReactNoop.flush();
expect(ReactNoop.getChildren()).toEqual([span(
'Element type is invalid: expected a string (for built-in components) or ' +
'a class/function (for composite components) but got: undefined. ' +
'You likely forgot to export your component from the file it\'s ' +
'defined in.\n\nCheck the render method of `BrokenRender`.'
)]);
expect(console.error.calls.count()).toBe(1);
});
it('catches reconciler errors in a boundary during update', () => {
spyOn(console, 'error');
class ErrorBoundary extends React.Component {
state = {error: null};
unstable_handleError(error) {
this.setState({error});
}
render() {
if (this.state.error) {
return <span prop={this.state.error.message} />;
}
return this.props.children;
}
}
const InvalidType = undefined;
function BrokenRender(props) {
return props.fail ? <InvalidType /> : <span />;
}
ReactNoop.render(
<ErrorBoundary>
<BrokenRender fail={false} />
</ErrorBoundary>
);
ReactNoop.flush();
ReactNoop.render(
<ErrorBoundary>
<BrokenRender fail={true} />
</ErrorBoundary>
);
ReactNoop.flush();
expect(ReactNoop.getChildren()).toEqual([span(
'Element type is invalid: expected a string (for built-in components) or ' +
'a class/function (for composite components) but got: undefined. ' +
'You likely forgot to export your component from the file it\'s ' +
'defined in.\n\nCheck the render method of `BrokenRender`.'
)]);
expect(console.error.calls.count()).toBe(1);
});
it('recovers from uncaught reconciler errors', () => {
spyOn(console, 'error');
const InvalidType = undefined;
ReactNoop.render(<InvalidType />);
expect(() => {
ReactNoop.flush();
}).toThrowError(
'Element type is invalid: expected a string (for built-in components) or ' +
'a class/function (for composite components) but got: undefined. ' +
'You likely forgot to export your component from the file it\'s ' +
'defined in.'
);
ReactNoop.render(<span prop="hi" />);
ReactNoop.flush();
expect(ReactNoop.getChildren()).toEqual([span('hi')]);
});
it('unmounts components with uncaught errors', () => {
const ops = [];
let inst;
class BrokenRenderAndUnmount extends React.Component {
state = {fail: false};
componentWillUnmount() {
ops.push('BrokenRenderAndUnmount componentWillUnmount');
}
render() {
inst = this;
if (this.state.fail) {
throw new Error('Hello.');
}
return null;
}
}
class Parent extends React.Component {
componentWillUnmount() {
ops.push('Parent componentWillUnmount [!]');
throw new Error('One does not simply unmount me.');
}
render() {
return this.props.children;
}
}
ReactNoop.render(
<Parent>
<Parent>
<BrokenRenderAndUnmount />
</Parent>
</Parent>
);
ReactNoop.flush();
inst.setState({fail: true});
expect(() => {
ReactNoop.flush();
}).toThrowError('Hello.');
expect(ops).toEqual([
// Attempt to clean up.
// Errors in parents shouldn't stop children from unmounting.
'Parent componentWillUnmount [!]',
'Parent componentWillUnmount [!]',
'BrokenRenderAndUnmount componentWillUnmount',
]);
expect(ReactNoop.getChildren()).toEqual([]);
});
it('does not interrupt unmounting if detaching a ref throws', () => {
var ops = [];
class Bar extends React.Component {
componentWillUnmount() {
ops.push('Bar unmount');
}
render() {
return <span prop="Bar" />;
}
}
function barRef(inst) {
if (inst === null) {
ops.push('barRef detach');
throw new Error('Detach error');
}
ops.push('barRef attach');
}
function Foo(props) {
return (
<div>
{props.hide ? null : <Bar ref={barRef} />}
</div>
);
}
ReactNoop.render(<Foo />);
ReactNoop.flush();
expect(ops).toEqual(['barRef attach']);
expect(ReactNoop.getChildren()).toEqual([
div(
span('Bar'),
),
]);
ops = [];
// Unmount
ReactNoop.render(<Foo hide={true} />);
expect(() => ReactNoop.flush()).toThrow('Detach error');
expect(ops).toEqual([
'barRef detach',
// Bar should unmount even though its ref threw an error while detaching
'Bar unmount',
]);
// Because there was an error, entire tree should unmount
expect(ReactNoop.getChildren()).toEqual([]);
});
it('handles error thrown by host config while working on failed root', () => {
ReactNoop.simulateErrorInHostConfig(() => {
ReactNoop.render(<span />);
expect(() => ReactNoop.flush()).toThrow('Error in host config.');
});
});
it('handles error thrown by top-level callback', () => {
ReactNoop.render(<div />, () => {
throw new Error('Error!');
});
expect(() => ReactNoop.flush()).toThrow('Error!');
});
describe('ReactFiberErrorLogger', () => {
function initReactFiberErrorLoggerMock(mock) {
jest.resetModules();
if (mock) {
jest.mock('ReactFiberErrorLogger');
} else {
jest.unmock('ReactFiberErrorLogger');
}
React = require('React');
ReactNoop = require('ReactNoop');
}
function normalizeCodeLocInfo(str) {
return str && str.replace(/\(at .+?:\d+\)/g, '(at **)');
}
it('should log errors that occur during the begin phase', () => {
initReactFiberErrorLoggerMock();
spyOn(console, 'error');
class ErrorThrowingComponent extends React.Component {
componentWillMount() {
throw Error('componentWillMount error');
}
render() {
return <div/>;
}
}
try {
ReactNoop.render(<div><span><ErrorThrowingComponent/></span></div>);
ReactNoop.flushDeferredPri();
} catch (error) {}
expect(console.error.calls.count()).toBe(1);
const errorMessage = console.error.calls.argsFor(0)[0];
expect(errorMessage).toContain(
'React caught an error thrown by ErrorThrowingComponent. ' +
'You should fix this error in your code. ' +
'Consider adding an error boundary to your tree to customize error handling behavior.'
);
expect(errorMessage).toContain('Error: componentWillMount error');
expect(normalizeCodeLocInfo(errorMessage)).toContain(
'The error is located at: \n' +
' in ErrorThrowingComponent (at **)\n' +
' in span (at **)\n' +
' in div (at **)'
);
});
it('should log errors that occur during the commit phase', () => {
initReactFiberErrorLoggerMock();
spyOn(console, 'error');
class ErrorThrowingComponent extends React.Component {
componentDidMount() {
throw Error('componentDidMount error');
}
render() {
return <div/>;
}
}
try {
ReactNoop.render(<div><span><ErrorThrowingComponent/></span></div>);
ReactNoop.flushDeferredPri();
} catch (error) {}
expect(console.error.calls.count()).toBe(1);
const errorMessage = console.error.calls.argsFor(0)[0];
expect(errorMessage).toContain(
'React caught an error thrown by ErrorThrowingComponent. ' +
'You should fix this error in your code. ' +
'Consider adding an error boundary to your tree to customize error handling behavior.'
);
expect(errorMessage).toContain('Error: componentDidMount error');
expect(normalizeCodeLocInfo(errorMessage)).toContain(
'The error is located at: \n' +
' in ErrorThrowingComponent (at **)\n' +
' in span (at **)\n' +
' in div (at **)'
);
});
it('should ignore errors thrown in log method to prevent cycle', () => {
initReactFiberErrorLoggerMock(true);
spyOn(console, 'error');
class ErrorThrowingComponent extends React.Component {
render() {
throw Error('render error');
}
}
const logCapturedErrorCalls = [];
const ReactFiberErrorLogger = require('ReactFiberErrorLogger');
ReactFiberErrorLogger.logCapturedError.mockImplementation(
(capturedError) => {
logCapturedErrorCalls.push(capturedError);
throw Error('logCapturedError error');
}
);
try {
ReactNoop.render(<div><span><ErrorThrowingComponent/></span></div>);
ReactNoop.flushDeferredPri();
} catch (error) {}
expect(logCapturedErrorCalls.length).toBe(1);
// The error thrown in logCapturedError should also be logged
expect(console.error.calls.count()).toBe(1);
expect(console.error.calls.argsFor(0)[0].message).toContain('logCapturedError error');
});
it('should relay info about error boundary and retry attempts if applicable', () => {
initReactFiberErrorLoggerMock();
spyOn(console, 'error');
class ParentComponent extends React.Component {
render() {
return <ErrorBoundaryComponent/>;
}
}
let handleErrorCalls = [];
let renderAttempts = 0;
class ErrorBoundaryComponent extends React.Component {
unstable_handleError(error) {
handleErrorCalls.push(error);
this.setState({}); // Render again
}
render() {
return <ErrorThrowingComponent/>;
}
}
class ErrorThrowingComponent extends React.Component {
componentDidMount() {
throw Error('componentDidMount error');
}
render() {
renderAttempts++;
return <div/>;
}
}
try {
ReactNoop.render(<ParentComponent/>);
ReactNoop.flush();
} catch (error) {}
expect(renderAttempts).toBe(2);
expect(handleErrorCalls.length).toBe(1);
expect(console.error.calls.count()).toBe(2);
expect(console.error.calls.argsFor(0)[0]).toContain(
'React caught an error thrown by ErrorThrowingComponent. ' +
'You should fix this error in your code. ' +
'React will try to recreate this component tree from scratch ' +
'using the error boundary you provided, ErrorBoundaryComponent.'
);
expect(console.error.calls.argsFor(1)[0]).toContain(
'React caught an error thrown by ErrorThrowingComponent. ' +
'You should fix this error in your code. ' +
'This error was initially handled by the error boundary ErrorBoundaryComponent. ' +
'Recreating the tree from scratch failed so React will unmount the tree.'
);
});
});
});