@samitouri / QOS-React-2 / commits / f33f03e357

Support passthrough updates for error boundaries (#7949)

* Initial pass at the easy case of updates (updates that start at the root). * Don't expect an extra componentWillUnmount call It was fixed in #6613. * Remove duplicate expectations from the test * Fix style issues * Make naming consistent throughout the tests * receiveComponent() does not accept safely argument * Assert that lifecycle and refs fire for error message * Add more tests for mounting * Do not call componentWillMount twice on error boundary * Document more of existing behavior in tests * Do not call componentWillUnmount() when aborting mounting Previously, we would call componentWillUnmount() safely on the tree whenever we abort mounting it. However this is likely risky because the tree was never mounted in the first place. People shouldn't hold resources in componentWillMount() so it's safe to say that we can skip componentWillUnmount() if componentDidMount() was never called. Here, we introduce a new flag. If we abort during mounting, we will not call componentWillUnmount(). However if we abort during an update, it is safe to call componentWillUnmount() because the previous tree has been mounted by now. * Consistently display error messages in tests * Add more logging to tests and remove redundant one * Refactor tests * Split complicated tests into smaller ones * Assert clean unmounting * Add assertions about update hooks * Add more tests to document existing behavior and remove irrelevant details * Verify we can recover from error state * Fix lint * Error in boundary’s componentWillMount should propagate up This test is currently failing. * Move calling componentWillMount() into mountComponent() This removes the unnecessary non-recursive skipLifecycle check. It fixes the previously failing test that verifies that if a boundary throws in its own componentWillMount(), the error will propagate. * Remove extra whitespace

Dan Abramov committed Oct 15, 2016 at 18:13 UTC f33f03e3572d11e6810f4ce110eb3af97cbd24a8
11 files changed +1574 -248
src/renderers/dom/client/ReactMount.js
+7 -4
@@ -170,11 +170,15 @@ function batchedMountComponentIntoNode(
170 * @internal
171 * @see {ReactMount.unmountComponentAtNode}
172 */
173 -function unmountComponentFromNode(instance, container, safely) {
173 +function unmountComponentFromNode(instance, container) {
174 if (__DEV__) {
175 ReactInstrumentation.debugTool.onBeginFlush();
176 }
177 - ReactReconciler.unmountComponent(instance, safely);
177 + ReactReconciler.unmountComponent(
178 + instance,
179 + false /* safely */,
180 + false /* skipLifecycle */
181 + );
182 if (__DEV__) {
183 ReactInstrumentation.debugTool.onEndFlush();
184 }
@@ -618,8 +622,7 @@ var ReactMount = {
622 ReactUpdates.batchedUpdates(
623 unmountComponentFromNode,
624 prevComponent,
621 - container,
622 - false
625 + container
626 );
627 return true;
628 },
src/renderers/dom/shared/ReactDOMComponent.js
+2 -2
@@ -1155,7 +1155,7 @@ ReactDOMComponent.Mixin = {
1155 *
1156 * @internal
1157 */
1158 - unmountComponent: function(safely) {
1158 + unmountComponent: function(safely, skipLifecycle) {
1159 switch (this._tag) {
1160 case 'audio':
1161 case 'form':
@@ -1197,7 +1197,7 @@ ReactDOMComponent.Mixin = {
1197 break;
1198 }
1199
1200 - this.unmountChildren(safely);
1200 + this.unmountChildren(safely, skipLifecycle);
1201 ReactDOMComponentTree.uncacheNode(this);
1202 EventPluginHub.deleteAllListeners(this);
1203 this._rootNodeID = 0;
src/renderers/native/ReactNativeBaseComponent.js
+2 -2
@@ -55,10 +55,10 @@ ReactNativeBaseComponent.Mixin = {
55 return this;
56 },
57
58 - unmountComponent: function() {
58 + unmountComponent: function(safely, skipLifecycle) {
59 ReactNativeComponentTree.uncacheNode(this);
60 deleteAllListeners(this);
61 - this.unmountChildren();
61 + this.unmountChildren(safely, skipLifecycle);
62 this._rootNodeID = 0;
63 },
64
src/renderers/shared/stack/reconciler/ReactChildReconciler.js
+12 -4
@@ -146,7 +146,11 @@ var ReactChildReconciler = {
146 } else {
147 if (prevChild) {
148 removedNodes[name] = ReactReconciler.getHostNode(prevChild);
149 - ReactReconciler.unmountComponent(prevChild, false);
149 + ReactReconciler.unmountComponent(
150 + prevChild,
151 + false, /* safely */
152 + false /* skipLifecycle */
153 + );
154 }
155 // The child must be instantiated before it's mounted.
156 var nextChildInstance = instantiateReactComponent(nextElement, true);
@@ -170,7 +174,11 @@ var ReactChildReconciler = {
174 !(nextChildren && nextChildren.hasOwnProperty(name))) {
175 prevChild = prevChildren[name];
176 removedNodes[name] = ReactReconciler.getHostNode(prevChild);
173 - ReactReconciler.unmountComponent(prevChild, false);
177 + ReactReconciler.unmountComponent(
178 + prevChild,
179 + false, /* safely */
180 + false /* skipLifecycle */
181 + );
182 }
183 }
184 },
@@ -182,11 +190,11 @@ var ReactChildReconciler = {
190 * @param {?object} renderedChildren Previously initialized set of children.
191 * @internal
192 */
185 - unmountChildren: function(renderedChildren, safely) {
193 + unmountChildren: function(renderedChildren, safely, skipLifecycle) {
194 for (var name in renderedChildren) {
195 if (renderedChildren.hasOwnProperty(name)) {
196 var renderedChild = renderedChildren[name];
189 - ReactReconciler.unmountComponent(renderedChild, safely);
197 + ReactReconciler.unmountComponent(renderedChild, safely, skipLifecycle);
198 }
199 }
200 },
src/renderers/shared/stack/reconciler/ReactCompositeComponent.js
+130 -37
@@ -333,6 +333,23 @@ var ReactCompositeComponent = {
333 this._pendingReplaceState = false;
334 this._pendingForceUpdate = false;
335
336 + if (inst.componentWillMount) {
337 + if (__DEV__) {
338 + measureLifeCyclePerf(
339 + () => inst.componentWillMount(),
340 + this._debugID,
341 + 'componentWillMount'
342 + );
343 + } else {
344 + inst.componentWillMount();
345 + }
346 + // When mounting, calls to `setState` by `componentWillMount` will set
347 + // `this._pendingStateQueue` without triggering a re-render.
348 + if (this._pendingStateQueue) {
349 + inst.state = this._processPendingState(inst.props, inst.context);
350 + }
351 + }
352 +
353 var markup;
354 if (inst.unstable_handleError) {
355 markup = this.performInitialMountWithErrorHandling(
@@ -343,7 +360,13 @@ var ReactCompositeComponent = {
360 context
361 );
362 } else {
346 - markup = this.performInitialMount(renderedElement, hostParent, hostContainerInfo, transaction, context);
363 + markup = this.performInitialMount(
364 + renderedElement,
365 + hostParent,
366 + hostContainerInfo,
367 + transaction,
368 + context
369 + );
370 }
371
372 if (inst.componentDidMount) {
@@ -434,7 +457,13 @@ var ReactCompositeComponent = {
457 var markup;
458 var checkpoint = transaction.checkpoint();
459 try {
437 - markup = this.performInitialMount(renderedElement, hostParent, hostContainerInfo, transaction, context);
460 + markup = this.performInitialMount(
461 + renderedElement,
462 + hostParent,
463 + hostContainerInfo,
464 + transaction,
465 + context
466 + );
467 } catch (e) {
468 // Roll back to checkpoint, handle error (which may add items to the transaction), and take a new checkpoint
469 transaction.rollback(checkpoint);
@@ -443,42 +472,33 @@ var ReactCompositeComponent = {
472 this._instance.state = this._processPendingState(this._instance.props, this._instance.context);
473 }
474 checkpoint = transaction.checkpoint();
446 -
447 - this._renderedComponent.unmountComponent(true);
475 + this._renderedComponent.unmountComponent(
476 + true, /* safely */
477 + // Don't call componentWillUnmount() because they never fully mounted:
478 + true /* skipLifecyle */
479 + );
480 transaction.rollback(checkpoint);
481
482 // Try again - we've informed the component about the error, so they can render an error message this time.
483 // If this throws again, the error will bubble up (and can be caught by a higher error boundary).
452 - markup = this.performInitialMount(renderedElement, hostParent, hostContainerInfo, transaction, context);
484 + markup = this.performInitialMount(
485 + renderedElement,
486 + hostParent,
487 + hostContainerInfo,
488 + transaction,
489 + context
490 + );
491 }
492 return markup;
493 },
494
457 - performInitialMount: function(renderedElement, hostParent, hostContainerInfo, transaction, context) {
458 - var inst = this._instance;
459 -
460 - var debugID = 0;
461 - if (__DEV__) {
462 - debugID = this._debugID;
463 - }
464 -
465 - if (inst.componentWillMount) {
466 - if (__DEV__) {
467 - measureLifeCyclePerf(
468 - () => inst.componentWillMount(),
469 - debugID,
470 - 'componentWillMount'
471 - );
472 - } else {
473 - inst.componentWillMount();
474 - }
475 - // When mounting, calls to `setState` by `componentWillMount` will set
476 - // `this._pendingStateQueue` without triggering a re-render.
477 - if (this._pendingStateQueue) {
478 - inst.state = this._processPendingState(inst.props, inst.context);
479 - }
480 - }
481 -
495 + performInitialMount: function(
496 + renderedElement,
497 + hostParent,
498 + hostContainerInfo,
499 + transaction,
500 + context
501 + ) {
502 // If not a stateless component, we now render
503 if (renderedElement === undefined) {
504 renderedElement = this._renderValidatedComponent();
@@ -492,6 +512,11 @@ var ReactCompositeComponent = {
512 );
513 this._renderedComponent = child;
514
515 + var debugID = 0;
516 + if (__DEV__) {
517 + debugID = this._debugID;
518 + }
519 +
520 var markup = ReactReconciler.mountComponent(
521 child,
522 transaction,
@@ -521,7 +546,7 @@ var ReactCompositeComponent = {
546 * @final
547 * @internal
548 */
524 - unmountComponent: function(safely) {
549 + unmountComponent: function(safely, skipLifecycle) {
550 if (!this._renderedComponent) {
551 return;
552 }
@@ -532,8 +557,10 @@ var ReactCompositeComponent = {
557 inst._calledComponentWillUnmount = true;
558
559 if (safely) {
535 - var name = this.getName() + '.componentWillUnmount()';
536 - ReactErrorUtils.invokeGuardedCallback(name, inst.componentWillUnmount.bind(inst));
560 + if (!skipLifecycle) {
561 + var name = this.getName() + '.componentWillUnmount()';
562 + ReactErrorUtils.invokeGuardedCallback(name, inst.componentWillUnmount.bind(inst));
563 + }
564 } else {
565 if (__DEV__) {
566 measureLifeCyclePerf(
@@ -548,7 +575,11 @@ var ReactCompositeComponent = {
575 }
576
577 if (this._renderedComponent) {
551 - ReactReconciler.unmountComponent(this._renderedComponent, safely);
578 + ReactReconciler.unmountComponent(
579 + this._renderedComponent,
580 + safely,
581 + skipLifecycle
582 + );
583 this._renderedNodeType = null;
584 this._renderedComponent = null;
585 this._instance = null;
@@ -941,7 +972,11 @@ var ReactCompositeComponent = {
972 inst.state = nextState;
973 inst.context = nextContext;
974
944 - this._updateRenderedComponent(transaction, unmaskedContext);
975 + if (inst.unstable_handleError) {
976 + this._updateRenderedComponentWithErrorHandling(transaction, unmaskedContext);
977 + } else {
978 + this._updateRenderedComponent(transaction, unmaskedContext);
979 + }
980
981 if (hasComponentDidUpdate) {
982 if (__DEV__) {
@@ -961,6 +996,40 @@ var ReactCompositeComponent = {
996 }
997 },
998
999 + /**
1000 + * Call the component's `render` method and update the DOM accordingly.
1001 + *
1002 + * @param {ReactReconcileTransaction} transaction
1003 + * @internal
1004 + */
1005 + _updateRenderedComponentWithErrorHandling: function(transaction, context) {
1006 + var checkpoint = transaction.checkpoint();
1007 + try {
1008 + this._updateRenderedComponent(transaction, context);
1009 + } catch (e) {
1010 + // Roll back to checkpoint, handle error (which may add items to the transaction),
1011 + // and take a new checkpoint
1012 + transaction.rollback(checkpoint);
1013 + this._instance.unstable_handleError(e);
1014 + if (this._pendingStateQueue) {
1015 + this._instance.state = this._processPendingState(this._instance.props, this._instance.context);
1016 + }
1017 + checkpoint = transaction.checkpoint();
1018 +
1019 + // Gracefully update to a clean state
1020 + this._updateRenderedComponentWithNextElement(
1021 + transaction,
1022 + context,
1023 + null,
1024 + true /* safely */
1025 + );
1026 +
1027 + // Try again - we've informed the component about the error, so they can render an error message this time.
1028 + // If this throws again, the error will bubble up (and can be caught by a higher error boundary).
1029 + this._updateRenderedComponent(transaction, context);
1030 + }
1031 + },
1032 +
1033 /**
1034 * Call the component's `render` method and update the DOM accordingly.
1035 *
@@ -968,9 +1037,29 @@ var ReactCompositeComponent = {
1037 * @internal
1038 */
1039 _updateRenderedComponent: function(transaction, context) {
1040 + var nextRenderedElement = this._renderValidatedComponent();
1041 + this._updateRenderedComponentWithNextElement(
1042 + transaction,
1043 + context,
1044 + nextRenderedElement,
1045 + false /* safely */
1046 + );
1047 + },
1048 +
1049 + /**
1050 + * Call the component's `render` method and update the DOM accordingly.
1051 + *
1052 + * @param {ReactReconcileTransaction} transaction
1053 + * @internal
1054 + */
1055 + _updateRenderedComponentWithNextElement: function(
1056 + transaction,
1057 + context,
1058 + nextRenderedElement,
1059 + safely
1060 + ) {
1061 var prevComponentInstance = this._renderedComponent;
1062 var prevRenderedElement = prevComponentInstance._currentElement;
973 - var nextRenderedElement = this._renderValidatedComponent();
1063
1064 var debugID = 0;
1065 if (__DEV__) {
@@ -986,7 +1075,11 @@ var ReactCompositeComponent = {
1075 );
1076 } else {
1077 var oldHostNode = ReactReconciler.getHostNode(prevComponentInstance);
989 - ReactReconciler.unmountComponent(prevComponentInstance, false);
1078 + ReactReconciler.unmountComponent(
1079 + prevComponentInstance,
1080 + safely,
1081 + false /* skipLifecycle */
1082 + );
1083
1084 var nodeType = ReactNodeTypes.getType(nextRenderedElement);
1085 this._renderedNodeType = nodeType;
src/renderers/shared/stack/reconciler/ReactMultiChild.js
+16 -4
@@ -289,7 +289,11 @@ var ReactMultiChild = {
289 updateTextContent: function(nextContent) {
290 var prevChildren = this._renderedChildren;
291 // Remove any rendered children.
292 - ReactChildReconciler.unmountChildren(prevChildren, false);
292 + ReactChildReconciler.unmountChildren(
293 + prevChildren,
294 + false, /* safely */
295 + false /* skipLifecycle */
296 + );
297 for (var name in prevChildren) {
298 if (prevChildren.hasOwnProperty(name)) {
299 invariant(false, 'updateTextContent called on non-empty component.');
@@ -309,7 +313,11 @@ var ReactMultiChild = {
313 updateMarkup: function(nextMarkup) {
314 var prevChildren = this._renderedChildren;
315 // Remove any rendered children.
312 - ReactChildReconciler.unmountChildren(prevChildren, false);
316 + ReactChildReconciler.unmountChildren(
317 + prevChildren,
318 + false, /* safely */
319 + false /* skipLifecycle */
320 + );
321 for (var name in prevChildren) {
322 if (prevChildren.hasOwnProperty(name)) {
323 invariant(false, 'updateTextContent called on non-empty component.');
@@ -423,9 +431,13 @@ var ReactMultiChild = {
431 *
432 * @internal
433 */
426 - unmountChildren: function(safely) {
434 + unmountChildren: function(safely, skipLifecycle) {
435 var renderedChildren = this._renderedChildren;
428 - ReactChildReconciler.unmountChildren(renderedChildren, safely);
436 + ReactChildReconciler.unmountChildren(
437 + renderedChildren,
438 + safely,
439 + skipLifecycle
440 + );
441 this._renderedChildren = null;
442 },
443
src/renderers/shared/stack/reconciler/ReactReconciler.js
+2 -2
@@ -93,7 +93,7 @@ var ReactReconciler = {
93 * @final
94 * @internal
95 */
96 - unmountComponent: function(internalInstance, safely) {
96 + unmountComponent: function(internalInstance, safely, skipLifecycle) {
97 if (__DEV__) {
98 if (internalInstance._debugID !== 0) {
99 ReactInstrumentation.debugTool.onBeforeUnmountComponent(
@@ -102,7 +102,7 @@ var ReactReconciler = {
102 }
103 }
104 ReactRef.detachRefs(internalInstance, internalInstance._currentElement);
105 - internalInstance.unmountComponent(safely);
105 + internalInstance.unmountComponent(safely, skipLifecycle);
106 if (__DEV__) {
107 if (internalInstance._debugID !== 0) {
108 ReactInstrumentation.debugTool.onUnmountComponent(
src/renderers/shared/stack/reconciler/ReactSimpleEmptyComponent.js
+6 -2
@@ -40,8 +40,12 @@ Object.assign(ReactSimpleEmptyComponent.prototype, {
40 getHostNode: function() {
41 return ReactReconciler.getHostNode(this._renderedComponent);
42 },
43 - unmountComponent: function() {
44 - ReactReconciler.unmountComponent(this._renderedComponent);
43 + unmountComponent: function(safely, skipLifecycle) {
44 + ReactReconciler.unmountComponent(
45 + this._renderedComponent,
46 + safely,
47 + skipLifecycle
48 + );
49 this._renderedComponent = null;
50 },
51 });
src/renderers/shared/stack/reconciler/__tests__/ReactErrorBoundaries-test.js
+1390 -189
@@ -15,291 +15,1492 @@ var React;
15 var ReactDOM;
16
17 describe('ReactErrorBoundaries', () => {
18 + var log;
19 +
20 + var BrokenConstructor;
21 + var BrokenComponentWillMount;
22 + var BrokenComponentDidMount;
23 + var BrokenComponentWillReceiveProps;
24 + var BrokenComponentWillUpdate;
25 + var BrokenComponentDidUpdate;
26 + var BrokenComponentWillUnmount;
27 + var BrokenRenderErrorBoundary;
28 + var BrokenComponentWillMountErrorBoundary;
29 + var BrokenRender;
30 + var ErrorBoundary;
31 + var ErrorMessage;
32 + var NoopErrorBoundary;
33 + var Normal;
34
35 beforeEach(() => {
36 ReactDOM = require('ReactDOM');
37 React = require('React');
22 - });
38
24 - it('does not register event handlers for unmounted children', () => {
25 - class Angry extends React.Component {
39 + log = [];
40 +
41 + BrokenConstructor = class extends React.Component {
42 + constructor(props) {
43 + super(props);
44 + log.push('BrokenConstructor constructor [!]');
45 + throw new Error('Hello');
46 + }
47 render() {
27 - throw new Error('Please, do not render me.');
48 + log.push('BrokenConstructor render');
49 + return <div />;
50 }
29 - }
51 + componentWillMount() {
52 + log.push('BrokenConstructor componentWillMount');
53 + }
54 + componentDidMount() {
55 + log.push('BrokenConstructor componentDidMount');
56 + }
57 + componentWillReceiveProps() {
58 + log.push('BrokenConstructor componentWillReceiveProps');
59 + }
60 + componentWillUpdate() {
61 + log.push('BrokenConstructor componentWillUpdate');
62 + }
63 + componentDidUpdate() {
64 + log.push('BrokenConstructor componentDidUpdate');
65 + }
66 + componentWillUnmount() {
67 + log.push('BrokenConstructor componentWillUnmount');
68 + }
69 + };
70
31 - class Boundary extends React.Component {
71 + BrokenComponentWillMount = class extends React.Component {
72 constructor(props) {
73 super(props);
34 - this.state = {error: false};
74 + log.push('BrokenComponentWillMount constructor');
75 }
76 render() {
37 - if (!this.state.error) {
38 - return (
39 - <div><button onClick={this.onClick}>ClickMe</button><Angry /></div>
40 - );
41 - } else {
42 - return <div>Happy Birthday!</div>;
43 - }
77 + log.push('BrokenComponentWillMount render');
78 + return <div />;
79 }
45 - onClick() {
46 - /* do nothing */
80 + componentWillMount() {
81 + log.push('BrokenComponentWillMount componentWillMount [!]');
82 + throw new Error('Hello');
83 }
48 - unstable_handleError() {
49 - this.setState({error: true});
84 + componentDidMount() {
85 + log.push('BrokenComponentWillMount componentDidMount');
86 }
51 - }
52 -
53 - var EventPluginHub = require('EventPluginHub');
54 - var container = document.createElement('div');
55 - EventPluginHub.putListener = jest.fn();
56 - ReactDOM.render(<Boundary />, container);
57 - expect(EventPluginHub.putListener).not.toBeCalled();
58 - });
87 + componentWillReceiveProps() {
88 + log.push('BrokenComponentWillMount componentWillReceiveProps');
89 + }
90 + componentWillUpdate() {
91 + log.push('BrokenComponentWillMount componentWillUpdate');
92 + }
93 + componentDidUpdate() {
94 + log.push('BrokenComponentWillMount componentDidUpdate');
95 + }
96 + componentWillUnmount() {
97 + log.push('BrokenComponentWillMount componentWillUnmount');
98 + }
99 + };
100
60 - it('renders an error state', () => {
61 - var log = [];
62 - class Angry extends React.Component {
101 + BrokenComponentDidMount = class extends React.Component {
102 + constructor(props) {
103 + super(props);
104 + log.push('BrokenComponentDidMount constructor');
105 + }
106 render() {
64 - log.push('Angry render');
65 - throw new Error('Please, do not render me.');
107 + log.push('BrokenComponentDidMount render');
108 + return <div />;
109 + }
110 + componentWillMount() {
111 + log.push('BrokenComponentDidMount componentWillMount');
112 }
113 componentDidMount() {
68 - log.push('Angry componentDidMount');
114 + log.push('BrokenComponentDidMount componentDidMount [!]');
115 + throw new Error('Hello');
116 + }
117 + componentWillReceiveProps() {
118 + log.push('BrokenComponentDidMount componentWillReceiveProps');
119 + }
120 + componentWillUpdate() {
121 + log.push('BrokenComponentDidMount componentWillUpdate');
122 + }
123 + componentDidUpdate() {
124 + log.push('BrokenComponentDidMount componentDidUpdate');
125 }
126 componentWillUnmount() {
71 - log.push('Angry componentWillUnmount');
127 + log.push('BrokenComponentDidMount componentWillUnmount');
128 }
73 - }
129 + };
130
75 - class Boundary extends React.Component {
131 + BrokenComponentWillReceiveProps = class extends React.Component {
132 constructor(props) {
133 super(props);
78 - this.state = {error: false};
134 + log.push('BrokenComponentWillReceiveProps constructor');
135 }
136 render() {
81 - log.push('Boundary render');
82 - if (!this.state.error) {
83 - return (
84 - <div><button onClick={this.onClick}>ClickMe</button><Angry /></div>
85 - );
86 - } else {
87 - return <div>Happy Birthday!</div>;
88 - }
137 + log.push('BrokenComponentWillReceiveProps render');
138 + return <div />;
139 + }
140 + componentWillMount() {
141 + log.push('BrokenComponentWillReceiveProps componentWillMount');
142 }
143 componentDidMount() {
91 - log.push('Boundary componentDidMount');
144 + log.push('BrokenComponentWillReceiveProps componentDidMount');
145 }
93 - componentWillUnmount() {
94 - log.push('Boundary componentWillUnmount');
146 + componentWillReceiveProps() {
147 + log.push('BrokenComponentWillReceiveProps componentWillReceiveProps [!]');
148 + throw new Error('Hello');
149 }
96 - onClick() {
97 - /* do nothing */
150 + componentWillUpdate() {
151 + log.push('BrokenComponentWillReceiveProps componentWillUpdate');
152 }
99 - unstable_handleError() {
100 - this.setState({error: true});
153 + componentDidUpdate() {
154 + log.push('BrokenComponentWillReceiveProps componentDidUpdate');
155 }
102 - }
103 -
104 - var container = document.createElement('div');
105 - ReactDOM.render(<Boundary />, container);
106 - expect(container.firstChild.innerHTML).toBe('Happy Birthday!');
107 - expect(log).toEqual([
108 - 'Boundary render',
109 - 'Angry render',
110 - 'Boundary render',
111 - 'Boundary componentDidMount',
112 - ]);
113 - });
114 -
115 - it('will catch exceptions in componentWillUnmount', () => {
116 - class ErrorBoundary extends React.Component {
117 - constructor() {
118 - super();
119 - this.state = {error: false};
156 + componentWillUnmount() {
157 + log.push('BrokenComponentWillReceiveProps componentWillUnmount');
158 }
159 + };
160
161 + BrokenComponentWillUpdate = class extends React.Component {
162 + constructor(props) {
163 + super(props);
164 + log.push('BrokenComponentWillUpdate constructor');
165 + }
166 render() {
123 - if (!this.state.error) {
124 - return <div>{this.props.children}</div>;
125 - }
126 - return <div>Error has been caught</div>;
167 + log.push('BrokenComponentWillUpdate render');
168 + return <div />;
169 }
128 -
129 - unstable_handleError() {
130 - this.setState({error: true});
170 + componentWillMount() {
171 + log.push('BrokenComponentWillUpdate componentWillMount');
172 }
132 - }
173 + componentDidMount() {
174 + log.push('BrokenComponentWillUpdate componentDidMount');
175 + }
176 + componentWillReceiveProps() {
177 + log.push('BrokenComponentWillUpdate componentWillReceiveProps');
178 + }
179 + componentWillUpdate() {
180 + log.push('BrokenComponentWillUpdate componentWillUpdate [!]');
181 + throw new Error('Hello');
182 + }
183 + componentDidUpdate() {
184 + log.push('BrokenComponentWillUpdate componentDidUpdate');
185 + }
186 + componentWillUnmount() {
187 + log.push('BrokenComponentWillUpdate componentWillUnmount');
188 + }
189 + };
190
134 - class BrokenRender extends React.Component {
191 + BrokenComponentDidUpdate = class extends React.Component {
192 + constructor(props) {
193 + super(props);
194 + log.push('BrokenComponentDidUpdate constructor');
195 + }
196 render() {
136 - throw new Error('Always broken.');
197 + log.push('BrokenComponentDidUpdate render');
198 + return <div />;
199 }
138 - }
200 + componentWillMount() {
201 + log.push('BrokenComponentDidUpdate componentWillMount');
202 + }
203 + componentDidMount() {
204 + log.push('BrokenComponentDidUpdate componentDidMount');
205 + }
206 + componentWillReceiveProps() {
207 + log.push('BrokenComponentDidUpdate componentWillReceiveProps');
208 + }
209 + componentWillUpdate() {
210 + log.push('BrokenComponentDidUpdate componentWillUpdate');
211 + }
212 + componentDidUpdate() {
213 + log.push('BrokenComponentDidUpdate componentDidUpdate [!]');
214 + throw new Error('Hello');
215 + }
216 + componentWillUnmount() {
217 + log.push('BrokenComponentDidUpdate componentWillUnmount');
218 + }
219 + };
220
140 - class BrokenUnmount extends React.Component {
221 + BrokenComponentWillUnmount = class extends React.Component {
222 + constructor(props) {
223 + super(props);
224 + log.push('BrokenComponentWillUnmount constructor');
225 + }
226 render() {
227 + log.push('BrokenComponentWillUnmount render');
228 return <div />;
229 }
230 + componentWillMount() {
231 + log.push('BrokenComponentWillUnmount componentWillMount');
232 + }
233 + componentDidMount() {
234 + log.push('BrokenComponentWillUnmount componentDidMount');
235 + }
236 + componentWillReceiveProps() {
237 + log.push('BrokenComponentWillUnmount componentWillReceiveProps');
238 + }
239 + componentWillUpdate() {
240 + log.push('BrokenComponentWillUnmount componentWillUpdate');
241 + }
242 + componentDidUpdate() {
243 + log.push('BrokenComponentWillUnmount componentDidUpdate');
244 + }
245 componentWillUnmount() {
145 - throw new Error('Always broken.');
246 + log.push('BrokenComponentWillUnmount componentWillUnmount [!]');
247 + throw new Error('Hello');
248 }
147 - }
148 -
149 - var container = document.createElement('div');
150 - ReactDOM.render(
151 - <ErrorBoundary>
152 - <BrokenUnmount />
153 - <BrokenRender />
154 - <BrokenUnmount />
155 - </ErrorBoundary>,
156 - container
157 - );
158 - ReactDOM.unmountComponentAtNode(container);
159 - });
249 + };
250
161 - it('expect uneventful render to succeed', () => {
162 - var log = [];
163 - class Boundary extends React.Component {
251 + BrokenComponentWillMountErrorBoundary = class extends React.Component {
252 constructor(props) {
253 super(props);
166 - this.state = {error: false};
254 + this.state = {error: null};
255 + log.push('BrokenComponentWillMountErrorBoundary constructor');
256 }
257 render() {
169 - log.push('Boundary render');
170 - return <div><button onClick={this.onClick}>ClickMe</button></div>;
258 + if (this.state.error) {
259 + log.push('BrokenComponentWillMountErrorBoundary render error');
260 + return <div>Caught an error: {this.state.error.message}.</div>;
261 + }
262 + log.push('BrokenComponentWillMountErrorBoundary render success');
263 + return <div>{this.props.children}</div>;
264 }
172 - onClick() {
173 - /* do nothing */
265 + componentWillMount() {
266 + log.push('BrokenComponentWillMountErrorBoundary componentWillMount [!]');
267 + throw new Error('Hello');
268 }
269 componentDidMount() {
176 - log.push('Boundary componentDidMount');
270 + log.push('BrokenComponentWillMountErrorBoundary componentDidMount');
271 }
272 componentWillUnmount() {
179 - log.push('Boundary componentWillUnmount');
273 + log.push('BrokenComponentWillMountErrorBoundary componentWillUnmount');
274 }
181 - unstable_handleError() {
182 - this.setState({error: true});
275 + unstable_handleError(error) {
276 + log.push('BrokenComponentWillMountErrorBoundary unstable_handleError');
277 + this.setState({error});
278 }
184 - }
185 -
186 - var container = document.createElement('div');
187 - ReactDOM.render(<Boundary />, container);
188 - expect(log).toEqual([
189 - 'Boundary render',
190 - 'Boundary componentDidMount',
191 - ]);
192 - });
279 + };
280
194 - it('correctly handles composite siblings', () => {
195 - class ErrorBoundary extends React.Component {
196 - constructor() {
197 - super();
198 - this.state = {error: false};
281 + BrokenRenderErrorBoundary = class extends React.Component {
282 + constructor(props) {
283 + super(props);
284 + this.state = {error: null};
285 + log.push('BrokenRenderErrorBoundary constructor');
286 }
200 -
287 render() {
202 - if (!this.state.error) {
203 - return <div>{this.props.children}</div>;
288 + if (this.state.error) {
289 + log.push('BrokenRenderErrorBoundary render error [!]');
290 + throw new Error('Hello');
291 }
205 - return <div>Error has been caught</div>;
292 + log.push('BrokenRenderErrorBoundary render success');
293 + return <div>{this.props.children}</div>;
294 }
207 -
208 - unstable_handleError() {
209 - this.setState({error: true});
295 + componentWillMount() {
296 + log.push('BrokenRenderErrorBoundary componentWillMount');
297 }
211 - }
212 -
213 - function Broken() {
214 - throw new Error('Always broken.');
215 - }
216 -
217 - function Composite() {
218 - return <div />;
219 - }
220 -
221 - var container = document.createElement('div');
222 - ReactDOM.render(
223 - <ErrorBoundary><Broken /><Composite /></ErrorBoundary>,
224 - container
225 - );
226 - ReactDOM.unmountComponentAtNode(container);
227 - });
298 + componentDidMount() {
299 + log.push('BrokenRenderErrorBoundary componentDidMount');
300 + }
301 + componentWillUnmount() {
302 + log.push('BrokenRenderErrorBoundary componentWillUnmount');
303 + }
304 + unstable_handleError(error) {
305 + log.push('BrokenRenderErrorBoundary unstable_handleError');
306 + this.setState({error});
307 + }
308 + };
309
229 - it('catches errors from children', () => {
230 - var log = [];
310 + BrokenRender = class extends React.Component {
311 + constructor(props) {
312 + super(props);
313 + log.push('BrokenRender constructor');
314 + }
315 + render() {
316 + log.push('BrokenRender render [!]');
317 + throw new Error('Hello');
318 + }
319 + componentWillMount() {
320 + log.push('BrokenRender componentWillMount');
321 + }
322 + componentDidMount() {
323 + log.push('BrokenRender componentDidMount');
324 + }
325 + componentWillReceiveProps() {
326 + log.push('BrokenRender componentWillReceiveProps');
327 + }
328 + componentWillUpdate() {
329 + log.push('BrokenRender componentWillUpdate');
330 + }
331 + componentDidUpdate() {
332 + log.push('BrokenRender componentDidUpdate');
333 + }
334 + componentWillUnmount() {
335 + log.push('BrokenRender componentWillUnmount');
336 + }
337 + };
338
232 - class Box extends React.Component {
339 + NoopErrorBoundary = class extends React.Component {
340 constructor(props) {
341 super(props);
235 - this.state = {errorMessage: null};
342 + log.push('NoopErrorBoundary constructor');
343 }
344 render() {
238 - if (this.state.errorMessage != null) {
239 - log.push('Box renderError');
240 - return <div>Error: {this.state.errorMessage}</div>;
241 - }
242 - log.push('Box render');
243 - var ref = function(x) {
244 - log.push('Inquisitive ref ' + x);
245 - };
246 - return (
247 - <div>
248 - <Inquisitive ref={ref} />
249 - <Angry />
250 - </div>
251 - );
345 + log.push('NoopErrorBoundary render');
346 + return <BrokenRender />;
347 }
253 - unstable_handleError(e) {
254 - this.setState({errorMessage: e.message});
348 + componentWillMount() {
349 + log.push('NoopErrorBoundary componentWillMount');
350 }
351 componentDidMount() {
257 - log.push('Box componentDidMount');
352 + log.push('NoopErrorBoundary componentDidMount');
353 }
354 componentWillUnmount() {
260 - log.push('Box componentWillUnmount');
355 + log.push('NoopErrorBoundary componentWillUnmount');
356 }
262 - }
357 + unstable_handleError() {
358 + log.push('NoopErrorBoundary unstable_handleError');
359 + }
360 + };
361
264 - class Inquisitive extends React.Component {
362 + Normal = class extends React.Component {
363 + static defaultProps = {
364 + logName: 'Normal',
365 + };
366 + constructor(props) {
367 + super(props);
368 + log.push(`${this.props.logName} constructor`);
369 + }
370 render() {
266 - log.push('Inquisitive render');
267 - return <div>What is love?</div>;
371 + log.push(`${this.props.logName} render`);
372 + return <div>{this.props.children}</div>;
373 + }
374 + componentWillMount() {
375 + log.push(`${this.props.logName} componentWillMount`);
376 }
377 componentDidMount() {
270 - log.push('Inquisitive componentDidMount');
378 + log.push(`${this.props.logName} componentDidMount`);
379 + }
380 + componentWillReceiveProps() {
381 + log.push(`${this.props.logName} componentWillReceiveProps`);
382 + }
383 + componentWillUpdate() {
384 + log.push(`${this.props.logName} componentWillUpdate`);
385 + }
386 + componentDidUpdate() {
387 + log.push(`${this.props.logName} componentDidUpdate`);
388 }
389 componentWillUnmount() {
273 - log.push('Inquisitive componentWillUnmount');
390 + log.push(`${this.props.logName} componentWillUnmount`);
391 }
275 - }
392 + };
393
277 - class Angry extends React.Component {
394 + ErrorBoundary = class extends React.Component {
395 + constructor() {
396 + super();
397 + this.state = {error: null};
398 + log.push('ErrorBoundary constructor');
399 + }
400 render() {
279 - log.push('Angry render');
280 - throw new Error('Please, do not render me.');
401 + if (this.state.error && !this.props.forceRetry) {
402 + log.push('ErrorBoundary render error');
403 + return this.props.renderError(this.state.error, this.props);
404 + }
405 + log.push('ErrorBoundary render success');
406 + return <div>{this.props.children}</div>;
407 + }
408 + unstable_handleError(error) {
409 + log.push('ErrorBoundary unstable_handleError');
410 + this.setState({error});
411 + }
412 + componentWillMount() {
413 + log.push('ErrorBoundary componentWillMount');
414 + }
415 + componentDidMount() {
416 + log.push('ErrorBoundary componentDidMount');
417 + }
418 + componentWillReceiveProps() {
419 + log.push('ErrorBoundary componentWillReceiveProps');
420 + }
421 + componentWillUpdate() {
422 + log.push('ErrorBoundary componentWillUpdate');
423 + }
424 + componentDidUpdate() {
425 + log.push('ErrorBoundary componentDidUpdate');
426 + }
427 + componentWillUnmount() {
428 + log.push('ErrorBoundary componentWillUnmount');
429 + }
430 + };
431 + ErrorBoundary.defaultProps = {
432 + renderError(error, props) {
433 + return (
434 + <div ref={props.errorMessageRef}>
435 + Caught an error: {error.message}.
436 + </div>
437 + );
438 + },
439 + };
440 +
441 + ErrorMessage = class extends React.Component {
442 + constructor(props) {
443 + super(props);
444 + log.push('ErrorMessage constructor');
445 + }
446 + componentWillMount() {
447 + log.push('ErrorMessage componentWillMount');
448 }
449 componentDidMount() {
283 - log.push('Angry componentDidMount');
450 + log.push('ErrorMessage componentDidMount');
451 }
452 componentWillUnmount() {
286 - log.push('Angry componentWillUnmount');
453 + log.push('ErrorMessage componentWillUnmount');
454 + }
455 + render() {
456 + log.push('ErrorMessage render');
457 + return <div>Caught an error: {this.props.message}.</div>;
458 + }
459 + };
460 + });
461 +
462 + // Known limitation: error boundary only "sees" errors caused by updates
463 + // flowing through it. This might be easier to fix in Fiber.
464 + it('currently does not catch errors originating downstream', () => {
465 + var fail = false;
466 + class Stateful extends React.Component {
467 + state = {shouldThrow: false};
468 +
469 + render() {
470 + if (fail) {
471 + log.push('Stateful render [!]');
472 + throw new Error('Hello');
473 + }
474 + return <div />;
475 }
476 }
477
478 + var statefulInst;
479 + var container = document.createElement('div');
480 + ReactDOM.render(
481 + <ErrorBoundary>
482 + <Stateful ref={inst => statefulInst = inst} />
483 + </ErrorBoundary>,
484 + container
485 + );
486 +
487 + log.length = 0;
488 + expect(() => {
489 + fail = true;
490 + statefulInst.forceUpdate();
491 + }).toThrow();
492 +
493 + expect(log).toEqual([
494 + 'Stateful render [!]',
495 + // FIXME: uncomment when downstream errors get caught.
496 + // Catch and render an error message
497 + // 'ErrorBoundary unstable_handleError',
498 + // 'ErrorBoundary render error',
499 + // 'ErrorBoundary componentDidUpdate',
500 + ]);
501 +
502 + log.length = 0;
503 + ReactDOM.unmountComponentAtNode(container);
504 + expect(log).toEqual([
505 + 'ErrorBoundary componentWillUnmount',
506 + ]);
507 + });
508 +
509 + it('renders an error state if child throws in render', () => {
510 + var container = document.createElement('div');
511 + ReactDOM.render(
512 + <ErrorBoundary>
513 + <BrokenRender />
514 + </ErrorBoundary>,
515 + container
516 + );
517 + expect(container.firstChild.textContent).toBe(
518 + 'Caught an error: Hello.'
519 + );
520 + expect(log).toEqual([
521 + 'ErrorBoundary constructor',
522 + 'ErrorBoundary componentWillMount',
523 + 'ErrorBoundary render success',
524 + 'BrokenRender constructor',
525 + 'BrokenRender componentWillMount',
526 + 'BrokenRender render [!]',
527 + // Catch and render an error message
528 + 'ErrorBoundary unstable_handleError',
529 + 'ErrorBoundary render error',
530 + 'ErrorBoundary componentDidMount',
531 + ]);
532 +
533 + log.length = 0;
534 + ReactDOM.unmountComponentAtNode(container);
535 + expect(log).toEqual([
536 + 'ErrorBoundary componentWillUnmount',
537 + ]);
538 + });
539 +
540 + it('renders an error state if child throws in constructor', () => {
541 + var container = document.createElement('div');
542 + ReactDOM.render(
543 + <ErrorBoundary>
544 + <BrokenConstructor />
545 + </ErrorBoundary>,
546 + container
547 + );
548 + expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
549 + expect(log).toEqual([
550 + 'ErrorBoundary constructor',
551 + 'ErrorBoundary componentWillMount',
552 + 'ErrorBoundary render success',
553 + 'BrokenConstructor constructor [!]',
554 + // Catch and render an error message
555 + 'ErrorBoundary unstable_handleError',
556 + 'ErrorBoundary render error',
557 + 'ErrorBoundary componentDidMount',
558 + ]);
559 +
560 + log.length = 0;
561 + ReactDOM.unmountComponentAtNode(container);
562 + expect(log).toEqual([
563 + 'ErrorBoundary componentWillUnmount',
564 + ]);
565 + });
566 +
567 + it('renders an error state if child throws in componentWillMount', () => {
568 var container = document.createElement('div');
291 - ReactDOM.render(<Box />, container);
292 - expect(container.textContent).toBe('Error: Please, do not render me.');
569 + ReactDOM.render(
570 + <ErrorBoundary>
571 + <BrokenComponentWillMount />
572 + </ErrorBoundary>,
573 + container
574 + );
575 + expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
576 + expect(log).toEqual([
577 + 'ErrorBoundary constructor',
578 + 'ErrorBoundary componentWillMount',
579 + 'ErrorBoundary render success',
580 + 'BrokenComponentWillMount constructor',
581 + 'BrokenComponentWillMount componentWillMount [!]',
582 + // Catch and render an error message
583 + 'ErrorBoundary unstable_handleError',
584 + 'ErrorBoundary render error',
585 + 'ErrorBoundary componentDidMount',
586 + ]);
587 +
588 + log.length = 0;
589 + ReactDOM.unmountComponentAtNode(container);
590 + expect(log).toEqual([
591 + 'ErrorBoundary componentWillUnmount',
592 + ]);
593 + });
594 +
595 + it('mounts the error message if mounting fails', () => {
596 + function renderError(error) {
597 + return (
598 + <ErrorMessage message={error.message} />
599 + );
600 + }
601 +
602 + var container = document.createElement('div');
603 + ReactDOM.render(
604 + <ErrorBoundary renderError={renderError}>
605 + <BrokenRender />
606 + </ErrorBoundary>,
607 + container
608 + );
609 + expect(log).toEqual([
610 + 'ErrorBoundary constructor',
611 + 'ErrorBoundary componentWillMount',
612 + 'ErrorBoundary render success',
613 + 'BrokenRender constructor',
614 + 'BrokenRender componentWillMount',
615 + 'BrokenRender render [!]',
616 + // Handle the error:
617 + 'ErrorBoundary unstable_handleError',
618 + 'ErrorBoundary render error',
619 + // Mount the error message:
620 + 'ErrorMessage constructor',
621 + 'ErrorMessage componentWillMount',
622 + 'ErrorMessage render',
623 + 'ErrorMessage componentDidMount',
624 + 'ErrorBoundary componentDidMount',
625 + ]);
626 +
627 + log.length = 0;
628 + ReactDOM.unmountComponentAtNode(container);
629 + expect(log).toEqual([
630 + 'ErrorBoundary componentWillUnmount',
631 + 'ErrorMessage componentWillUnmount',
632 + ]);
633 + });
634 +
635 + // Known limitation because componentDidMount() does not occur on the stack.
636 + // We could either hardcode searching for parent boundary, or wait for Fiber.
637 + it('currently does not catch errors in componentDidMount', () => {
638 + var container = document.createElement('div');
639 + expect(() => {
640 + ReactDOM.render(
641 + <ErrorBoundary>
642 + <BrokenComponentDidMount />
643 + </ErrorBoundary>,
644 + container
645 + );
646 + }).toThrow();
647 + expect(log).toEqual([
648 + 'ErrorBoundary constructor',
649 + 'ErrorBoundary componentWillMount',
650 + 'ErrorBoundary render success',
651 + 'BrokenComponentDidMount constructor',
652 + 'BrokenComponentDidMount componentWillMount',
653 + 'BrokenComponentDidMount render',
654 + 'BrokenComponentDidMount componentDidMount [!]',
655 + // FIXME: uncomment when componentDidMount() gets caught.
656 + // Catch and render an error message
657 + // 'ErrorBoundary unstable_handleError',
658 + // 'ErrorBoundary render error',
659 + // 'ErrorBoundary componentDidMount',
660 + ]);
661 +
662 + log.length = 0;
663 + ReactDOM.unmountComponentAtNode(container);
664 + expect(log).toEqual([
665 + 'ErrorBoundary componentWillUnmount',
666 + 'BrokenComponentDidMount componentWillUnmount',
667 + ]);
668 + });
669 +
670 + it('propagates errors on retry on mounting', () => {
671 + var container = document.createElement('div');
672 + ReactDOM.render(
673 + <ErrorBoundary>
674 + <NoopErrorBoundary>
675 + <BrokenRender />
676 + </NoopErrorBoundary>
677 + </ErrorBoundary>,
678 + container
679 + );
680 + expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
681 + expect(log).toEqual([
682 + 'ErrorBoundary constructor',
683 + 'ErrorBoundary componentWillMount',
684 + 'ErrorBoundary render success',
685 + 'NoopErrorBoundary constructor',
686 + 'NoopErrorBoundary componentWillMount',
687 + 'NoopErrorBoundary render',
688 + 'BrokenRender constructor',
689 + 'BrokenRender componentWillMount',
690 + 'BrokenRender render [!]',
691 + // The first error boundary catches the error
692 + // However, it doesn't adjust its state so next render also fails
693 + 'NoopErrorBoundary unstable_handleError',
694 + 'NoopErrorBoundary render',
695 + 'BrokenRender constructor',
696 + 'BrokenRender componentWillMount',
697 + 'BrokenRender render [!]',
698 + // This time, the error propagates to the higher boundary
699 + 'ErrorBoundary unstable_handleError',
700 + // Render the error
701 + 'ErrorBoundary render error',
702 + 'ErrorBoundary componentDidMount',
703 + ]);
704 +
705 + log.length = 0;
706 + ReactDOM.unmountComponentAtNode(container);
707 + expect(log).toEqual([
708 + 'ErrorBoundary componentWillUnmount',
709 + ]);
710 + });
711 +
712 + it('propagates errors inside boundary during componentWillMount', () => {
713 + var container = document.createElement('div');
714 + ReactDOM.render(
715 + <ErrorBoundary>
716 + <BrokenComponentWillMountErrorBoundary />
717 + </ErrorBoundary>,
718 + container
719 + );
720 + expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
721 + expect(log).toEqual([
722 + 'ErrorBoundary constructor',
723 + 'ErrorBoundary componentWillMount',
724 + 'ErrorBoundary render success',
725 + 'BrokenComponentWillMountErrorBoundary constructor',
726 + 'BrokenComponentWillMountErrorBoundary componentWillMount [!]',
727 + // The error propagates to the higher boundary
728 + 'ErrorBoundary unstable_handleError',
729 + // Render the error
730 + 'ErrorBoundary render error',
731 + 'ErrorBoundary componentDidMount',
732 + ]);
733 +
734 + log.length = 0;
735 + ReactDOM.unmountComponentAtNode(container);
736 + expect(log).toEqual([
737 + 'ErrorBoundary componentWillUnmount',
738 + ]);
739 + });
740 +
741 + it('propagates errors inside boundary while rendering error state', () => {
742 + var container = document.createElement('div');
743 + ReactDOM.render(
744 + <ErrorBoundary>
745 + <BrokenRenderErrorBoundary>
746 + <BrokenRender />
747 + </BrokenRenderErrorBoundary>
748 + </ErrorBoundary>,
749 + container
750 + );
751 + expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
752 + expect(log).toEqual([
753 + 'ErrorBoundary constructor',
754 + 'ErrorBoundary componentWillMount',
755 + 'ErrorBoundary render success',
756 + 'BrokenRenderErrorBoundary constructor',
757 + 'BrokenRenderErrorBoundary componentWillMount',
758 + 'BrokenRenderErrorBoundary render success',
759 + 'BrokenRender constructor',
760 + 'BrokenRender componentWillMount',
761 + 'BrokenRender render [!]',
762 + // The first error boundary catches the error
763 + // It adjusts state but throws displaying the message
764 + 'BrokenRenderErrorBoundary unstable_handleError',
765 + 'BrokenRenderErrorBoundary render error [!]',
766 + // The error propagates to the higher boundary
767 + 'ErrorBoundary unstable_handleError',
768 + // Render the error
769 + 'ErrorBoundary render error',
770 + 'ErrorBoundary componentDidMount',
771 + ]);
772 +
773 + log.length = 0;
774 + ReactDOM.unmountComponentAtNode(container);
775 + expect(log).toEqual([
776 + 'ErrorBoundary componentWillUnmount',
777 + ]);
778 + });
779 +
780 + it('does not register event handlers for unmounted children', () => {
781 + var EventPluginHub = require('EventPluginHub');
782 + var container = document.createElement('div');
783 + EventPluginHub.putListener = jest.fn();
784 + ReactDOM.render(
785 + <ErrorBoundary>
786 + <button onClick={() => {}}>Click me</button>
787 + <BrokenRender />
788 + </ErrorBoundary>,
789 + container
790 + );
791 + expect(EventPluginHub.putListener).not.toBeCalled();
792 +
793 + log.length = 0;
794 + ReactDOM.unmountComponentAtNode(container);
795 + expect(log).toEqual([
796 + 'ErrorBoundary componentWillUnmount',
797 + ]);
798 + });
799 +
800 + it('does not call componentWillUnmount when aborting initial mount', () => {
801 + var container = document.createElement('div');
802 + ReactDOM.render(
803 + <ErrorBoundary>
804 + <Normal />
805 + <BrokenRender />
806 + <Normal />
807 + </ErrorBoundary>,
808 + container
809 + );
810 + expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
811 + expect(log).toEqual([
812 + 'ErrorBoundary constructor',
813 + 'ErrorBoundary componentWillMount',
814 + 'ErrorBoundary render success',
815 + // Render first child
816 + 'Normal constructor',
817 + 'Normal componentWillMount',
818 + 'Normal render',
819 + // Render second child (it throws)
820 + 'BrokenRender constructor',
821 + 'BrokenRender componentWillMount',
822 + 'BrokenRender render [!]',
823 + // Error boundary catches the error
824 + 'ErrorBoundary unstable_handleError',
825 + // Render the error message
826 + 'ErrorBoundary render error',
827 + 'ErrorBoundary componentDidMount',
828 + ]);
829 +
830 + log.length = 0;
831 + ReactDOM.unmountComponentAtNode(container);
832 + expect(log).toEqual([
833 + 'ErrorBoundary componentWillUnmount',
834 + ]);
835 + });
836 +
837 + it('resets refs if mounting aborts', () => {
838 + function childRef(x) {
839 + log.push('Child ref is set to ' + x);
840 + }
841 + function errorMessageRef(x) {
842 + log.push('Error message ref is set to ' + x);
843 + }
844 +
845 + var container = document.createElement('div');
846 + ReactDOM.render(
847 + <ErrorBoundary errorMessageRef={errorMessageRef}>
848 + <div ref={childRef} />
849 + <BrokenRender />
850 + </ErrorBoundary>,
851 + container
852 + );
853 + expect(container.textContent).toBe('Caught an error: Hello.');
854 + expect(log).toEqual([
855 + 'ErrorBoundary constructor',
856 + 'ErrorBoundary componentWillMount',
857 + 'ErrorBoundary render success',
858 + 'BrokenRender constructor',
859 + 'BrokenRender componentWillMount',
860 + 'BrokenRender render [!]',
861 + // Handle error:
862 + 'ErrorBoundary unstable_handleError',
863 + // Child ref wasn't (and won't be) set but there's no harm in clearing:
864 + 'Child ref is set to null',
865 + 'ErrorBoundary render error',
866 + // Ref to error message should get set:
867 + 'Error message ref is set to [object HTMLDivElement]',
868 + 'ErrorBoundary componentDidMount',
869 + ]);
870 +
871 + log.length = 0;
872 + ReactDOM.unmountComponentAtNode(container);
873 + expect(log).toEqual([
874 + 'ErrorBoundary componentWillUnmount',
875 + 'Error message ref is set to null',
876 + ]);
877 + });
878 +
879 + it('successfully mounts if no error occurs', () => {
880 + var container = document.createElement('div');
881 + ReactDOM.render(
882 + <ErrorBoundary>
883 + <div>Mounted successfully.</div>
884 + </ErrorBoundary>,
885 + container
886 + );
887 + expect(container.firstChild.textContent).toBe('Mounted successfully.');
888 + expect(log).toEqual([
889 + 'ErrorBoundary constructor',
890 + 'ErrorBoundary componentWillMount',
891 + 'ErrorBoundary render success',
892 + 'ErrorBoundary componentDidMount',
893 + ]);
894 +
895 + log.length = 0;
896 + ReactDOM.unmountComponentAtNode(container);
897 + expect(log).toEqual([
898 + 'ErrorBoundary componentWillUnmount',
899 + ]);
900 + });
901 +
902 + it('catches if child throws in constructor during update', () => {
903 + var container = document.createElement('div');
904 + ReactDOM.render(
905 + <ErrorBoundary>
906 + <Normal />
907 + </ErrorBoundary>,
908 + container
909 + );
910 +
911 + log.length = 0;
912 + ReactDOM.render(
913 + <ErrorBoundary>
914 + <Normal />
915 + <Normal logName="Normal2" />
916 + <BrokenConstructor />
917 + </ErrorBoundary>,
918 + container
919 + );
920 + expect(container.textContent).toBe('Caught an error: Hello.');
921 + expect(log).toEqual([
922 + 'ErrorBoundary componentWillReceiveProps',
923 + 'ErrorBoundary componentWillUpdate',
924 + 'ErrorBoundary render success',
925 + 'Normal componentWillReceiveProps',
926 + 'Normal componentWillUpdate',
927 + 'Normal render',
928 + // Normal2 will attempt to mount:
929 + 'Normal2 constructor',
930 + 'Normal2 componentWillMount',
931 + 'Normal2 render',
932 + // BrokenConstructor will abort rendering:
933 + 'BrokenConstructor constructor [!]',
934 + 'ErrorBoundary unstable_handleError',
935 + // Unmount the previously mounted components:
936 + 'Normal componentWillUnmount',
937 + // Normal2 does not get lifefycle because it was never mounted
938 + 'ErrorBoundary render error',
939 + 'ErrorBoundary componentDidUpdate',
940 + ]);
941 +
942 + log.length = 0;
943 + ReactDOM.unmountComponentAtNode(container);
944 + expect(log).toEqual([
945 + 'ErrorBoundary componentWillUnmount',
946 + ]);
947 + });
948 +
949 + it('catches if child throws in componentWillMount during update', () => {
950 + var container = document.createElement('div');
951 + ReactDOM.render(
952 + <ErrorBoundary>
953 + <Normal />
954 + </ErrorBoundary>,
955 + container
956 + );
957 +
958 + log.length = 0;
959 + ReactDOM.render(
960 + <ErrorBoundary>
961 + <Normal />
962 + <Normal logName="Normal2" />
963 + <BrokenComponentWillMount />
964 + </ErrorBoundary>,
965 + container
966 + );
967 + expect(container.textContent).toBe('Caught an error: Hello.');
968 + expect(log).toEqual([
969 + 'ErrorBoundary componentWillReceiveProps',
970 + 'ErrorBoundary componentWillUpdate',
971 + 'ErrorBoundary render success',
972 + 'Normal componentWillReceiveProps',
973 + 'Normal componentWillUpdate',
974 + 'Normal render',
975 + // Normal2 will attempt to mount:
976 + 'Normal2 constructor',
977 + 'Normal2 componentWillMount',
978 + 'Normal2 render',
979 + // BrokenComponentWillMount will abort rendering:
980 + 'BrokenComponentWillMount constructor',
981 + 'BrokenComponentWillMount componentWillMount [!]',
982 + 'ErrorBoundary unstable_handleError',
983 + // Unmount the previously mounted components:
984 + 'Normal componentWillUnmount',
985 + // Normal2 does not get lifefycle because it was never mounted
986 + 'ErrorBoundary render error',
987 + 'ErrorBoundary componentDidUpdate',
988 + ]);
989 +
990 + log.length = 0;
991 + ReactDOM.unmountComponentAtNode(container);
992 + expect(log).toEqual([
993 + 'ErrorBoundary componentWillUnmount',
994 + ]);
995 + });
996 +
997 + it('catches if child throws in componentWillReceiveProps during update', () => {
998 + var container = document.createElement('div');
999 + ReactDOM.render(
1000 + <ErrorBoundary>
1001 + <Normal />
1002 + <BrokenComponentWillReceiveProps />
1003 + </ErrorBoundary>,
1004 + container
1005 + );
1006 +
1007 + log.length = 0;
1008 + ReactDOM.render(
1009 + <ErrorBoundary>
1010 + <Normal />
1011 + <BrokenComponentWillReceiveProps />
1012 + </ErrorBoundary>,
1013 + container
1014 + );
1015 + expect(container.textContent).toBe('Caught an error: Hello.');
1016 + expect(log).toEqual([
1017 + 'ErrorBoundary componentWillReceiveProps',
1018 + 'ErrorBoundary componentWillUpdate',
1019 + 'ErrorBoundary render success',
1020 + 'Normal componentWillReceiveProps',
1021 + 'Normal componentWillUpdate',
1022 + 'Normal render',
1023 + // BrokenComponentWillReceiveProps will abort rendering:
1024 + 'BrokenComponentWillReceiveProps componentWillReceiveProps [!]',
1025 + 'ErrorBoundary unstable_handleError',
1026 + // Unmount the previously mounted components:
1027 + 'Normal componentWillUnmount',
1028 + 'BrokenComponentWillReceiveProps componentWillUnmount',
1029 + // Render error:
1030 + 'ErrorBoundary render error',
1031 + 'ErrorBoundary componentDidUpdate',
1032 + ]);
1033 +
1034 + log.length = 0;
1035 + ReactDOM.unmountComponentAtNode(container);
1036 + expect(log).toEqual([
1037 + 'ErrorBoundary componentWillUnmount',
1038 + ]);
1039 + });
1040 +
1041 + it('catches if child throws in componentWillUpdate during update', () => {
1042 + var container = document.createElement('div');
1043 + ReactDOM.render(
1044 + <ErrorBoundary>
1045 + <Normal />
1046 + <BrokenComponentWillUpdate />
1047 + </ErrorBoundary>,
1048 + container
1049 + );
1050 +
1051 + log.length = 0;
1052 + ReactDOM.render(
1053 + <ErrorBoundary>
1054 + <Normal />
1055 + <BrokenComponentWillUpdate />
1056 + </ErrorBoundary>,
1057 + container
1058 + );
1059 + expect(container.textContent).toBe('Caught an error: Hello.');
1060 + expect(log).toEqual([
1061 + 'ErrorBoundary componentWillReceiveProps',
1062 + 'ErrorBoundary componentWillUpdate',
1063 + 'ErrorBoundary render success',
1064 + 'Normal componentWillReceiveProps',
1065 + 'Normal componentWillUpdate',
1066 + 'Normal render',
1067 + // BrokenComponentWillUpdate will abort rendering:
1068 + 'BrokenComponentWillUpdate componentWillReceiveProps',
1069 + 'BrokenComponentWillUpdate componentWillUpdate [!]',
1070 + 'ErrorBoundary unstable_handleError',
1071 + // Unmount the previously mounted components:
1072 + 'Normal componentWillUnmount',
1073 + 'BrokenComponentWillUpdate componentWillUnmount',
1074 + // Render error:
1075 + 'ErrorBoundary render error',
1076 + 'ErrorBoundary componentDidUpdate',
1077 + ]);
1078 +
1079 + log.length = 0;
1080 + ReactDOM.unmountComponentAtNode(container);
1081 + expect(log).toEqual([
1082 + 'ErrorBoundary componentWillUnmount',
1083 + ]);
1084 + });
1085 +
1086 + it('catches if child throws in render during update', () => {
1087 + var container = document.createElement('div');
1088 + ReactDOM.render(
1089 + <ErrorBoundary>
1090 + <Normal />
1091 + </ErrorBoundary>,
1092 + container
1093 + );
1094 +
1095 + log.length = 0;
1096 + ReactDOM.render(
1097 + <ErrorBoundary>
1098 + <Normal />
1099 + <Normal logName="Normal2" />
1100 + <BrokenRender />
1101 + </ErrorBoundary>,
1102 + container
1103 + );
1104 + expect(container.textContent).toBe('Caught an error: Hello.');
1105 + expect(log).toEqual([
1106 + 'ErrorBoundary componentWillReceiveProps',
1107 + 'ErrorBoundary componentWillUpdate',
1108 + 'ErrorBoundary render success',
1109 + 'Normal componentWillReceiveProps',
1110 + 'Normal componentWillUpdate',
1111 + 'Normal render',
1112 + // Normal2 will attempt to mount:
1113 + 'Normal2 constructor',
1114 + 'Normal2 componentWillMount',
1115 + 'Normal2 render',
1116 + // BrokenRender will abort rendering:
1117 + 'BrokenRender constructor',
1118 + 'BrokenRender componentWillMount',
1119 + 'BrokenRender render [!]',
1120 + 'ErrorBoundary unstable_handleError',
1121 + // Unmount the previously mounted components:
1122 + 'Normal componentWillUnmount',
1123 + // Normal2 does not get lifefycle because it was never mounted
1124 + 'ErrorBoundary render error',
1125 + 'ErrorBoundary componentDidUpdate',
1126 + ]);
1127 +
1128 + log.length = 0;
1129 + ReactDOM.unmountComponentAtNode(container);
1130 + expect(log).toEqual([
1131 + 'ErrorBoundary componentWillUnmount',
1132 + ]);
1133 + });
1134 +
1135 + it('keeps refs up-to-date during updates', () => {
1136 + function child1Ref(x) {
1137 + log.push('Child1 ref is set to ' + x);
1138 + }
1139 + function child2Ref(x) {
1140 + log.push('Child2 ref is set to ' + x);
1141 + }
1142 + function errorMessageRef(x) {
1143 + log.push('Error message ref is set to ' + x);
1144 + }
1145 +
1146 + var container = document.createElement('div');
1147 + ReactDOM.render(
1148 + <ErrorBoundary errorMessageRef={errorMessageRef}>
1149 + <div ref={child1Ref} />
1150 + </ErrorBoundary>,
1151 + container
1152 + );
1153 + expect(log).toEqual([
1154 + 'ErrorBoundary constructor',
1155 + 'ErrorBoundary componentWillMount',
1156 + 'ErrorBoundary render success',
1157 + 'Child1 ref is set to [object HTMLDivElement]',
1158 + 'ErrorBoundary componentDidMount',
1159 + ]);
1160 +
1161 + log.length = 0;
1162 + ReactDOM.render(
1163 + <ErrorBoundary errorMessageRef={errorMessageRef}>
1164 + <div ref={child1Ref} />
1165 + <div ref={child2Ref} />
1166 + <BrokenRender />
1167 + </ErrorBoundary>,
1168 + container
1169 + );
1170 + expect(container.textContent).toBe('Caught an error: Hello.');
1171 + expect(log).toEqual([
1172 + 'ErrorBoundary componentWillReceiveProps',
1173 + 'ErrorBoundary componentWillUpdate',
1174 + 'ErrorBoundary render success',
1175 + // BrokenRender will abort rendering:
1176 + 'BrokenRender constructor',
1177 + 'BrokenRender componentWillMount',
1178 + 'BrokenRender render [!]',
1179 + 'ErrorBoundary unstable_handleError',
1180 + // Unmount the previously mounted components:
1181 + 'Child1 ref is set to null',
1182 + 'ErrorBoundary render error',
1183 + 'Error message ref is set to [object HTMLDivElement]',
1184 + // Child2 ref is never set because its mounting aborted
1185 + 'ErrorBoundary componentDidUpdate',
1186 + ]);
1187 +
1188 + log.length = 0;
1189 + ReactDOM.unmountComponentAtNode(container);
1190 + expect(log).toEqual([
1191 + 'ErrorBoundary componentWillUnmount',
1192 + 'Error message ref is set to null',
1193 + ]);
1194 + });
1195 +
1196 + // Known limitation because componentDidUpdate() does not occur on the stack.
1197 + // We could either hardcode searching for parent boundary, or wait for Fiber.
1198 + it('currently does not catch errors in componentDidUpdate', () => {
1199 + var container = document.createElement('div');
1200 + ReactDOM.render(
1201 + <ErrorBoundary>
1202 + <BrokenComponentDidUpdate />
1203 + </ErrorBoundary>,
1204 + container
1205 + );
1206 +
1207 + log.length = 0;
1208 + expect(() => {
1209 + ReactDOM.render(
1210 + <ErrorBoundary>
1211 + <BrokenComponentDidUpdate />
1212 + </ErrorBoundary>,
1213 + container
1214 + );
1215 + }).toThrow();
1216 + expect(log).toEqual([
1217 + 'ErrorBoundary componentWillReceiveProps',
1218 + 'ErrorBoundary componentWillUpdate',
1219 + 'ErrorBoundary render success',
1220 + 'BrokenComponentDidUpdate componentWillReceiveProps',
1221 + 'BrokenComponentDidUpdate componentWillUpdate',
1222 + 'BrokenComponentDidUpdate render',
1223 + 'BrokenComponentDidUpdate componentDidUpdate [!]',
1224 + // FIXME: uncomment when componentDidUpdate() gets caught.
1225 + // Catch and render an error message
1226 + // 'ErrorBoundary unstable_handleError',
1227 + // 'ErrorBoundary render error',
1228 + // 'ErrorBoundary componentDidUpdate',
1229 + ]);
1230 +
1231 + log.length = 0;
1232 + ReactDOM.unmountComponentAtNode(container);
1233 + expect(log).toEqual([
1234 + 'ErrorBoundary componentWillUnmount',
1235 + 'BrokenComponentDidUpdate componentWillUnmount',
1236 + ]);
1237 + });
1238 +
1239 + it('recovers from componentWillUnmount errors on update', () => {
1240 + var container = document.createElement('div');
1241 + ReactDOM.render(
1242 + <ErrorBoundary>
1243 + <BrokenComponentWillUnmount />
1244 + <BrokenComponentWillUnmount />
1245 + <BrokenComponentWillUnmount />
1246 + </ErrorBoundary>,
1247 + container
1248 + );
1249 +
1250 + log.length = 0;
1251 + ReactDOM.render(
1252 + <ErrorBoundary>
1253 + <BrokenComponentWillUnmount />
1254 + <BrokenComponentWillUnmount />
1255 + </ErrorBoundary>,
1256 + container
1257 + );
1258 + expect(container.textContent).toBe('Caught an error: Hello.');
1259 + expect(log).toEqual([
1260 + 'ErrorBoundary componentWillReceiveProps',
1261 + 'ErrorBoundary componentWillUpdate',
1262 + 'ErrorBoundary render success',
1263 + // Update existing children:
1264 + 'BrokenComponentWillUnmount componentWillReceiveProps',
1265 + 'BrokenComponentWillUnmount componentWillUpdate',
1266 + 'BrokenComponentWillUnmount render',
1267 + 'BrokenComponentWillUnmount componentWillReceiveProps',
1268 + 'BrokenComponentWillUnmount componentWillUpdate',
1269 + 'BrokenComponentWillUnmount render',
1270 + // Unmounting throws:
1271 + 'BrokenComponentWillUnmount componentWillUnmount [!]',
1272 + 'ErrorBoundary unstable_handleError',
1273 + // Attempt to unmount previous children:
1274 + 'BrokenComponentWillUnmount componentWillUnmount [!]',
1275 + 'BrokenComponentWillUnmount componentWillUnmount [!]',
1276 + // Render error:
1277 + 'ErrorBoundary render error',
1278 + 'ErrorBoundary componentDidUpdate',
1279 + // Children don't get componentDidUpdate() since update was aborted
1280 + ]);
1281 +
1282 + log.length = 0;
1283 + ReactDOM.unmountComponentAtNode(container);
1284 + expect(log).toEqual([
1285 + 'ErrorBoundary componentWillUnmount',
1286 + ]);
1287 + });
1288 +
1289 + it('recovers from nested componentWillUnmount errors on update', () => {
1290 + var container = document.createElement('div');
1291 + ReactDOM.render(
1292 + <ErrorBoundary>
1293 + <Normal>
1294 + <BrokenComponentWillUnmount />
1295 + </Normal>
1296 + <BrokenComponentWillUnmount />
1297 + </ErrorBoundary>,
1298 + container
1299 + );
1300 +
1301 + log.length = 0;
1302 + ReactDOM.render(
1303 + <ErrorBoundary>
1304 + <Normal>
1305 + <BrokenComponentWillUnmount />
1306 + </Normal>
1307 + </ErrorBoundary>,
1308 + container
1309 + );
1310 + expect(container.textContent).toBe('Caught an error: Hello.');
1311 + expect(log).toEqual([
1312 + 'ErrorBoundary componentWillReceiveProps',
1313 + 'ErrorBoundary componentWillUpdate',
1314 + 'ErrorBoundary render success',
1315 + // Update existing children:
1316 + 'Normal componentWillReceiveProps',
1317 + 'Normal componentWillUpdate',
1318 + 'Normal render',
1319 + 'BrokenComponentWillUnmount componentWillReceiveProps',
1320 + 'BrokenComponentWillUnmount componentWillUpdate',
1321 + 'BrokenComponentWillUnmount render',
1322 + // Unmounting throws:
1323 + 'BrokenComponentWillUnmount componentWillUnmount [!]',
1324 + 'ErrorBoundary unstable_handleError',
1325 + // Attempt to unmount previous children:
1326 + 'Normal componentWillUnmount',
1327 + 'BrokenComponentWillUnmount componentWillUnmount [!]',
1328 + // Render error:
1329 + 'ErrorBoundary render error',
1330 + 'ErrorBoundary componentDidUpdate',
1331 + ]);
1332 +
1333 + log.length = 0;
1334 + ReactDOM.unmountComponentAtNode(container);
1335 + expect(log).toEqual([
1336 + 'ErrorBoundary componentWillUnmount',
1337 + ]);
1338 + });
1339 +
1340 + it('can recover from error state', () => {
1341 + var container = document.createElement('div');
1342 + ReactDOM.render(
1343 + <ErrorBoundary>
1344 + <BrokenRender />
1345 + </ErrorBoundary>,
1346 + container
1347 + );
1348 +
1349 + ReactDOM.render(
1350 + <ErrorBoundary>
1351 + <Normal />
1352 + </ErrorBoundary>,
1353 + container
1354 + );
1355 + // Error boundary doesn't retry by itself:
1356 + expect(container.textContent).toBe('Caught an error: Hello.');
1357 +
1358 + // Force the success path:
1359 + log.length = 0;
1360 + ReactDOM.render(
1361 + <ErrorBoundary forceRetry={true}>
1362 + <Normal />
1363 + </ErrorBoundary>,
1364 + container
1365 + );
1366 + expect(container.textContent).not.toContain('Caught an error');
1367 + expect(log).toEqual([
1368 + 'ErrorBoundary componentWillReceiveProps',
1369 + 'ErrorBoundary componentWillUpdate',
1370 + 'ErrorBoundary render success',
1371 + // Mount children:
1372 + 'Normal constructor',
1373 + 'Normal componentWillMount',
1374 + 'Normal render',
1375 + // Finalize updates:
1376 + 'Normal componentDidMount',
1377 + 'ErrorBoundary componentDidUpdate',
1378 + ]);
1379 +
1380 + log.length = 0;
1381 + ReactDOM.unmountComponentAtNode(container);
1382 + expect(log).toEqual([
1383 + 'ErrorBoundary componentWillUnmount',
1384 + 'Normal componentWillUnmount',
1385 + ]);
1386 + });
1387 +
1388 + it('can update multiple times in error state', () => {
1389 + var container = document.createElement('div');
1390 + ReactDOM.render(
1391 + <ErrorBoundary>
1392 + <BrokenRender />
1393 + </ErrorBoundary>,
1394 + container
1395 + );
1396 + expect(container.textContent).toBe('Caught an error: Hello.');
1397 +
1398 + ReactDOM.render(
1399 + <ErrorBoundary>
1400 + <BrokenRender />
1401 + </ErrorBoundary>,
1402 + container
1403 + );
1404 + expect(container.textContent).toBe('Caught an error: Hello.');
1405 +
1406 + ReactDOM.render(<div>Other screen</div>, container);
1407 + expect(container.textContent).toBe('Other screen');
1408 +
1409 + ReactDOM.unmountComponentAtNode(container);
1410 + });
1411 +
1412 + it('doesn\'t get into inconsistent state during removals', () => {
1413 + var container = document.createElement('div');
1414 + ReactDOM.render(
1415 + <ErrorBoundary>
1416 + <Normal />
1417 + <BrokenComponentWillUnmount />
1418 + <Normal />
1419 + </ErrorBoundary>,
1420 + container
1421 + );
1422 +
1423 + ReactDOM.render(<ErrorBoundary />, container);
1424 + expect(container.textContent).toBe('Caught an error: Hello.');
1425 +
1426 + log.length = 0;
1427 + ReactDOM.unmountComponentAtNode(container);
1428 + expect(log).toEqual([
1429 + 'ErrorBoundary componentWillUnmount',
1430 + ]);
1431 + });
1432 +
1433 + it('doesn\'t get into inconsistent state during additions', () => {
1434 + var container = document.createElement('div');
1435 + ReactDOM.render(<ErrorBoundary />, container);
1436 + ReactDOM.render(
1437 + <ErrorBoundary>
1438 + <Normal />
1439 + <BrokenRender />
1440 + <Normal />
1441 + </ErrorBoundary>,
1442 + container
1443 + );
1444 + expect(container.textContent).toBe('Caught an error: Hello.');
1445 +
1446 + log.length = 0;
1447 + ReactDOM.unmountComponentAtNode(container);
1448 + expect(log).toEqual([
1449 + 'ErrorBoundary componentWillUnmount',
1450 + ]);
1451 + });
1452 +
1453 + it('doesn\'t get into inconsistent state during reorders', () => {
1454 + function getAMixOfNormalAndBrokenRenderElements() {
1455 + var elements = [];
1456 + for (var i = 0; i < 100; i++) {
1457 + elements.push(<Normal key={i} />);
1458 + }
1459 + elements.push(<MaybeBrokenRender key={100} />);
1460 +
1461 + var currentIndex = elements.length;
1462 + while (0 !== currentIndex) {
1463 + var randomIndex = Math.floor(Math.random() * currentIndex);
1464 + currentIndex -= 1;
1465 + var temporaryValue = elements[currentIndex];
1466 + elements[currentIndex] = elements[randomIndex];
1467 + elements[randomIndex] = temporaryValue;
1468 + }
1469 + return elements;
1470 + }
1471 +
1472 + class MaybeBrokenRender extends React.Component {
1473 + render() {
1474 + if (fail) {
1475 + throw new Error('Hello');
1476 + }
1477 + return <div />;
1478 + }
1479 + }
1480 +
1481 + var fail = false;
1482 + var container = document.createElement('div');
1483 + ReactDOM.render(
1484 + <ErrorBoundary>
1485 + {getAMixOfNormalAndBrokenRenderElements()}
1486 + </ErrorBoundary>,
1487 + container
1488 + );
1489 + expect(container.textContent).not.toContain('Caught an error');
1490 +
1491 + fail = true;
1492 + ReactDOM.render(
1493 + <ErrorBoundary>
1494 + {getAMixOfNormalAndBrokenRenderElements()}
1495 + </ErrorBoundary>,
1496 + container
1497 + );
1498 + expect(container.textContent).toBe('Caught an error: Hello.');
1499 +
1500 + log.length = 0;
1501 ReactDOM.unmountComponentAtNode(container);
1502 expect(log).toEqual([
295 - 'Box render',
296 - 'Inquisitive render',
297 - 'Angry render',
298 - 'Inquisitive ref null',
299 - 'Inquisitive componentWillUnmount',
300 - 'Box renderError',
301 - 'Box componentDidMount',
302 - 'Box componentWillUnmount',
1503 + 'ErrorBoundary componentWillUnmount',
1504 ]);
1505 });
1506 });
src/renderers/testing/ReactTestMount.js
+2 -1
@@ -128,7 +128,8 @@ ReactTestInstance.prototype.unmount = function(nextElement) {
128 transaction.perform(function() {
129 ReactReconciler.unmountComponent(
130 component,
131 - false
131 + false, /* safely */
132 + false /* skipLifecycle */
133 );
134 });
135 ReactUpdates.ReactReconcileTransaction.release(transaction);
src/test/ReactShallowRenderer.js
+5 -1
@@ -117,7 +117,11 @@ class ReactShallowRenderer {
117 }
118 unmount() {
119 if (this._instance) {
120 - ReactReconciler.unmountComponent(this._instance, false);
120 + ReactReconciler.unmountComponent(
121 + this._instance,
122 + false, /* safely */
123 + false /* skipLifecycle */
124 + );
125 }
126 }
127 _render(element, transaction, context) {