@samitouri / QOS-React / commits / bd3c90ac6f

Add more tests for error boundaries

Dan Abramov committed Oct 28, 2016 at 15:29 UTC bd3c90ac6f9b601e0c0b53ae38b0e8d4bbc5ac03
1 file changed +299 -162
src/renderers/shared/stack/reconciler/__tests__/ReactErrorBoundaries-test.js
+299 -162
@@ -28,6 +28,7 @@ describe('ReactErrorBoundaries', () => {
28 var BrokenComponentWillUnmount;
29 var BrokenRenderErrorBoundary;
30 var BrokenComponentWillMountErrorBoundary;
31 + var BrokenComponentDidMountErrorBoundary;
32 var BrokenRender;
33 var ErrorBoundary;
34 var ErrorMessage;
@@ -283,6 +284,36 @@ describe('ReactErrorBoundaries', () => {
284 }
285 };
286
287 + BrokenComponentDidMountErrorBoundary = class extends React.Component {
288 + constructor(props) {
289 + super(props);
290 + this.state = {error: null};
291 + log.push('BrokenComponentDidMountErrorBoundary constructor');
292 + }
293 + render() {
294 + if (this.state.error) {
295 + log.push('BrokenComponentDidMountErrorBoundary render error');
296 + return <div>Caught an error: {this.state.error.message}.</div>;
297 + }
298 + log.push('BrokenComponentDidMountErrorBoundary render success');
299 + return <div>{this.props.children}</div>;
300 + }
301 + componentWillMount() {
302 + log.push('BrokenComponentDidMountErrorBoundary componentWillMount');
303 + }
304 + componentDidMount() {
305 + log.push('BrokenComponentDidMountErrorBoundary componentDidMount [!]');
306 + throw new Error('Hello');
307 + }
308 + componentWillUnmount() {
309 + log.push('BrokenComponentDidMountErrorBoundary componentWillUnmount');
310 + }
311 + unstable_handleError(error) {
312 + log.push('BrokenComponentDidMountErrorBoundary unstable_handleError');
313 + this.setState({error});
314 + }
315 + };
316 +
317 BrokenRenderErrorBoundary = class extends React.Component {
318 constructor(props) {
319 super(props);
@@ -397,43 +428,44 @@ describe('ReactErrorBoundaries', () => {
428 };
429
430 ErrorBoundary = class extends React.Component {
400 - constructor() {
401 - super();
431 + constructor(props) {
432 + super(props);
433 this.state = {error: null};
403 - log.push('ErrorBoundary constructor');
434 + log.push(`${this.props.logName} constructor`);
435 }
436 render() {
437 if (this.state.error && !this.props.forceRetry) {
407 - log.push('ErrorBoundary render error');
438 + log.push(`${this.props.logName} render error`);
439 return this.props.renderError(this.state.error, this.props);
440 }
410 - log.push('ErrorBoundary render success');
441 + log.push(`${this.props.logName} render success`);
442 return <div>{this.props.children}</div>;
443 }
444 unstable_handleError(error) {
414 - log.push('ErrorBoundary unstable_handleError');
445 + log.push(`${this.props.logName} unstable_handleError`);
446 this.setState({error});
447 }
448 componentWillMount() {
418 - log.push('ErrorBoundary componentWillMount');
449 + log.push(`${this.props.logName} componentWillMount`);
450 }
451 componentDidMount() {
421 - log.push('ErrorBoundary componentDidMount');
452 + log.push(`${this.props.logName} componentDidMount`);
453 }
454 componentWillReceiveProps() {
424 - log.push('ErrorBoundary componentWillReceiveProps');
455 + log.push(`${this.props.logName} componentWillReceiveProps`);
456 }
457 componentWillUpdate() {
427 - log.push('ErrorBoundary componentWillUpdate');
458 + log.push(`${this.props.logName} componentWillUpdate`);
459 }
460 componentDidUpdate() {
430 - log.push('ErrorBoundary componentDidUpdate');
461 + log.push(`${this.props.logName} componentDidUpdate`);
462 }
463 componentWillUnmount() {
433 - log.push('ErrorBoundary componentWillUnmount');
464 + log.push(`${this.props.logName} componentWillUnmount`);
465 }
466 };
467 ErrorBoundary.defaultProps = {
468 + logName: 'ErrorBoundary',
469 renderError(error, props) {
470 return (
471 <div ref={props.errorMessageRef}>
@@ -464,53 +496,6 @@ describe('ReactErrorBoundaries', () => {
496 };
497 });
498
467 - if (ReactDOMFeatureFlags.useFiber) {
468 - // This test implements a new feature in Fiber.
469 - it('catches errors originating downstream', () => {
470 - var fail = false;
471 - class Stateful extends React.Component {
472 - state = {shouldThrow: false};
473 -
474 - render() {
475 - if (fail) {
476 - log.push('Stateful render [!]');
477 - throw new Error('Hello');
478 - }
479 - return <div>{this.props.children}</div>;
480 - }
481 - }
482 -
483 - var statefulInst;
484 - var container = document.createElement('div');
485 - ReactDOM.render(
486 - <ErrorBoundary>
487 - <Stateful ref={inst => statefulInst = inst} />
488 - </ErrorBoundary>,
489 - container
490 - );
491 -
492 - log.length = 0;
493 - expect(() => {
494 - fail = true;
495 - statefulInst.forceUpdate();
496 - }).not.toThrow();
497 -
498 - expect(log).toEqual([
499 - 'Stateful render [!]',
500 - 'ErrorBoundary unstable_handleError',
501 - 'ErrorBoundary componentWillUpdate',
502 - 'ErrorBoundary render error',
503 - 'ErrorBoundary componentDidUpdate',
504 - ]);
505 -
506 - log.length = 0;
507 - ReactDOM.unmountComponentAtNode(container);
508 - expect(log).toEqual([
509 - 'ErrorBoundary componentWillUnmount',
510 - ]);
511 - });
512 - }
513 -
499 it('renders an error state if child throws in render', () => {
500 var container = document.createElement('div');
501 ReactDOM.render(
@@ -661,65 +646,6 @@ describe('ReactErrorBoundaries', () => {
646 ]);
647 });
648
664 - if (ReactDOMFeatureFlags.useFiber) {
665 - // This test implements a new feature in Fiber.
666 - it('catches errors in componentDidMount', () => {
667 - var container = document.createElement('div');
668 - ReactDOM.render(
669 - <ErrorBoundary>
670 - <BrokenComponentWillUnmount>
671 - <Normal />
672 - </BrokenComponentWillUnmount>
673 - <BrokenComponentDidMount />
674 - <Normal logName="LastChild" />
675 - </ErrorBoundary>,
676 - container
677 - );
678 - expect(log).toEqual([
679 - 'ErrorBoundary constructor',
680 - 'ErrorBoundary componentWillMount',
681 - 'ErrorBoundary render success',
682 - 'BrokenComponentWillUnmount constructor',
683 - 'BrokenComponentWillUnmount componentWillMount',
684 - 'BrokenComponentWillUnmount render',
685 - 'Normal constructor',
686 - 'Normal componentWillMount',
687 - 'Normal render',
688 - 'BrokenComponentDidMount constructor',
689 - 'BrokenComponentDidMount componentWillMount',
690 - 'BrokenComponentDidMount render',
691 - 'LastChild constructor',
692 - 'LastChild componentWillMount',
693 - 'LastChild render',
694 - // Start flushing didMount queue
695 - 'Normal componentDidMount',
696 - 'BrokenComponentWillUnmount componentDidMount',
697 - 'BrokenComponentDidMount componentDidMount [!]',
698 - // Continue despite the error
699 - 'LastChild componentDidMount',
700 - 'ErrorBoundary componentDidMount',
701 - // Now we are ready to handle the error
702 - 'ErrorBoundary unstable_handleError',
703 - 'ErrorBoundary componentWillUpdate',
704 - 'ErrorBoundary render error',
705 - // Safely unmount every child
706 - 'BrokenComponentWillUnmount componentWillUnmount [!]',
707 - // Continue unmounting safely despite any errors
708 - 'Normal componentWillUnmount',
709 - 'BrokenComponentDidMount componentWillUnmount',
710 - 'LastChild componentWillUnmount',
711 - // The update has finished
712 - 'ErrorBoundary componentDidUpdate',
713 - ]);
714 -
715 - log.length = 0;
716 - ReactDOM.unmountComponentAtNode(container);
717 - expect(log).toEqual([
718 - 'ErrorBoundary componentWillUnmount',
719 - ]);
720 - });
721 - }
722 -
649 it('propagates errors on retry on mounting', () => {
650 var container = document.createElement('div');
651 ReactDOM.render(
@@ -1356,50 +1282,6 @@ describe('ReactErrorBoundaries', () => {
1282 ]);
1283 });
1284
1359 - if (ReactDOMFeatureFlags.useFiber) {
1360 - // This test implements a new feature in Fiber.
1361 - it('catches errors in componentDidUpdate', () => {
1362 - var container = document.createElement('div');
1363 - ReactDOM.render(
1364 - <ErrorBoundary>
1365 - <BrokenComponentDidUpdate />
1366 - </ErrorBoundary>,
1367 - container
1368 - );
1369 -
1370 - log.length = 0;
1371 - ReactDOM.render(
1372 - <ErrorBoundary>
1373 - <BrokenComponentDidUpdate />
1374 - </ErrorBoundary>,
1375 - container
1376 - );
1377 - expect(log).toEqual([
1378 - 'ErrorBoundary componentWillReceiveProps',
1379 - 'ErrorBoundary componentWillUpdate',
1380 - 'ErrorBoundary render success',
1381 - 'BrokenComponentDidUpdate componentWillReceiveProps',
1382 - 'BrokenComponentDidUpdate componentWillUpdate',
1383 - 'BrokenComponentDidUpdate render',
1384 - // All lifecycles run
1385 - 'BrokenComponentDidUpdate componentDidUpdate [!]',
1386 - 'ErrorBoundary componentDidUpdate',
1387 - // Then, error is handled
1388 - 'ErrorBoundary unstable_handleError',
1389 - 'ErrorBoundary componentWillUpdate',
1390 - 'ErrorBoundary render error',
1391 - 'BrokenComponentDidUpdate componentWillUnmount',
1392 - 'ErrorBoundary componentDidUpdate',
1393 - ]);
1394 -
1395 - log.length = 0;
1396 - ReactDOM.unmountComponentAtNode(container);
1397 - expect(log).toEqual([
1398 - 'ErrorBoundary componentWillUnmount',
1399 - ]);
1400 - });
1401 - }
1402 -
1285 it('recovers from componentWillUnmount errors on update', () => {
1286 var container = document.createElement('div');
1287 ReactDOM.render(
@@ -1535,6 +1417,76 @@ describe('ReactErrorBoundaries', () => {
1417 ]);
1418 });
1419
1420 + it('picks the right boundary when handling unmounting errors', () => {
1421 + function renderInnerError(error, props) {
1422 + return <div>Caught an inner error: {error.message}.</div>;
1423 + }
1424 + function renderOuterError(error, props) {
1425 + return <div>Caught an outer error: {error.message}.</div>;
1426 + }
1427 +
1428 + var container = document.createElement('div');
1429 + ReactDOM.render(
1430 + <ErrorBoundary logName="OuterErrorBoundary" renderError={renderOuterError}>
1431 + <ErrorBoundary logName="InnerErrorBoundary" renderError={renderInnerError}>
1432 + <BrokenComponentWillUnmount />
1433 + </ErrorBoundary>
1434 + </ErrorBoundary>,
1435 + container
1436 + );
1437 +
1438 + log.length = 0;
1439 + ReactDOM.render(
1440 + <ErrorBoundary logName="OuterErrorBoundary" renderError={renderOuterError}>
1441 + <ErrorBoundary logName="InnerErrorBoundary" renderError={renderInnerError} />
1442 + </ErrorBoundary>,
1443 + container
1444 + );
1445 + expect(container.textContent).toBe('Caught an inner error: Hello.');
1446 + expect(log).toEqual([
1447 + // Update outer boundary
1448 + 'OuterErrorBoundary componentWillReceiveProps',
1449 + 'OuterErrorBoundary componentWillUpdate',
1450 + 'OuterErrorBoundary render success',
1451 + // Update inner boundary
1452 + 'InnerErrorBoundary componentWillReceiveProps',
1453 + 'InnerErrorBoundary componentWillUpdate',
1454 + 'InnerErrorBoundary render success',
1455 + // Try unmounting child
1456 + 'BrokenComponentWillUnmount componentWillUnmount [!]',
1457 + ...(ReactDOMFeatureFlags.useFiber ? [
1458 + // Fiber proceeds with lifecycles despite errors
1459 + // Inner and outer boundaries have updated in this phase
1460 + 'InnerErrorBoundary componentDidUpdate',
1461 + 'OuterErrorBoundary componentDidUpdate',
1462 + // Now that commit phase is done, Fiber handles errors
1463 + // Only inner boundary receives the error:
1464 + 'InnerErrorBoundary unstable_handleError',
1465 + 'InnerErrorBoundary componentWillUpdate',
1466 + // Render an error now
1467 + 'InnerErrorBoundary render error',
1468 + // In Fiber, this was a local update to the
1469 + // inner boundary so only its hook fires
1470 + 'InnerErrorBoundary componentDidUpdate',
1471 + ] : [
1472 + // Stack will handle error immediately
1473 + 'InnerErrorBoundary unstable_handleError',
1474 + 'InnerErrorBoundary render error',
1475 + // In stack, this was a part of the update to the
1476 + // outer boundary so both lifecycles fire
1477 + 'InnerErrorBoundary componentDidUpdate',
1478 + 'OuterErrorBoundary componentDidUpdate',
1479 + ]),
1480 + ]);
1481 +
1482 + log.length = 0;
1483 + ReactDOM.unmountComponentAtNode(container);
1484 + expect(log).toEqual([
1485 + 'OuterErrorBoundary componentWillUnmount',
1486 + 'InnerErrorBoundary componentWillUnmount',
1487 + ]);
1488 + });
1489 +
1490 it('can recover from error state', () => {
1491 var container = document.createElement('div');
1492 ReactDOM.render(
@@ -1701,4 +1653,189 @@ describe('ReactErrorBoundaries', () => {
1653 'ErrorBoundary componentWillUnmount',
1654 ]);
1655 });
1656 +
1657 + // The tests below implement new features in Fiber.
1658 + if (ReactDOMFeatureFlags.useFiber) {
1659 + it('catches errors originating downstream', () => {
1660 + var fail = false;
1661 + class Stateful extends React.Component {
1662 + state = {shouldThrow: false};
1663 +
1664 + render() {
1665 + if (fail) {
1666 + log.push('Stateful render [!]');
1667 + throw new Error('Hello');
1668 + }
1669 + return <div>{this.props.children}</div>;
1670 + }
1671 + }
1672 +
1673 + var statefulInst;
1674 + var container = document.createElement('div');
1675 + ReactDOM.render(
1676 + <ErrorBoundary>
1677 + <Stateful ref={inst => statefulInst = inst} />
1678 + </ErrorBoundary>,
1679 + container
1680 + );
1681 +
1682 + log.length = 0;
1683 + expect(() => {
1684 + fail = true;
1685 + statefulInst.forceUpdate();
1686 + }).not.toThrow();
1687 +
1688 + expect(log).toEqual([
1689 + 'Stateful render [!]',
1690 + 'ErrorBoundary unstable_handleError',
1691 + 'ErrorBoundary componentWillUpdate',
1692 + 'ErrorBoundary render error',
1693 + 'ErrorBoundary componentDidUpdate',
1694 + ]);
1695 +
1696 + log.length = 0;
1697 + ReactDOM.unmountComponentAtNode(container);
1698 + expect(log).toEqual([
1699 + 'ErrorBoundary componentWillUnmount',
1700 + ]);
1701 + });
1702 +
1703 + it('catches errors in componentDidMount', () => {
1704 + var container = document.createElement('div');
1705 + ReactDOM.render(
1706 + <ErrorBoundary>
1707 + <BrokenComponentWillUnmount>
1708 + <Normal />
1709 + </BrokenComponentWillUnmount>
1710 + <BrokenComponentDidMount />
1711 + <Normal logName="LastChild" />
1712 + </ErrorBoundary>,
1713 + container
1714 + );
1715 + expect(log).toEqual([
1716 + 'ErrorBoundary constructor',
1717 + 'ErrorBoundary componentWillMount',
1718 + 'ErrorBoundary render success',
1719 + 'BrokenComponentWillUnmount constructor',
1720 + 'BrokenComponentWillUnmount componentWillMount',
1721 + 'BrokenComponentWillUnmount render',
1722 + 'Normal constructor',
1723 + 'Normal componentWillMount',
1724 + 'Normal render',
1725 + 'BrokenComponentDidMount constructor',
1726 + 'BrokenComponentDidMount componentWillMount',
1727 + 'BrokenComponentDidMount render',
1728 + 'LastChild constructor',
1729 + 'LastChild componentWillMount',
1730 + 'LastChild render',
1731 + // Start flushing didMount queue
1732 + 'Normal componentDidMount',
1733 + 'BrokenComponentWillUnmount componentDidMount',
1734 + 'BrokenComponentDidMount componentDidMount [!]',
1735 + // Continue despite the error
1736 + 'LastChild componentDidMount',
1737 + 'ErrorBoundary componentDidMount',
1738 + // Now we are ready to handle the error
1739 + 'ErrorBoundary unstable_handleError',
1740 + 'ErrorBoundary componentWillUpdate',
1741 + 'ErrorBoundary render error',
1742 + // Safely unmount every child
1743 + 'BrokenComponentWillUnmount componentWillUnmount [!]',
1744 + // Continue unmounting safely despite any errors
1745 + 'Normal componentWillUnmount',
1746 + 'BrokenComponentDidMount componentWillUnmount',
1747 + 'LastChild componentWillUnmount',
1748 + // The update has finished
1749 + 'ErrorBoundary componentDidUpdate',
1750 + ]);
1751 +
1752 + log.length = 0;
1753 + ReactDOM.unmountComponentAtNode(container);
1754 + expect(log).toEqual([
1755 + 'ErrorBoundary componentWillUnmount',
1756 + ]);
1757 + });
1758 +
1759 + it('propagates errors inside boundary during componentDidMount', () => {
1760 + var container = document.createElement('div');
1761 + ReactDOM.render(
1762 + <ErrorBoundary>
1763 + <BrokenComponentDidMountErrorBoundary
1764 + renderError={error => (
1765 + <div>
1766 + We should never catch our own error: {error.message}.
1767 + </div>
1768 + )} />
1769 + </ErrorBoundary>,
1770 + container
1771 + );
1772 + expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
1773 + expect(log).toEqual([
1774 + 'ErrorBoundary constructor',
1775 + 'ErrorBoundary componentWillMount',
1776 + 'ErrorBoundary render success',
1777 + 'BrokenComponentDidMountErrorBoundary constructor',
1778 + 'BrokenComponentDidMountErrorBoundary componentWillMount',
1779 + 'BrokenComponentDidMountErrorBoundary render success',
1780 + 'BrokenComponentDidMountErrorBoundary componentDidMount [!]',
1781 + // Fiber proceeds with the hooks
1782 + 'ErrorBoundary componentDidMount',
1783 + // The error propagates to the higher boundary
1784 + 'ErrorBoundary unstable_handleError',
1785 + // Fiber retries from the root
1786 + 'ErrorBoundary componentWillUpdate',
1787 + 'ErrorBoundary render error',
1788 + 'BrokenComponentDidMountErrorBoundary componentWillUnmount',
1789 + 'ErrorBoundary componentDidUpdate',
1790 + ]);
1791 +
1792 + log.length = 0;
1793 + ReactDOM.unmountComponentAtNode(container);
1794 + expect(log).toEqual([
1795 + 'ErrorBoundary componentWillUnmount',
1796 + ]);
1797 + });
1798 +
1799 + it('catches errors in componentDidUpdate', () => {
1800 + var container = document.createElement('div');
1801 + ReactDOM.render(
1802 + <ErrorBoundary>
1803 + <BrokenComponentDidUpdate />
1804 + </ErrorBoundary>,
1805 + container
1806 + );
1807 +
1808 + log.length = 0;
1809 + ReactDOM.render(
1810 + <ErrorBoundary>
1811 + <BrokenComponentDidUpdate />
1812 + </ErrorBoundary>,
1813 + container
1814 + );
1815 + expect(log).toEqual([
1816 + 'ErrorBoundary componentWillReceiveProps',
1817 + 'ErrorBoundary componentWillUpdate',
1818 + 'ErrorBoundary render success',
1819 + 'BrokenComponentDidUpdate componentWillReceiveProps',
1820 + 'BrokenComponentDidUpdate componentWillUpdate',
1821 + 'BrokenComponentDidUpdate render',
1822 + // All lifecycles run
1823 + 'BrokenComponentDidUpdate componentDidUpdate [!]',
1824 + 'ErrorBoundary componentDidUpdate',
1825 + // Then, error is handled
1826 + 'ErrorBoundary unstable_handleError',
1827 + 'ErrorBoundary componentWillUpdate',
1828 + 'ErrorBoundary render error',
1829 + 'BrokenComponentDidUpdate componentWillUnmount',
1830 + 'ErrorBoundary componentDidUpdate',
1831 + ]);
1832 +
1833 + log.length = 0;
1834 + ReactDOM.unmountComponentAtNode(container);
1835 + expect(log).toEqual([
1836 + 'ErrorBoundary componentWillUnmount',
1837 + ]);
1838 + });
1839 + }
1840 +
1841 });