main
js 2,261 lines 72 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @emails react-core
8 */
9
10 'use strict';
11
12 let PropTypes;
13 let React;
14 let ReactDOM;
15 let act;
16 let assertConsoleErrorDev;
17
18 // TODO: Refactor this test once componentDidCatch setState is deprecated.
19 describe('ReactLegacyErrorBoundaries', () => {
20 let log;
21
22 let BrokenConstructor;
23 let BrokenComponentWillMount;
24 let BrokenComponentDidMount;
25 let BrokenComponentWillReceiveProps;
26 let BrokenComponentWillUpdate;
27 let BrokenComponentDidUpdate;
28 let BrokenComponentWillUnmount;
29 let BrokenRenderErrorBoundary;
30 let BrokenComponentWillMountErrorBoundary;
31 let BrokenComponentDidMountErrorBoundary;
32 let BrokenRender;
33 let ErrorBoundary;
34 let BothErrorBoundaries;
35 let ErrorMessage;
36 let NoopErrorBoundary;
37 let RetryErrorBoundary;
38 let Normal;
39
40 beforeEach(() => {
41 jest.resetModules();
42 PropTypes = require('prop-types');
43 ReactDOM = require('react-dom');
44 React = require('react');
45 act = require('internal-test-utils').act;
46 assertConsoleErrorDev =
47 require('internal-test-utils').assertConsoleErrorDev;
48
49 log = [];
50
51 BrokenConstructor = class extends React.Component {
52 constructor(props) {
53 super(props);
54 log.push('BrokenConstructor constructor [!]');
55 throw new Error('Hello');
56 }
57 render() {
58 log.push('BrokenConstructor render');
59 return <div>{this.props.children}</div>;
60 }
61 UNSAFE_componentWillMount() {
62 log.push('BrokenConstructor componentWillMount');
63 }
64 componentDidMount() {
65 log.push('BrokenConstructor componentDidMount');
66 }
67 UNSAFE_componentWillReceiveProps() {
68 log.push('BrokenConstructor componentWillReceiveProps');
69 }
70 UNSAFE_componentWillUpdate() {
71 log.push('BrokenConstructor componentWillUpdate');
72 }
73 componentDidUpdate() {
74 log.push('BrokenConstructor componentDidUpdate');
75 }
76 componentWillUnmount() {
77 log.push('BrokenConstructor componentWillUnmount');
78 }
79 };
80
81 BrokenComponentWillMount = class extends React.Component {
82 constructor(props) {
83 super(props);
84 log.push('BrokenComponentWillMount constructor');
85 }
86 render() {
87 log.push('BrokenComponentWillMount render');
88 return <div>{this.props.children}</div>;
89 }
90 UNSAFE_componentWillMount() {
91 log.push('BrokenComponentWillMount componentWillMount [!]');
92 throw new Error('Hello');
93 }
94 componentDidMount() {
95 log.push('BrokenComponentWillMount componentDidMount');
96 }
97 UNSAFE_componentWillReceiveProps() {
98 log.push('BrokenComponentWillMount componentWillReceiveProps');
99 }
100 UNSAFE_componentWillUpdate() {
101 log.push('BrokenComponentWillMount componentWillUpdate');
102 }
103 componentDidUpdate() {
104 log.push('BrokenComponentWillMount componentDidUpdate');
105 }
106 componentWillUnmount() {
107 log.push('BrokenComponentWillMount componentWillUnmount');
108 }
109 };
110
111 BrokenComponentDidMount = class extends React.Component {
112 constructor(props) {
113 super(props);
114 log.push('BrokenComponentDidMount constructor');
115 }
116 render() {
117 log.push('BrokenComponentDidMount render');
118 return <div>{this.props.children}</div>;
119 }
120 UNSAFE_componentWillMount() {
121 log.push('BrokenComponentDidMount componentWillMount');
122 }
123 componentDidMount() {
124 log.push('BrokenComponentDidMount componentDidMount [!]');
125 throw new Error('Hello');
126 }
127 UNSAFE_componentWillReceiveProps() {
128 log.push('BrokenComponentDidMount componentWillReceiveProps');
129 }
130 UNSAFE_componentWillUpdate() {
131 log.push('BrokenComponentDidMount componentWillUpdate');
132 }
133 componentDidUpdate() {
134 log.push('BrokenComponentDidMount componentDidUpdate');
135 }
136 componentWillUnmount() {
137 log.push('BrokenComponentDidMount componentWillUnmount');
138 }
139 };
140
141 BrokenComponentWillReceiveProps = class extends React.Component {
142 constructor(props) {
143 super(props);
144 log.push('BrokenComponentWillReceiveProps constructor');
145 }
146 render() {
147 log.push('BrokenComponentWillReceiveProps render');
148 return <div>{this.props.children}</div>;
149 }
150 UNSAFE_componentWillMount() {
151 log.push('BrokenComponentWillReceiveProps componentWillMount');
152 }
153 componentDidMount() {
154 log.push('BrokenComponentWillReceiveProps componentDidMount');
155 }
156 UNSAFE_componentWillReceiveProps() {
157 log.push(
158 'BrokenComponentWillReceiveProps componentWillReceiveProps [!]',
159 );
160 throw new Error('Hello');
161 }
162 UNSAFE_componentWillUpdate() {
163 log.push('BrokenComponentWillReceiveProps componentWillUpdate');
164 }
165 componentDidUpdate() {
166 log.push('BrokenComponentWillReceiveProps componentDidUpdate');
167 }
168 componentWillUnmount() {
169 log.push('BrokenComponentWillReceiveProps componentWillUnmount');
170 }
171 };
172
173 BrokenComponentWillUpdate = class extends React.Component {
174 constructor(props) {
175 super(props);
176 log.push('BrokenComponentWillUpdate constructor');
177 }
178 render() {
179 log.push('BrokenComponentWillUpdate render');
180 return <div>{this.props.children}</div>;
181 }
182 UNSAFE_componentWillMount() {
183 log.push('BrokenComponentWillUpdate componentWillMount');
184 }
185 componentDidMount() {
186 log.push('BrokenComponentWillUpdate componentDidMount');
187 }
188 UNSAFE_componentWillReceiveProps() {
189 log.push('BrokenComponentWillUpdate componentWillReceiveProps');
190 }
191 UNSAFE_componentWillUpdate() {
192 log.push('BrokenComponentWillUpdate componentWillUpdate [!]');
193 throw new Error('Hello');
194 }
195 componentDidUpdate() {
196 log.push('BrokenComponentWillUpdate componentDidUpdate');
197 }
198 componentWillUnmount() {
199 log.push('BrokenComponentWillUpdate componentWillUnmount');
200 }
201 };
202
203 BrokenComponentDidUpdate = class extends React.Component {
204 static defaultProps = {
205 errorText: 'Hello',
206 };
207 constructor(props) {
208 super(props);
209 log.push('BrokenComponentDidUpdate constructor');
210 }
211 render() {
212 log.push('BrokenComponentDidUpdate render');
213 return <div>{this.props.children}</div>;
214 }
215 UNSAFE_componentWillMount() {
216 log.push('BrokenComponentDidUpdate componentWillMount');
217 }
218 componentDidMount() {
219 log.push('BrokenComponentDidUpdate componentDidMount');
220 }
221 UNSAFE_componentWillReceiveProps() {
222 log.push('BrokenComponentDidUpdate componentWillReceiveProps');
223 }
224 UNSAFE_componentWillUpdate() {
225 log.push('BrokenComponentDidUpdate componentWillUpdate');
226 }
227 componentDidUpdate() {
228 log.push('BrokenComponentDidUpdate componentDidUpdate [!]');
229 throw new Error(this.props.errorText);
230 }
231 componentWillUnmount() {
232 log.push('BrokenComponentDidUpdate componentWillUnmount');
233 }
234 };
235
236 BrokenComponentWillUnmount = class extends React.Component {
237 static defaultProps = {
238 errorText: 'Hello',
239 };
240 constructor(props) {
241 super(props);
242 log.push('BrokenComponentWillUnmount constructor');
243 }
244 render() {
245 log.push('BrokenComponentWillUnmount render');
246 return <div>{this.props.children}</div>;
247 }
248 UNSAFE_componentWillMount() {
249 log.push('BrokenComponentWillUnmount componentWillMount');
250 }
251 componentDidMount() {
252 log.push('BrokenComponentWillUnmount componentDidMount');
253 }
254 UNSAFE_componentWillReceiveProps() {
255 log.push('BrokenComponentWillUnmount componentWillReceiveProps');
256 }
257 UNSAFE_componentWillUpdate() {
258 log.push('BrokenComponentWillUnmount componentWillUpdate');
259 }
260 componentDidUpdate() {
261 log.push('BrokenComponentWillUnmount componentDidUpdate');
262 }
263 componentWillUnmount() {
264 log.push('BrokenComponentWillUnmount componentWillUnmount [!]');
265 throw new Error(this.props.errorText);
266 }
267 };
268
269 BrokenComponentWillMountErrorBoundary = class extends React.Component {
270 constructor(props) {
271 super(props);
272 this.state = {error: null};
273 log.push('BrokenComponentWillMountErrorBoundary constructor');
274 }
275 render() {
276 if (this.state.error) {
277 log.push('BrokenComponentWillMountErrorBoundary render error');
278 return <div>Caught an error: {this.state.error.message}.</div>;
279 }
280 log.push('BrokenComponentWillMountErrorBoundary render success');
281 return <div>{this.props.children}</div>;
282 }
283 UNSAFE_componentWillMount() {
284 log.push(
285 'BrokenComponentWillMountErrorBoundary componentWillMount [!]',
286 );
287 throw new Error('Hello');
288 }
289 componentDidMount() {
290 log.push('BrokenComponentWillMountErrorBoundary componentDidMount');
291 }
292 componentWillUnmount() {
293 log.push('BrokenComponentWillMountErrorBoundary componentWillUnmount');
294 }
295 componentDidCatch(error) {
296 log.push('BrokenComponentWillMountErrorBoundary componentDidCatch');
297 this.setState({error});
298 }
299 };
300
301 BrokenComponentDidMountErrorBoundary = class extends React.Component {
302 constructor(props) {
303 super(props);
304 this.state = {error: null};
305 log.push('BrokenComponentDidMountErrorBoundary constructor');
306 }
307 render() {
308 if (this.state.error) {
309 log.push('BrokenComponentDidMountErrorBoundary render error');
310 return <div>Caught an error: {this.state.error.message}.</div>;
311 }
312 log.push('BrokenComponentDidMountErrorBoundary render success');
313 return <div>{this.props.children}</div>;
314 }
315 UNSAFE_componentWillMount() {
316 log.push('BrokenComponentDidMountErrorBoundary componentWillMount');
317 }
318 componentDidMount() {
319 log.push('BrokenComponentDidMountErrorBoundary componentDidMount [!]');
320 throw new Error('Hello');
321 }
322 componentWillUnmount() {
323 log.push('BrokenComponentDidMountErrorBoundary componentWillUnmount');
324 }
325 componentDidCatch(error) {
326 log.push('BrokenComponentDidMountErrorBoundary componentDidCatch');
327 this.setState({error});
328 }
329 };
330
331 BrokenRenderErrorBoundary = class extends React.Component {
332 constructor(props) {
333 super(props);
334 this.state = {error: null};
335 log.push('BrokenRenderErrorBoundary constructor');
336 }
337 render() {
338 if (this.state.error) {
339 log.push('BrokenRenderErrorBoundary render error [!]');
340 throw new Error('Hello');
341 }
342 log.push('BrokenRenderErrorBoundary render success');
343 return <div>{this.props.children}</div>;
344 }
345 UNSAFE_componentWillMount() {
346 log.push('BrokenRenderErrorBoundary componentWillMount');
347 }
348 componentDidMount() {
349 log.push('BrokenRenderErrorBoundary componentDidMount');
350 }
351 componentWillUnmount() {
352 log.push('BrokenRenderErrorBoundary componentWillUnmount');
353 }
354 componentDidCatch(error) {
355 log.push('BrokenRenderErrorBoundary componentDidCatch');
356 this.setState({error});
357 }
358 };
359
360 BrokenRender = class extends React.Component {
361 constructor(props) {
362 super(props);
363 log.push('BrokenRender constructor');
364 }
365 render() {
366 log.push('BrokenRender render [!]');
367 throw new Error('Hello');
368 }
369 UNSAFE_componentWillMount() {
370 log.push('BrokenRender componentWillMount');
371 }
372 componentDidMount() {
373 log.push('BrokenRender componentDidMount');
374 }
375 UNSAFE_componentWillReceiveProps() {
376 log.push('BrokenRender componentWillReceiveProps');
377 }
378 UNSAFE_componentWillUpdate() {
379 log.push('BrokenRender componentWillUpdate');
380 }
381 componentDidUpdate() {
382 log.push('BrokenRender componentDidUpdate');
383 }
384 componentWillUnmount() {
385 log.push('BrokenRender componentWillUnmount');
386 }
387 };
388
389 NoopErrorBoundary = class extends React.Component {
390 constructor(props) {
391 super(props);
392 log.push('NoopErrorBoundary constructor');
393 }
394 render() {
395 log.push('NoopErrorBoundary render');
396 return <BrokenRender />;
397 }
398 UNSAFE_componentWillMount() {
399 log.push('NoopErrorBoundary componentWillMount');
400 }
401 componentDidMount() {
402 log.push('NoopErrorBoundary componentDidMount');
403 }
404 componentWillUnmount() {
405 log.push('NoopErrorBoundary componentWillUnmount');
406 }
407 componentDidCatch() {
408 log.push('NoopErrorBoundary componentDidCatch');
409 }
410 };
411
412 Normal = class extends React.Component {
413 static defaultProps = {
414 logName: 'Normal',
415 };
416 constructor(props) {
417 super(props);
418 log.push(`${this.props.logName} constructor`);
419 }
420 render() {
421 log.push(`${this.props.logName} render`);
422 return <div>{this.props.children}</div>;
423 }
424 UNSAFE_componentWillMount() {
425 log.push(`${this.props.logName} componentWillMount`);
426 }
427 componentDidMount() {
428 log.push(`${this.props.logName} componentDidMount`);
429 }
430 UNSAFE_componentWillReceiveProps() {
431 log.push(`${this.props.logName} componentWillReceiveProps`);
432 }
433 UNSAFE_componentWillUpdate() {
434 log.push(`${this.props.logName} componentWillUpdate`);
435 }
436 componentDidUpdate() {
437 log.push(`${this.props.logName} componentDidUpdate`);
438 }
439 componentWillUnmount() {
440 log.push(`${this.props.logName} componentWillUnmount`);
441 }
442 };
443
444 ErrorBoundary = class extends React.Component {
445 constructor(props) {
446 super(props);
447 this.state = {error: null};
448 log.push(`${this.props.logName} constructor`);
449 }
450 render() {
451 if (this.state.error && !this.props.forceRetry) {
452 log.push(`${this.props.logName} render error`);
453 return this.props.renderError(this.state.error, this.props);
454 }
455 log.push(`${this.props.logName} render success`);
456 return <div>{this.props.children}</div>;
457 }
458 componentDidCatch(error) {
459 log.push(`${this.props.logName} componentDidCatch`);
460 this.setState({error});
461 }
462 UNSAFE_componentWillMount() {
463 log.push(`${this.props.logName} componentWillMount`);
464 }
465 componentDidMount() {
466 log.push(`${this.props.logName} componentDidMount`);
467 }
468 UNSAFE_componentWillReceiveProps() {
469 log.push(`${this.props.logName} componentWillReceiveProps`);
470 }
471 UNSAFE_componentWillUpdate() {
472 log.push(`${this.props.logName} componentWillUpdate`);
473 }
474 componentDidUpdate() {
475 log.push(`${this.props.logName} componentDidUpdate`);
476 }
477 componentWillUnmount() {
478 log.push(`${this.props.logName} componentWillUnmount`);
479 }
480 };
481 ErrorBoundary.defaultProps = {
482 logName: 'ErrorBoundary',
483 renderError(error, props) {
484 return (
485 <div ref={props.errorMessageRef}>
486 Caught an error: {error.message}.
487 </div>
488 );
489 },
490 };
491
492 BothErrorBoundaries = class extends React.Component {
493 constructor(props) {
494 super(props);
495 this.state = {error: null};
496 log.push('BothErrorBoundaries constructor');
497 }
498
499 static getDerivedStateFromError(error) {
500 log.push('BothErrorBoundaries static getDerivedStateFromError');
501 return {error};
502 }
503
504 render() {
505 if (this.state.error) {
506 log.push('BothErrorBoundaries render error');
507 return <div>Caught an error: {this.state.error.message}.</div>;
508 }
509 log.push('BothErrorBoundaries render success');
510 return <div>{this.props.children}</div>;
511 }
512
513 componentDidCatch(error) {
514 log.push('BothErrorBoundaries componentDidCatch');
515 this.setState({error});
516 }
517
518 UNSAFE_componentWillMount() {
519 log.push('BothErrorBoundaries componentWillMount');
520 }
521
522 componentDidMount() {
523 log.push('BothErrorBoundaries componentDidMount');
524 }
525
526 UNSAFE_componentWillReceiveProps() {
527 log.push('BothErrorBoundaries componentWillReceiveProps');
528 }
529
530 UNSAFE_componentWillUpdate() {
531 log.push('BothErrorBoundaries componentWillUpdate');
532 }
533
534 componentDidUpdate() {
535 log.push('BothErrorBoundaries componentDidUpdate');
536 }
537
538 componentWillUnmount() {
539 log.push('BothErrorBoundaries componentWillUnmount');
540 }
541 };
542
543 RetryErrorBoundary = class extends React.Component {
544 constructor(props) {
545 super(props);
546 log.push('RetryErrorBoundary constructor');
547 }
548 render() {
549 log.push('RetryErrorBoundary render');
550 return <BrokenRender />;
551 }
552 UNSAFE_componentWillMount() {
553 log.push('RetryErrorBoundary componentWillMount');
554 }
555 componentDidMount() {
556 log.push('RetryErrorBoundary componentDidMount');
557 }
558 componentWillUnmount() {
559 log.push('RetryErrorBoundary componentWillUnmount');
560 }
561 componentDidCatch(error) {
562 log.push('RetryErrorBoundary componentDidCatch [!]');
563 // In Fiber, calling setState() (and failing) is treated as a rethrow.
564 this.setState({});
565 }
566 };
567
568 ErrorMessage = class extends React.Component {
569 constructor(props) {
570 super(props);
571 log.push('ErrorMessage constructor');
572 }
573 UNSAFE_componentWillMount() {
574 log.push('ErrorMessage componentWillMount');
575 }
576 componentDidMount() {
577 log.push('ErrorMessage componentDidMount');
578 }
579 componentWillUnmount() {
580 log.push('ErrorMessage componentWillUnmount');
581 }
582 render() {
583 log.push('ErrorMessage render');
584 return <div>Caught an error: {this.props.message}.</div>;
585 }
586 };
587 });
588
589 afterEach(() => {
590 jest.restoreAllMocks();
591 });
592
593 // @gate !disableLegacyMode
594 it('does not swallow exceptions on mounting without boundaries', async () => {
595 let container = document.createElement('div');
596 await expect(async () => {
597 await act(() => {
598 ReactDOM.render(<BrokenRender />, container);
599 });
600 }).rejects.toThrow('Hello');
601
602 container = document.createElement('div');
603 await expect(async () => {
604 await act(() => {
605 ReactDOM.render(<BrokenComponentWillMount />, container);
606 });
607 }).rejects.toThrow('Hello');
608
609 container = document.createElement('div');
610 await expect(async () => {
611 await act(() => {
612 ReactDOM.render(<BrokenComponentDidMount />, container);
613 });
614 }).rejects.toThrow('Hello');
615 });
616
617 // @gate !disableLegacyMode
618 it('does not swallow exceptions on updating without boundaries', async () => {
619 let container = document.createElement('div');
620 ReactDOM.render(<BrokenComponentWillUpdate />, container);
621 await expect(async () => {
622 await act(() => {
623 ReactDOM.render(<BrokenComponentWillUpdate />, container);
624 });
625 }).rejects.toThrow('Hello');
626
627 container = document.createElement('div');
628 ReactDOM.render(<BrokenComponentWillReceiveProps />, container);
629 await expect(async () => {
630 await act(() => {
631 ReactDOM.render(<BrokenComponentWillReceiveProps />, container);
632 });
633 }).rejects.toThrow('Hello');
634
635 container = document.createElement('div');
636 ReactDOM.render(<BrokenComponentDidUpdate />, container);
637 await expect(async () => {
638 await act(() => {
639 ReactDOM.render(<BrokenComponentDidUpdate />, container);
640 });
641 }).rejects.toThrow('Hello');
642 });
643
644 // @gate !disableLegacyMode
645 it('does not swallow exceptions on unmounting without boundaries', async () => {
646 const container = document.createElement('div');
647 ReactDOM.render(<BrokenComponentWillUnmount />, container);
648 await expect(async () => {
649 await act(() => {
650 ReactDOM.unmountComponentAtNode(container);
651 });
652 }).rejects.toThrow('Hello');
653 });
654
655 // @gate !disableLegacyMode
656 it('prevents errors from leaking into other roots', async () => {
657 const container1 = document.createElement('div');
658 const container2 = document.createElement('div');
659 const container3 = document.createElement('div');
660
661 ReactDOM.render(<span>Before 1</span>, container1);
662 await expect(async () => {
663 await act(() => {
664 ReactDOM.render(<BrokenRender />, container2);
665 });
666 }).rejects.toThrow('Hello');
667 ReactDOM.render(
668 <ErrorBoundary>
669 <BrokenRender />
670 </ErrorBoundary>,
671 container3,
672 );
673 expect(container1.firstChild.textContent).toBe('Before 1');
674 expect(container2.firstChild).toBe(null);
675 expect(container3.firstChild.textContent).toBe('Caught an error: Hello.');
676
677 ReactDOM.render(<span>After 1</span>, container1);
678 ReactDOM.render(<span>After 2</span>, container2);
679 ReactDOM.render(
680 <ErrorBoundary forceRetry={true}>After 3</ErrorBoundary>,
681 container3,
682 );
683 expect(container1.firstChild.textContent).toBe('After 1');
684 expect(container2.firstChild.textContent).toBe('After 2');
685 expect(container3.firstChild.textContent).toBe('After 3');
686
687 ReactDOM.unmountComponentAtNode(container1);
688 ReactDOM.unmountComponentAtNode(container2);
689 ReactDOM.unmountComponentAtNode(container3);
690 expect(container1.firstChild).toBe(null);
691 expect(container2.firstChild).toBe(null);
692 expect(container3.firstChild).toBe(null);
693 });
694
695 // @gate !disableLegacyMode
696 it('logs a single error using both error boundaries', () => {
697 const container = document.createElement('div');
698 spyOnDev(console, 'error');
699 ReactDOM.render(
700 <BothErrorBoundaries>
701 <BrokenRender />
702 </BothErrorBoundaries>,
703 container,
704 );
705 if (__DEV__) {
706 expect(console.error).toHaveBeenCalledTimes(2);
707 expect(console.error.mock.calls[0][0]).toContain(
708 'ReactDOM.render has not been supported since React 18',
709 );
710 expect(console.error.mock.calls[1][2]).toContain(
711 'The above error occurred in the <BrokenRender> component',
712 );
713 }
714
715 expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
716 expect(log).toEqual([
717 'BothErrorBoundaries constructor',
718 'BothErrorBoundaries componentWillMount',
719 'BothErrorBoundaries render success',
720 'BrokenRender constructor',
721 'BrokenRender componentWillMount',
722 'BrokenRender render [!]',
723 // Both getDerivedStateFromError and componentDidCatch should be called
724 'BothErrorBoundaries static getDerivedStateFromError',
725 'BothErrorBoundaries componentWillMount',
726 'BothErrorBoundaries render error',
727 // Fiber mounts with null children before capturing error
728 'BothErrorBoundaries componentDidMount',
729 // Catch and render an error message
730 'BothErrorBoundaries componentDidCatch',
731 'BothErrorBoundaries componentWillUpdate',
732 'BothErrorBoundaries render error',
733 'BothErrorBoundaries componentDidUpdate',
734 ]);
735
736 log.length = 0;
737 ReactDOM.unmountComponentAtNode(container);
738 expect(log).toEqual(['BothErrorBoundaries componentWillUnmount']);
739 });
740
741 // @gate !disableLegacyMode
742 it('renders an error state if child throws in render', () => {
743 const container = document.createElement('div');
744 ReactDOM.render(
745 <ErrorBoundary>
746 <BrokenRender />
747 </ErrorBoundary>,
748 container,
749 );
750 expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
751 expect(log).toEqual([
752 'ErrorBoundary constructor',
753 'ErrorBoundary componentWillMount',
754 'ErrorBoundary render success',
755 'BrokenRender constructor',
756 'BrokenRender componentWillMount',
757 'BrokenRender render [!]',
758 // Fiber mounts with null children before capturing error
759 'ErrorBoundary componentDidMount',
760 // Catch and render an error message
761 'ErrorBoundary componentDidCatch',
762 'ErrorBoundary componentWillUpdate',
763 'ErrorBoundary render error',
764 'ErrorBoundary componentDidUpdate',
765 ]);
766
767 log.length = 0;
768 ReactDOM.unmountComponentAtNode(container);
769 expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
770 });
771
772 // @gate !disableLegacyMode
773 it('renders an error state if child throws in constructor', () => {
774 const container = document.createElement('div');
775 ReactDOM.render(
776 <ErrorBoundary>
777 <BrokenConstructor />
778 </ErrorBoundary>,
779 container,
780 );
781 expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
782 expect(log).toEqual([
783 'ErrorBoundary constructor',
784 'ErrorBoundary componentWillMount',
785 'ErrorBoundary render success',
786 'BrokenConstructor constructor [!]',
787 // Fiber mounts with null children before capturing error
788 'ErrorBoundary componentDidMount',
789 // Catch and render an error message
790 'ErrorBoundary componentDidCatch',
791 'ErrorBoundary componentWillUpdate',
792 'ErrorBoundary render error',
793 'ErrorBoundary componentDidUpdate',
794 ]);
795
796 log.length = 0;
797 ReactDOM.unmountComponentAtNode(container);
798 expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
799 });
800
801 // @gate !disableLegacyMode
802 it('renders an error state if child throws in componentWillMount', () => {
803 const container = document.createElement('div');
804 ReactDOM.render(
805 <ErrorBoundary>
806 <BrokenComponentWillMount />
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 'BrokenComponentWillMount constructor',
816 'BrokenComponentWillMount componentWillMount [!]',
817 'ErrorBoundary componentDidMount',
818 'ErrorBoundary componentDidCatch',
819 'ErrorBoundary componentWillUpdate',
820 'ErrorBoundary render error',
821 'ErrorBoundary componentDidUpdate',
822 ]);
823
824 log.length = 0;
825 ReactDOM.unmountComponentAtNode(container);
826 expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
827 });
828
829 // @gate !disableLegacyContext || !__DEV__
830 // @gate !disableLegacyMode
831 it('renders an error state if context provider throws in componentWillMount', () => {
832 class BrokenComponentWillMountWithContext extends React.Component {
833 static childContextTypes = {foo: PropTypes.number};
834 getChildContext() {
835 return {foo: 42};
836 }
837 render() {
838 return <div>{this.props.children}</div>;
839 }
840 UNSAFE_componentWillMount() {
841 throw new Error('Hello');
842 }
843 }
844
845 const container = document.createElement('div');
846 ReactDOM.render(
847 <ErrorBoundary>
848 <BrokenComponentWillMountWithContext />
849 </ErrorBoundary>,
850 container,
851 );
852 assertConsoleErrorDev([
853 'BrokenComponentWillMountWithContext uses the legacy childContextTypes API which will soon be removed. ' +
854 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
855 ' in BrokenComponentWillMountWithContext (at **)',
856 ]);
857 expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
858 });
859
860 // @gate !disableLegacyMode
861 it('mounts the error message if mounting fails', () => {
862 function renderError(error) {
863 return <ErrorMessage message={error.message} />;
864 }
865
866 const container = document.createElement('div');
867 ReactDOM.render(
868 <ErrorBoundary renderError={renderError}>
869 <BrokenRender />
870 </ErrorBoundary>,
871 container,
872 );
873 expect(log).toEqual([
874 'ErrorBoundary constructor',
875 'ErrorBoundary componentWillMount',
876 'ErrorBoundary render success',
877 'BrokenRender constructor',
878 'BrokenRender componentWillMount',
879 'BrokenRender render [!]',
880 'ErrorBoundary componentDidMount',
881 'ErrorBoundary componentDidCatch',
882 'ErrorBoundary componentWillUpdate',
883 'ErrorBoundary render error',
884 'ErrorMessage constructor',
885 'ErrorMessage componentWillMount',
886 'ErrorMessage render',
887 'ErrorMessage componentDidMount',
888 'ErrorBoundary componentDidUpdate',
889 ]);
890
891 log.length = 0;
892 ReactDOM.unmountComponentAtNode(container);
893 expect(log).toEqual([
894 'ErrorBoundary componentWillUnmount',
895 'ErrorMessage componentWillUnmount',
896 ]);
897 });
898
899 // @gate !disableLegacyMode
900 it('propagates errors on retry on mounting', () => {
901 const container = document.createElement('div');
902 ReactDOM.render(
903 <ErrorBoundary>
904 <RetryErrorBoundary>
905 <BrokenRender />
906 </RetryErrorBoundary>
907 </ErrorBoundary>,
908 container,
909 );
910 expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
911 expect(log).toEqual([
912 'ErrorBoundary constructor',
913 'ErrorBoundary componentWillMount',
914 'ErrorBoundary render success',
915 'RetryErrorBoundary constructor',
916 'RetryErrorBoundary componentWillMount',
917 'RetryErrorBoundary render',
918 'BrokenRender constructor',
919 'BrokenRender componentWillMount',
920 'BrokenRender render [!]',
921 // In Fiber, failed error boundaries render null before attempting to recover
922 'RetryErrorBoundary componentDidMount',
923 'RetryErrorBoundary componentDidCatch [!]',
924 'ErrorBoundary componentDidMount',
925 // Retry
926 'RetryErrorBoundary render',
927 'BrokenRender constructor',
928 'BrokenRender componentWillMount',
929 'BrokenRender render [!]',
930 // This time, the error propagates to the higher boundary
931 'RetryErrorBoundary componentWillUnmount',
932 'ErrorBoundary componentDidCatch',
933 // Render the error
934 'ErrorBoundary componentWillUpdate',
935 'ErrorBoundary render error',
936 'ErrorBoundary componentDidUpdate',
937 ]);
938
939 log.length = 0;
940 ReactDOM.unmountComponentAtNode(container);
941 expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
942 });
943
944 // @gate !disableLegacyMode
945 it('propagates errors inside boundary during componentWillMount', () => {
946 const container = document.createElement('div');
947 ReactDOM.render(
948 <ErrorBoundary>
949 <BrokenComponentWillMountErrorBoundary />
950 </ErrorBoundary>,
951 container,
952 );
953 expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
954 expect(log).toEqual([
955 'ErrorBoundary constructor',
956 'ErrorBoundary componentWillMount',
957 'ErrorBoundary render success',
958 'BrokenComponentWillMountErrorBoundary constructor',
959 'BrokenComponentWillMountErrorBoundary componentWillMount [!]',
960 // The error propagates to the higher boundary
961 'ErrorBoundary componentDidMount',
962 'ErrorBoundary componentDidCatch',
963 'ErrorBoundary componentWillUpdate',
964 'ErrorBoundary render error',
965 'ErrorBoundary componentDidUpdate',
966 ]);
967
968 log.length = 0;
969 ReactDOM.unmountComponentAtNode(container);
970 expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
971 });
972
973 // @gate !disableLegacyMode
974 it('propagates errors inside boundary while rendering error state', () => {
975 const container = document.createElement('div');
976 ReactDOM.render(
977 <ErrorBoundary>
978 <BrokenRenderErrorBoundary>
979 <BrokenRender />
980 </BrokenRenderErrorBoundary>
981 </ErrorBoundary>,
982 container,
983 );
984 expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
985 expect(log).toEqual([
986 'ErrorBoundary constructor',
987 'ErrorBoundary componentWillMount',
988 'ErrorBoundary render success',
989 'BrokenRenderErrorBoundary constructor',
990 'BrokenRenderErrorBoundary componentWillMount',
991 'BrokenRenderErrorBoundary render success',
992 'BrokenRender constructor',
993 'BrokenRender componentWillMount',
994 'BrokenRender render [!]',
995 // The first error boundary catches the error
996 // It adjusts state but throws displaying the message
997 // Finish mounting with null children
998 'BrokenRenderErrorBoundary componentDidMount',
999 // Attempt to handle the error
1000 'BrokenRenderErrorBoundary componentDidCatch',
1001 'ErrorBoundary componentDidMount',
1002 'BrokenRenderErrorBoundary render error [!]',
1003 // Boundary fails with new error, propagate to next boundary
1004 'BrokenRenderErrorBoundary componentWillUnmount',
1005 // Attempt to handle the error again
1006 'ErrorBoundary componentDidCatch',
1007 'ErrorBoundary componentWillUpdate',
1008 'ErrorBoundary render error',
1009 'ErrorBoundary componentDidUpdate',
1010 ]);
1011
1012 log.length = 0;
1013 ReactDOM.unmountComponentAtNode(container);
1014 expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
1015 });
1016
1017 // @gate !disableLegacyMode
1018 it('does not call componentWillUnmount when aborting initial mount', () => {
1019 const container = document.createElement('div');
1020 ReactDOM.render(
1021 <ErrorBoundary>
1022 <Normal />
1023 <BrokenRender />
1024 <Normal />
1025 </ErrorBoundary>,
1026 container,
1027 );
1028 expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
1029 expect(log).toEqual([
1030 'ErrorBoundary constructor',
1031 'ErrorBoundary componentWillMount',
1032 'ErrorBoundary render success',
1033 // Render first child
1034 'Normal constructor',
1035 'Normal componentWillMount',
1036 'Normal render',
1037 // Render second child (it throws)
1038 'BrokenRender constructor',
1039 'BrokenRender componentWillMount',
1040 'BrokenRender render [!]',
1041 // Skip the remaining siblings
1042 // Finish mounting with null children
1043 'ErrorBoundary componentDidMount',
1044 // Handle the error
1045 'ErrorBoundary componentDidCatch',
1046 // Render the error message
1047 'ErrorBoundary componentWillUpdate',
1048 'ErrorBoundary render error',
1049 'ErrorBoundary componentDidUpdate',
1050 ]);
1051
1052 log.length = 0;
1053 ReactDOM.unmountComponentAtNode(container);
1054 expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
1055 });
1056
1057 // @gate !disableLegacyMode
1058 it('resets callback refs if mounting aborts', () => {
1059 function childRef(x) {
1060 log.push('Child ref is set to ' + x);
1061 }
1062 function errorMessageRef(x) {
1063 log.push('Error message ref is set to ' + x);
1064 }
1065
1066 const container = document.createElement('div');
1067 ReactDOM.render(
1068 <ErrorBoundary errorMessageRef={errorMessageRef}>
1069 <div ref={childRef} />
1070 <BrokenRender />
1071 </ErrorBoundary>,
1072 container,
1073 );
1074 expect(container.textContent).toBe('Caught an error: Hello.');
1075 expect(log).toEqual([
1076 'ErrorBoundary constructor',
1077 'ErrorBoundary componentWillMount',
1078 'ErrorBoundary render success',
1079 'BrokenRender constructor',
1080 'BrokenRender componentWillMount',
1081 'BrokenRender render [!]',
1082 // Handle error:
1083 // Finish mounting with null children
1084 'ErrorBoundary componentDidMount',
1085 // Handle the error
1086 'ErrorBoundary componentDidCatch',
1087 // Render the error message
1088 'ErrorBoundary componentWillUpdate',
1089 'ErrorBoundary render error',
1090 'Error message ref is set to [object HTMLDivElement]',
1091 'ErrorBoundary componentDidUpdate',
1092 ]);
1093
1094 log.length = 0;
1095 ReactDOM.unmountComponentAtNode(container);
1096 expect(log).toEqual([
1097 'ErrorBoundary componentWillUnmount',
1098 'Error message ref is set to null',
1099 ]);
1100 });
1101
1102 // @gate !disableLegacyMode
1103 it('resets object refs if mounting aborts', () => {
1104 const childRef = React.createRef();
1105 const errorMessageRef = React.createRef();
1106
1107 const container = document.createElement('div');
1108 ReactDOM.render(
1109 <ErrorBoundary errorMessageRef={errorMessageRef}>
1110 <div ref={childRef} />
1111 <BrokenRender />
1112 </ErrorBoundary>,
1113 container,
1114 );
1115 expect(container.textContent).toBe('Caught an error: Hello.');
1116 expect(log).toEqual([
1117 'ErrorBoundary constructor',
1118 'ErrorBoundary componentWillMount',
1119 'ErrorBoundary render success',
1120 'BrokenRender constructor',
1121 'BrokenRender componentWillMount',
1122 'BrokenRender render [!]',
1123 // Handle error:
1124 // Finish mounting with null children
1125 'ErrorBoundary componentDidMount',
1126 // Handle the error
1127 'ErrorBoundary componentDidCatch',
1128 // Render the error message
1129 'ErrorBoundary componentWillUpdate',
1130 'ErrorBoundary render error',
1131 'ErrorBoundary componentDidUpdate',
1132 ]);
1133 expect(errorMessageRef.current.toString()).toEqual(
1134 '[object HTMLDivElement]',
1135 );
1136
1137 log.length = 0;
1138 ReactDOM.unmountComponentAtNode(container);
1139 expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
1140 expect(errorMessageRef.current).toEqual(null);
1141 });
1142
1143 // @gate !disableLegacyMode
1144 it('successfully mounts if no error occurs', () => {
1145 const container = document.createElement('div');
1146 ReactDOM.render(
1147 <ErrorBoundary>
1148 <div>Mounted successfully.</div>
1149 </ErrorBoundary>,
1150 container,
1151 );
1152 expect(container.firstChild.textContent).toBe('Mounted successfully.');
1153 expect(log).toEqual([
1154 'ErrorBoundary constructor',
1155 'ErrorBoundary componentWillMount',
1156 'ErrorBoundary render success',
1157 'ErrorBoundary componentDidMount',
1158 ]);
1159
1160 log.length = 0;
1161 ReactDOM.unmountComponentAtNode(container);
1162 expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
1163 });
1164
1165 // @gate !disableLegacyMode
1166 it('catches if child throws in constructor during update', () => {
1167 const container = document.createElement('div');
1168 ReactDOM.render(
1169 <ErrorBoundary>
1170 <Normal />
1171 </ErrorBoundary>,
1172 container,
1173 );
1174
1175 log.length = 0;
1176 ReactDOM.render(
1177 <ErrorBoundary>
1178 <Normal />
1179 <Normal logName="Normal2" />
1180 <BrokenConstructor />
1181 </ErrorBoundary>,
1182 container,
1183 );
1184 expect(container.textContent).toBe('Caught an error: Hello.');
1185 expect(log).toEqual([
1186 'ErrorBoundary componentWillReceiveProps',
1187 'ErrorBoundary componentWillUpdate',
1188 'ErrorBoundary render success',
1189 'Normal componentWillReceiveProps',
1190 'Normal componentWillUpdate',
1191 'Normal render',
1192 // Normal2 will attempt to mount:
1193 'Normal2 constructor',
1194 'Normal2 componentWillMount',
1195 'Normal2 render',
1196 // BrokenConstructor will abort rendering:
1197 'BrokenConstructor constructor [!]',
1198 // Finish updating with null children
1199 'Normal componentWillUnmount',
1200 'ErrorBoundary componentDidUpdate',
1201 // Handle the error
1202 'ErrorBoundary componentDidCatch',
1203 // Render the error message
1204 'ErrorBoundary componentWillUpdate',
1205 'ErrorBoundary render error',
1206 'ErrorBoundary componentDidUpdate',
1207 ]);
1208
1209 log.length = 0;
1210 ReactDOM.unmountComponentAtNode(container);
1211 expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
1212 });
1213
1214 // @gate !disableLegacyMode
1215 it('catches if child throws in componentWillMount during update', () => {
1216 const container = document.createElement('div');
1217 ReactDOM.render(
1218 <ErrorBoundary>
1219 <Normal />
1220 </ErrorBoundary>,
1221 container,
1222 );
1223
1224 log.length = 0;
1225 ReactDOM.render(
1226 <ErrorBoundary>
1227 <Normal />
1228 <Normal logName="Normal2" />
1229 <BrokenComponentWillMount />
1230 </ErrorBoundary>,
1231 container,
1232 );
1233 expect(container.textContent).toBe('Caught an error: Hello.');
1234 expect(log).toEqual([
1235 'ErrorBoundary componentWillReceiveProps',
1236 'ErrorBoundary componentWillUpdate',
1237 'ErrorBoundary render success',
1238 'Normal componentWillReceiveProps',
1239 'Normal componentWillUpdate',
1240 'Normal render',
1241 // Normal2 will attempt to mount:
1242 'Normal2 constructor',
1243 'Normal2 componentWillMount',
1244 'Normal2 render',
1245 // BrokenComponentWillMount will abort rendering:
1246 'BrokenComponentWillMount constructor',
1247 'BrokenComponentWillMount componentWillMount [!]',
1248 // Finish updating with null children
1249 'Normal componentWillUnmount',
1250 'ErrorBoundary componentDidUpdate',
1251 // Handle the error
1252 'ErrorBoundary componentDidCatch',
1253 // Render the error message
1254 'ErrorBoundary componentWillUpdate',
1255 'ErrorBoundary render error',
1256 'ErrorBoundary componentDidUpdate',
1257 ]);
1258
1259 log.length = 0;
1260 ReactDOM.unmountComponentAtNode(container);
1261 expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
1262 });
1263
1264 // @gate !disableLegacyMode
1265 it('catches if child throws in componentWillReceiveProps during update', () => {
1266 const container = document.createElement('div');
1267 ReactDOM.render(
1268 <ErrorBoundary>
1269 <Normal />
1270 <BrokenComponentWillReceiveProps />
1271 </ErrorBoundary>,
1272 container,
1273 );
1274
1275 log.length = 0;
1276 ReactDOM.render(
1277 <ErrorBoundary>
1278 <Normal />
1279 <BrokenComponentWillReceiveProps />
1280 </ErrorBoundary>,
1281 container,
1282 );
1283 expect(container.textContent).toBe('Caught an error: Hello.');
1284 expect(log).toEqual([
1285 'ErrorBoundary componentWillReceiveProps',
1286 'ErrorBoundary componentWillUpdate',
1287 'ErrorBoundary render success',
1288 'Normal componentWillReceiveProps',
1289 'Normal componentWillUpdate',
1290 'Normal render',
1291 // BrokenComponentWillReceiveProps will abort rendering:
1292 'BrokenComponentWillReceiveProps componentWillReceiveProps [!]',
1293 // Finish updating with null children
1294 'Normal componentWillUnmount',
1295 'BrokenComponentWillReceiveProps componentWillUnmount',
1296 'ErrorBoundary componentDidUpdate',
1297 // Handle the error
1298 'ErrorBoundary componentDidCatch',
1299 'ErrorBoundary componentWillUpdate',
1300 'ErrorBoundary render error',
1301 'ErrorBoundary componentDidUpdate',
1302 ]);
1303
1304 log.length = 0;
1305 ReactDOM.unmountComponentAtNode(container);
1306 expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
1307 });
1308
1309 // @gate !disableLegacyMode
1310 it('catches if child throws in componentWillUpdate during update', () => {
1311 const container = document.createElement('div');
1312 ReactDOM.render(
1313 <ErrorBoundary>
1314 <Normal />
1315 <BrokenComponentWillUpdate />
1316 </ErrorBoundary>,
1317 container,
1318 );
1319
1320 log.length = 0;
1321 ReactDOM.render(
1322 <ErrorBoundary>
1323 <Normal />
1324 <BrokenComponentWillUpdate />
1325 </ErrorBoundary>,
1326 container,
1327 );
1328 expect(container.textContent).toBe('Caught an error: Hello.');
1329 expect(log).toEqual([
1330 'ErrorBoundary componentWillReceiveProps',
1331 'ErrorBoundary componentWillUpdate',
1332 'ErrorBoundary render success',
1333 'Normal componentWillReceiveProps',
1334 'Normal componentWillUpdate',
1335 'Normal render',
1336 // BrokenComponentWillUpdate will abort rendering:
1337 'BrokenComponentWillUpdate componentWillReceiveProps',
1338 'BrokenComponentWillUpdate componentWillUpdate [!]',
1339 // Finish updating with null children
1340 'Normal componentWillUnmount',
1341 'BrokenComponentWillUpdate componentWillUnmount',
1342 'ErrorBoundary componentDidUpdate',
1343 // Handle the error
1344 'ErrorBoundary componentDidCatch',
1345 'ErrorBoundary componentWillUpdate',
1346 'ErrorBoundary render error',
1347 'ErrorBoundary componentDidUpdate',
1348 ]);
1349
1350 log.length = 0;
1351 ReactDOM.unmountComponentAtNode(container);
1352 expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
1353 });
1354
1355 // @gate !disableLegacyMode
1356 it('catches if child throws in render during update', () => {
1357 const container = document.createElement('div');
1358 ReactDOM.render(
1359 <ErrorBoundary>
1360 <Normal />
1361 </ErrorBoundary>,
1362 container,
1363 );
1364
1365 log.length = 0;
1366 ReactDOM.render(
1367 <ErrorBoundary>
1368 <Normal />
1369 <Normal logName="Normal2" />
1370 <BrokenRender />
1371 </ErrorBoundary>,
1372 container,
1373 );
1374 expect(container.textContent).toBe('Caught an error: Hello.');
1375 expect(log).toEqual([
1376 'ErrorBoundary componentWillReceiveProps',
1377 'ErrorBoundary componentWillUpdate',
1378 'ErrorBoundary render success',
1379 'Normal componentWillReceiveProps',
1380 'Normal componentWillUpdate',
1381 'Normal render',
1382 // Normal2 will attempt to mount:
1383 'Normal2 constructor',
1384 'Normal2 componentWillMount',
1385 'Normal2 render',
1386 // BrokenRender will abort rendering:
1387 'BrokenRender constructor',
1388 'BrokenRender componentWillMount',
1389 'BrokenRender render [!]',
1390 // Finish updating with null children
1391 'Normal componentWillUnmount',
1392 'ErrorBoundary componentDidUpdate',
1393 // Handle the error
1394 'ErrorBoundary componentDidCatch',
1395 'ErrorBoundary componentWillUpdate',
1396 'ErrorBoundary render error',
1397 'ErrorBoundary componentDidUpdate',
1398 ]);
1399
1400 log.length = 0;
1401 ReactDOM.unmountComponentAtNode(container);
1402 expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
1403 });
1404
1405 // @gate !disableLegacyMode
1406 it('keeps refs up-to-date during updates', () => {
1407 function child1Ref(x) {
1408 log.push('Child1 ref is set to ' + x);
1409 }
1410 function child2Ref(x) {
1411 log.push('Child2 ref is set to ' + x);
1412 }
1413 function errorMessageRef(x) {
1414 log.push('Error message ref is set to ' + x);
1415 }
1416
1417 const container = document.createElement('div');
1418 ReactDOM.render(
1419 <ErrorBoundary errorMessageRef={errorMessageRef}>
1420 <div ref={child1Ref} />
1421 </ErrorBoundary>,
1422 container,
1423 );
1424 expect(log).toEqual([
1425 'ErrorBoundary constructor',
1426 'ErrorBoundary componentWillMount',
1427 'ErrorBoundary render success',
1428 'Child1 ref is set to [object HTMLDivElement]',
1429 'ErrorBoundary componentDidMount',
1430 ]);
1431
1432 log.length = 0;
1433 ReactDOM.render(
1434 <ErrorBoundary errorMessageRef={errorMessageRef}>
1435 <div ref={child1Ref} />
1436 <div ref={child2Ref} />
1437 <BrokenRender />
1438 </ErrorBoundary>,
1439 container,
1440 );
1441 expect(container.textContent).toBe('Caught an error: Hello.');
1442 expect(log).toEqual([
1443 'ErrorBoundary componentWillReceiveProps',
1444 'ErrorBoundary componentWillUpdate',
1445 'ErrorBoundary render success',
1446 // BrokenRender will abort rendering:
1447 'BrokenRender constructor',
1448 'BrokenRender componentWillMount',
1449 'BrokenRender render [!]',
1450 // Finish updating with null children
1451 'Child1 ref is set to null',
1452 'ErrorBoundary componentDidUpdate',
1453 // Handle the error
1454 'ErrorBoundary componentDidCatch',
1455 'ErrorBoundary componentWillUpdate',
1456 'ErrorBoundary render error',
1457 'Error message ref is set to [object HTMLDivElement]',
1458 // Child2 ref is never set because its mounting aborted
1459 'ErrorBoundary componentDidUpdate',
1460 ]);
1461
1462 log.length = 0;
1463 ReactDOM.unmountComponentAtNode(container);
1464 expect(log).toEqual([
1465 'ErrorBoundary componentWillUnmount',
1466 'Error message ref is set to null',
1467 ]);
1468 });
1469
1470 // @gate !disableLegacyMode
1471 it('recovers from componentWillUnmount errors on update', () => {
1472 const container = document.createElement('div');
1473 ReactDOM.render(
1474 <ErrorBoundary>
1475 <BrokenComponentWillUnmount />
1476 <BrokenComponentWillUnmount />
1477 <Normal />
1478 </ErrorBoundary>,
1479 container,
1480 );
1481
1482 log.length = 0;
1483 ReactDOM.render(
1484 <ErrorBoundary>
1485 <BrokenComponentWillUnmount />
1486 </ErrorBoundary>,
1487 container,
1488 );
1489 expect(container.textContent).toBe('Caught an error: Hello.');
1490 expect(log).toEqual([
1491 'ErrorBoundary componentWillReceiveProps',
1492 'ErrorBoundary componentWillUpdate',
1493 'ErrorBoundary render success',
1494 // Update existing child:
1495 'BrokenComponentWillUnmount componentWillReceiveProps',
1496 'BrokenComponentWillUnmount componentWillUpdate',
1497 'BrokenComponentWillUnmount render',
1498 // Unmounting throws:
1499 'BrokenComponentWillUnmount componentWillUnmount [!]',
1500 // Fiber proceeds with lifecycles despite errors
1501 'Normal componentWillUnmount',
1502 // The components have updated in this phase
1503 'BrokenComponentWillUnmount componentDidUpdate',
1504 'ErrorBoundary componentDidUpdate',
1505 // Now that commit phase is done, Fiber unmounts the boundary's children
1506 'BrokenComponentWillUnmount componentWillUnmount [!]',
1507 'ErrorBoundary componentDidCatch',
1508 // The initial render was aborted, so
1509 // Fiber retries from the root.
1510 'ErrorBoundary componentWillUpdate',
1511 'ErrorBoundary componentDidUpdate',
1512 // The second willUnmount error should be captured and logged, too.
1513 'ErrorBoundary componentDidCatch',
1514 'ErrorBoundary componentWillUpdate',
1515 // Render an error now (stack will do it later)
1516 'ErrorBoundary render error',
1517 // Attempt to unmount previous child:
1518 // Done
1519 'ErrorBoundary componentDidUpdate',
1520 ]);
1521
1522 log.length = 0;
1523 ReactDOM.unmountComponentAtNode(container);
1524 expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
1525 });
1526
1527 // @gate !disableLegacyMode
1528 it('recovers from nested componentWillUnmount errors on update', () => {
1529 const container = document.createElement('div');
1530 ReactDOM.render(
1531 <ErrorBoundary>
1532 <Normal>
1533 <BrokenComponentWillUnmount />
1534 </Normal>
1535 <BrokenComponentWillUnmount />
1536 </ErrorBoundary>,
1537 container,
1538 );
1539
1540 log.length = 0;
1541 ReactDOM.render(
1542 <ErrorBoundary>
1543 <Normal>
1544 <BrokenComponentWillUnmount />
1545 </Normal>
1546 </ErrorBoundary>,
1547 container,
1548 );
1549 expect(container.textContent).toBe('Caught an error: Hello.');
1550 expect(log).toEqual([
1551 'ErrorBoundary componentWillReceiveProps',
1552 'ErrorBoundary componentWillUpdate',
1553 'ErrorBoundary render success',
1554 // Update existing children:
1555 'Normal componentWillReceiveProps',
1556 'Normal componentWillUpdate',
1557 'Normal render',
1558 'BrokenComponentWillUnmount componentWillReceiveProps',
1559 'BrokenComponentWillUnmount componentWillUpdate',
1560 'BrokenComponentWillUnmount render',
1561 // Unmounting throws:
1562 'BrokenComponentWillUnmount componentWillUnmount [!]',
1563 // Fiber proceeds with lifecycles despite errors
1564 'BrokenComponentWillUnmount componentDidUpdate',
1565 'Normal componentDidUpdate',
1566 'ErrorBoundary componentDidUpdate',
1567 'Normal componentWillUnmount',
1568 'BrokenComponentWillUnmount componentWillUnmount [!]',
1569 // Now that commit phase is done, Fiber handles errors
1570 'ErrorBoundary componentDidCatch',
1571 // The initial render was aborted, so
1572 // Fiber retries from the root.
1573 'ErrorBoundary componentWillUpdate',
1574 'ErrorBoundary componentDidUpdate',
1575 // The second willUnmount error should be captured and logged, too.
1576 'ErrorBoundary componentDidCatch',
1577 'ErrorBoundary componentWillUpdate',
1578 // Render an error now (stack will do it later)
1579 'ErrorBoundary render error',
1580 // Done
1581 'ErrorBoundary componentDidUpdate',
1582 ]);
1583
1584 log.length = 0;
1585 ReactDOM.unmountComponentAtNode(container);
1586 expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
1587 });
1588
1589 // @gate !disableLegacyMode
1590 it('picks the right boundary when handling unmounting errors', () => {
1591 function renderInnerError(error) {
1592 return <div>Caught an inner error: {error.message}.</div>;
1593 }
1594 function renderOuterError(error) {
1595 return <div>Caught an outer error: {error.message}.</div>;
1596 }
1597
1598 const container = document.createElement('div');
1599 ReactDOM.render(
1600 <ErrorBoundary
1601 logName="OuterErrorBoundary"
1602 renderError={renderOuterError}>
1603 <ErrorBoundary
1604 logName="InnerErrorBoundary"
1605 renderError={renderInnerError}>
1606 <BrokenComponentWillUnmount />
1607 </ErrorBoundary>
1608 </ErrorBoundary>,
1609 container,
1610 );
1611
1612 log.length = 0;
1613 ReactDOM.render(
1614 <ErrorBoundary
1615 logName="OuterErrorBoundary"
1616 renderError={renderOuterError}>
1617 <ErrorBoundary
1618 logName="InnerErrorBoundary"
1619 renderError={renderInnerError}
1620 />
1621 </ErrorBoundary>,
1622 container,
1623 );
1624 expect(container.textContent).toBe('Caught an inner error: Hello.');
1625 expect(log).toEqual([
1626 // Update outer boundary
1627 'OuterErrorBoundary componentWillReceiveProps',
1628 'OuterErrorBoundary componentWillUpdate',
1629 'OuterErrorBoundary render success',
1630 // Update inner boundary
1631 'InnerErrorBoundary componentWillReceiveProps',
1632 'InnerErrorBoundary componentWillUpdate',
1633 'InnerErrorBoundary render success',
1634 // Try unmounting child
1635 'BrokenComponentWillUnmount componentWillUnmount [!]',
1636 // Fiber proceeds with lifecycles despite errors
1637 // Inner and outer boundaries have updated in this phase
1638 'InnerErrorBoundary componentDidUpdate',
1639 'OuterErrorBoundary componentDidUpdate',
1640 // Now that commit phase is done, Fiber handles errors
1641 // Only inner boundary receives the error:
1642 'InnerErrorBoundary componentDidCatch',
1643 'InnerErrorBoundary componentWillUpdate',
1644 // Render an error now
1645 'InnerErrorBoundary render error',
1646 // In Fiber, this was a local update to the
1647 // inner boundary so only its hook fires
1648 'InnerErrorBoundary componentDidUpdate',
1649 ]);
1650
1651 log.length = 0;
1652 ReactDOM.unmountComponentAtNode(container);
1653 expect(log).toEqual([
1654 'OuterErrorBoundary componentWillUnmount',
1655 'InnerErrorBoundary componentWillUnmount',
1656 ]);
1657 });
1658
1659 // @gate !disableLegacyMode
1660 it('can recover from error state', () => {
1661 const container = document.createElement('div');
1662 ReactDOM.render(
1663 <ErrorBoundary>
1664 <BrokenRender />
1665 </ErrorBoundary>,
1666 container,
1667 );
1668
1669 ReactDOM.render(
1670 <ErrorBoundary>
1671 <Normal />
1672 </ErrorBoundary>,
1673 container,
1674 );
1675 // Error boundary doesn't retry by itself:
1676 expect(container.textContent).toBe('Caught an error: Hello.');
1677
1678 // Force the success path:
1679 log.length = 0;
1680 ReactDOM.render(
1681 <ErrorBoundary forceRetry={true}>
1682 <Normal />
1683 </ErrorBoundary>,
1684 container,
1685 );
1686 expect(container.textContent).not.toContain('Caught an error');
1687 expect(log).toEqual([
1688 'ErrorBoundary componentWillReceiveProps',
1689 'ErrorBoundary componentWillUpdate',
1690 'ErrorBoundary render success',
1691 // Mount children:
1692 'Normal constructor',
1693 'Normal componentWillMount',
1694 'Normal render',
1695 // Finalize updates:
1696 'Normal componentDidMount',
1697 'ErrorBoundary componentDidUpdate',
1698 ]);
1699
1700 log.length = 0;
1701 ReactDOM.unmountComponentAtNode(container);
1702 expect(log).toEqual([
1703 'ErrorBoundary componentWillUnmount',
1704 'Normal componentWillUnmount',
1705 ]);
1706 });
1707
1708 // @gate !disableLegacyMode
1709 it('can update multiple times in error state', () => {
1710 const container = document.createElement('div');
1711 ReactDOM.render(
1712 <ErrorBoundary>
1713 <BrokenRender />
1714 </ErrorBoundary>,
1715 container,
1716 );
1717 expect(container.textContent).toBe('Caught an error: Hello.');
1718
1719 ReactDOM.render(
1720 <ErrorBoundary>
1721 <BrokenRender />
1722 </ErrorBoundary>,
1723 container,
1724 );
1725 expect(container.textContent).toBe('Caught an error: Hello.');
1726
1727 ReactDOM.render(<div>Other screen</div>, container);
1728 expect(container.textContent).toBe('Other screen');
1729
1730 ReactDOM.unmountComponentAtNode(container);
1731 });
1732
1733 // @gate !disableLegacyMode
1734 it("doesn't get into inconsistent state during removals", () => {
1735 const container = document.createElement('div');
1736 ReactDOM.render(
1737 <ErrorBoundary>
1738 <Normal />
1739 <BrokenComponentWillUnmount />
1740 <Normal />
1741 </ErrorBoundary>,
1742 container,
1743 );
1744
1745 ReactDOM.render(<ErrorBoundary />, container);
1746 expect(container.textContent).toBe('Caught an error: Hello.');
1747
1748 log.length = 0;
1749 ReactDOM.unmountComponentAtNode(container);
1750 expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
1751 });
1752
1753 // @gate !disableLegacyMode
1754 it("doesn't get into inconsistent state during additions", () => {
1755 const container = document.createElement('div');
1756 ReactDOM.render(<ErrorBoundary />, container);
1757 ReactDOM.render(
1758 <ErrorBoundary>
1759 <Normal />
1760 <BrokenRender />
1761 <Normal />
1762 </ErrorBoundary>,
1763 container,
1764 );
1765 expect(container.textContent).toBe('Caught an error: Hello.');
1766
1767 log.length = 0;
1768 ReactDOM.unmountComponentAtNode(container);
1769 expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
1770 });
1771
1772 // @gate !disableLegacyMode
1773 it("doesn't get into inconsistent state during reorders", () => {
1774 function getAMixOfNormalAndBrokenRenderElements() {
1775 const elements = [];
1776 for (let i = 0; i < 100; i++) {
1777 elements.push(<Normal key={i} />);
1778 }
1779 elements.push(<MaybeBrokenRender key={100} />);
1780
1781 let currentIndex = elements.length;
1782 while (0 !== currentIndex) {
1783 const randomIndex = Math.floor(Math.random() * currentIndex);
1784 currentIndex -= 1;
1785 const temporaryValue = elements[currentIndex];
1786 elements[currentIndex] = elements[randomIndex];
1787 elements[randomIndex] = temporaryValue;
1788 }
1789 return elements;
1790 }
1791
1792 class MaybeBrokenRender extends React.Component {
1793 render() {
1794 if (fail) {
1795 throw new Error('Hello');
1796 }
1797 return <div>{this.props.children}</div>;
1798 }
1799 }
1800
1801 let fail = false;
1802 const container = document.createElement('div');
1803 ReactDOM.render(
1804 <ErrorBoundary>{getAMixOfNormalAndBrokenRenderElements()}</ErrorBoundary>,
1805 container,
1806 );
1807 expect(container.textContent).not.toContain('Caught an error');
1808
1809 fail = true;
1810 ReactDOM.render(
1811 <ErrorBoundary>{getAMixOfNormalAndBrokenRenderElements()}</ErrorBoundary>,
1812 container,
1813 );
1814 expect(container.textContent).toBe('Caught an error: Hello.');
1815
1816 log.length = 0;
1817 ReactDOM.unmountComponentAtNode(container);
1818 expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
1819 });
1820
1821 // @gate !disableLegacyMode
1822 it('catches errors originating downstream', () => {
1823 let fail = false;
1824 class Stateful extends React.Component {
1825 state = {shouldThrow: false};
1826
1827 render() {
1828 if (fail) {
1829 log.push('Stateful render [!]');
1830 throw new Error('Hello');
1831 }
1832 return <div>{this.props.children}</div>;
1833 }
1834 }
1835
1836 let statefulInst;
1837 const container = document.createElement('div');
1838 ReactDOM.render(
1839 <ErrorBoundary>
1840 <Stateful ref={inst => (statefulInst = inst)} />
1841 </ErrorBoundary>,
1842 container,
1843 );
1844
1845 log.length = 0;
1846 expect(() => {
1847 fail = true;
1848 statefulInst.forceUpdate();
1849 }).not.toThrow();
1850
1851 expect(log).toEqual([
1852 'Stateful render [!]',
1853 'ErrorBoundary componentDidCatch',
1854 'ErrorBoundary componentWillUpdate',
1855 'ErrorBoundary render error',
1856 'ErrorBoundary componentDidUpdate',
1857 ]);
1858
1859 log.length = 0;
1860 ReactDOM.unmountComponentAtNode(container);
1861 expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
1862 });
1863
1864 // @gate !disableLegacyMode
1865 it('catches errors in componentDidMount', () => {
1866 const container = document.createElement('div');
1867 ReactDOM.render(
1868 <ErrorBoundary>
1869 <BrokenComponentWillUnmount>
1870 <Normal />
1871 </BrokenComponentWillUnmount>
1872 <BrokenComponentDidMount />
1873 <Normal logName="LastChild" />
1874 </ErrorBoundary>,
1875 container,
1876 );
1877 expect(log).toEqual([
1878 'ErrorBoundary constructor',
1879 'ErrorBoundary componentWillMount',
1880 'ErrorBoundary render success',
1881 'BrokenComponentWillUnmount constructor',
1882 'BrokenComponentWillUnmount componentWillMount',
1883 'BrokenComponentWillUnmount render',
1884 'Normal constructor',
1885 'Normal componentWillMount',
1886 'Normal render',
1887 'BrokenComponentDidMount constructor',
1888 'BrokenComponentDidMount componentWillMount',
1889 'BrokenComponentDidMount render',
1890 'LastChild constructor',
1891 'LastChild componentWillMount',
1892 'LastChild render',
1893 // Start flushing didMount queue
1894 'Normal componentDidMount',
1895 'BrokenComponentWillUnmount componentDidMount',
1896 'BrokenComponentDidMount componentDidMount [!]',
1897 // Continue despite the error
1898 'LastChild componentDidMount',
1899 'ErrorBoundary componentDidMount',
1900 // Now we are ready to handle the error
1901 // Safely unmount every child
1902 'BrokenComponentWillUnmount componentWillUnmount [!]',
1903 // Continue unmounting safely despite any errors
1904 'Normal componentWillUnmount',
1905 'BrokenComponentDidMount componentWillUnmount',
1906 'LastChild componentWillUnmount',
1907 // Handle the error
1908 'ErrorBoundary componentDidCatch',
1909 'ErrorBoundary componentWillUpdate',
1910 // The willUnmount error should be captured and logged, too.
1911 'ErrorBoundary componentDidUpdate',
1912 'ErrorBoundary componentDidCatch',
1913 'ErrorBoundary componentWillUpdate',
1914 'ErrorBoundary render error',
1915 // The update has finished
1916 'ErrorBoundary componentDidUpdate',
1917 ]);
1918
1919 log.length = 0;
1920 ReactDOM.unmountComponentAtNode(container);
1921 expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
1922 });
1923
1924 // @gate !disableLegacyMode
1925 it('catches errors in componentDidUpdate', () => {
1926 const container = document.createElement('div');
1927 ReactDOM.render(
1928 <ErrorBoundary>
1929 <BrokenComponentDidUpdate />
1930 </ErrorBoundary>,
1931 container,
1932 );
1933
1934 log.length = 0;
1935 ReactDOM.render(
1936 <ErrorBoundary>
1937 <BrokenComponentDidUpdate />
1938 </ErrorBoundary>,
1939 container,
1940 );
1941 expect(log).toEqual([
1942 'ErrorBoundary componentWillReceiveProps',
1943 'ErrorBoundary componentWillUpdate',
1944 'ErrorBoundary render success',
1945 'BrokenComponentDidUpdate componentWillReceiveProps',
1946 'BrokenComponentDidUpdate componentWillUpdate',
1947 'BrokenComponentDidUpdate render',
1948 // All lifecycles run
1949 'BrokenComponentDidUpdate componentDidUpdate [!]',
1950 'ErrorBoundary componentDidUpdate',
1951 'BrokenComponentDidUpdate componentWillUnmount',
1952 // Then, error is handled
1953 'ErrorBoundary componentDidCatch',
1954 'ErrorBoundary componentWillUpdate',
1955 'ErrorBoundary render error',
1956 'ErrorBoundary componentDidUpdate',
1957 ]);
1958
1959 log.length = 0;
1960 ReactDOM.unmountComponentAtNode(container);
1961 expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
1962 });
1963
1964 // @gate !disableLegacyMode
1965 it('propagates errors inside boundary during componentDidMount', () => {
1966 const container = document.createElement('div');
1967 ReactDOM.render(
1968 <ErrorBoundary>
1969 <BrokenComponentDidMountErrorBoundary
1970 renderError={error => (
1971 <div>We should never catch our own error: {error.message}.</div>
1972 )}
1973 />
1974 </ErrorBoundary>,
1975 container,
1976 );
1977 expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
1978 expect(log).toEqual([
1979 'ErrorBoundary constructor',
1980 'ErrorBoundary componentWillMount',
1981 'ErrorBoundary render success',
1982 'BrokenComponentDidMountErrorBoundary constructor',
1983 'BrokenComponentDidMountErrorBoundary componentWillMount',
1984 'BrokenComponentDidMountErrorBoundary render success',
1985 'BrokenComponentDidMountErrorBoundary componentDidMount [!]',
1986 // Fiber proceeds with the hooks
1987 'ErrorBoundary componentDidMount',
1988 'BrokenComponentDidMountErrorBoundary componentWillUnmount',
1989 // The error propagates to the higher boundary
1990 'ErrorBoundary componentDidCatch',
1991 // Fiber retries from the root
1992 'ErrorBoundary componentWillUpdate',
1993 'ErrorBoundary render error',
1994 'ErrorBoundary componentDidUpdate',
1995 ]);
1996
1997 log.length = 0;
1998 ReactDOM.unmountComponentAtNode(container);
1999 expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
2000 });
2001
2002 // @gate !disableLegacyMode
2003 it('calls componentDidCatch for each error that is captured', () => {
2004 function renderUnmountError(error) {
2005 return <div>Caught an unmounting error: {error.message}.</div>;
2006 }
2007 function renderUpdateError(error) {
2008 return <div>Caught an updating error: {error.message}.</div>;
2009 }
2010
2011 const container = document.createElement('div');
2012 ReactDOM.render(
2013 <ErrorBoundary logName="OuterErrorBoundary">
2014 <ErrorBoundary
2015 logName="InnerUnmountBoundary"
2016 renderError={renderUnmountError}>
2017 <BrokenComponentWillUnmount errorText="E1" />
2018 <BrokenComponentWillUnmount errorText="E2" />
2019 </ErrorBoundary>
2020 <ErrorBoundary
2021 logName="InnerUpdateBoundary"
2022 renderError={renderUpdateError}>
2023 <BrokenComponentDidUpdate errorText="E3" />
2024 <BrokenComponentDidUpdate errorText="E4" />
2025 </ErrorBoundary>
2026 </ErrorBoundary>,
2027 container,
2028 );
2029
2030 log.length = 0;
2031 ReactDOM.render(
2032 <ErrorBoundary logName="OuterErrorBoundary">
2033 <ErrorBoundary
2034 logName="InnerUnmountBoundary"
2035 renderError={renderUnmountError}
2036 />
2037 <ErrorBoundary
2038 logName="InnerUpdateBoundary"
2039 renderError={renderUpdateError}>
2040 <BrokenComponentDidUpdate errorText="E3" />
2041 <BrokenComponentDidUpdate errorText="E4" />
2042 </ErrorBoundary>
2043 </ErrorBoundary>,
2044 container,
2045 );
2046
2047 expect(container.firstChild.textContent).toBe(
2048 'Caught an unmounting error: E2.' + 'Caught an updating error: E4.',
2049 );
2050 expect(log).toEqual([
2051 // Begin update phase
2052 'OuterErrorBoundary componentWillReceiveProps',
2053 'OuterErrorBoundary componentWillUpdate',
2054 'OuterErrorBoundary render success',
2055 'InnerUnmountBoundary componentWillReceiveProps',
2056 'InnerUnmountBoundary componentWillUpdate',
2057 'InnerUnmountBoundary render success',
2058 'InnerUpdateBoundary componentWillReceiveProps',
2059 'InnerUpdateBoundary componentWillUpdate',
2060 'InnerUpdateBoundary render success',
2061 // First come the updates
2062 'BrokenComponentDidUpdate componentWillReceiveProps',
2063 'BrokenComponentDidUpdate componentWillUpdate',
2064 'BrokenComponentDidUpdate render',
2065 'BrokenComponentDidUpdate componentWillReceiveProps',
2066 'BrokenComponentDidUpdate componentWillUpdate',
2067 'BrokenComponentDidUpdate render',
2068 // We're in commit phase now, deleting
2069 'BrokenComponentWillUnmount componentWillUnmount [!]',
2070 'BrokenComponentWillUnmount componentWillUnmount [!]',
2071 // Continue despite errors, handle them after commit is done
2072 'InnerUnmountBoundary componentDidUpdate',
2073 // We're still in commit phase, now calling update lifecycles
2074 'BrokenComponentDidUpdate componentDidUpdate [!]',
2075 // Again, continue despite errors, we'll handle them later
2076 'BrokenComponentDidUpdate componentDidUpdate [!]',
2077 'InnerUpdateBoundary componentDidUpdate',
2078 'OuterErrorBoundary componentDidUpdate',
2079 // After the commit phase, attempt to recover from any errors that
2080 // were captured
2081 'BrokenComponentDidUpdate componentWillUnmount',
2082 'BrokenComponentDidUpdate componentWillUnmount',
2083 'InnerUnmountBoundary componentDidCatch',
2084 'InnerUnmountBoundary componentDidCatch',
2085 'InnerUpdateBoundary componentDidCatch',
2086 'InnerUpdateBoundary componentDidCatch',
2087 'InnerUnmountBoundary componentWillUpdate',
2088 'InnerUnmountBoundary render error',
2089 'InnerUpdateBoundary componentWillUpdate',
2090 'InnerUpdateBoundary render error',
2091 'InnerUnmountBoundary componentDidUpdate',
2092 'InnerUpdateBoundary componentDidUpdate',
2093 ]);
2094
2095 log.length = 0;
2096 ReactDOM.unmountComponentAtNode(container);
2097 expect(log).toEqual([
2098 'OuterErrorBoundary componentWillUnmount',
2099 'InnerUnmountBoundary componentWillUnmount',
2100 'InnerUpdateBoundary componentWillUnmount',
2101 ]);
2102 });
2103
2104 // @gate !disableLegacyMode
2105 it('discards a bad root if the root component fails', async () => {
2106 const X = null;
2107 const Y = undefined;
2108
2109 await expect(async () => {
2110 const container = document.createElement('div');
2111 await act(() => {
2112 ReactDOM.render(<X />, container);
2113 });
2114 }).rejects.toThrow('got: null');
2115
2116 await expect(async () => {
2117 const container = document.createElement('div');
2118 await act(() => {
2119 ReactDOM.render(<Y />, container);
2120 });
2121 }).rejects.toThrow('got: undefined');
2122 });
2123
2124 // @gate !disableLegacyMode
2125 it('renders empty output if error boundary does not handle the error', async () => {
2126 const container = document.createElement('div');
2127 ReactDOM.render(
2128 <div>
2129 Sibling
2130 <NoopErrorBoundary>
2131 <BrokenRender />
2132 </NoopErrorBoundary>
2133 </div>,
2134 container,
2135 );
2136 assertConsoleErrorDev([
2137 'NoopErrorBoundary: Error boundaries should implement getDerivedStateFromError(). ' +
2138 'In that method, return a state update to display an error message or fallback UI.\n' +
2139 ' in NoopErrorBoundary (at **)',
2140 ]);
2141 expect(container.firstChild.textContent).toBe('Sibling');
2142 expect(log).toEqual([
2143 'NoopErrorBoundary constructor',
2144 'NoopErrorBoundary componentWillMount',
2145 'NoopErrorBoundary render',
2146 'BrokenRender constructor',
2147 'BrokenRender componentWillMount',
2148 'BrokenRender render [!]',
2149 // In Fiber, noop error boundaries render null
2150 'NoopErrorBoundary componentDidMount',
2151 'NoopErrorBoundary componentDidCatch',
2152 // Nothing happens.
2153 ]);
2154
2155 log.length = 0;
2156 ReactDOM.unmountComponentAtNode(container);
2157 expect(log).toEqual(['NoopErrorBoundary componentWillUnmount']);
2158 });
2159
2160 // @gate !disableLegacyMode
2161 it('passes first error when two errors happen in commit', async () => {
2162 const errors = [];
2163 class Parent extends React.Component {
2164 render() {
2165 return <Child />;
2166 }
2167 componentDidMount() {
2168 errors.push('parent sad');
2169 throw new Error('parent sad');
2170 }
2171 }
2172 class Child extends React.Component {
2173 render() {
2174 return <div />;
2175 }
2176 componentDidMount() {
2177 errors.push('child sad');
2178 throw new Error('child sad');
2179 }
2180 }
2181
2182 const container = document.createElement('div');
2183 await expect(async () => {
2184 await act(() => {
2185 // Here, we test the behavior where there is no error boundary and we
2186 // delegate to the host root.
2187 ReactDOM.render(<Parent />, container);
2188 });
2189 }).rejects.toThrow(
2190 expect.objectContaining({
2191 errors: [
2192 expect.objectContaining({message: 'child sad'}),
2193 expect.objectContaining({message: 'parent sad'}),
2194 ],
2195 }),
2196 );
2197
2198 expect(errors).toEqual(['child sad', 'parent sad']);
2199 });
2200
2201 // @gate !disableLegacyMode
2202 it('propagates uncaught error inside unbatched initial mount', async () => {
2203 function Foo() {
2204 throw new Error('foo error');
2205 }
2206 const container = document.createElement('div');
2207 await expect(async () => {
2208 await act(() => {
2209 ReactDOM.unstable_batchedUpdates(() => {
2210 ReactDOM.render(<Foo />, container);
2211 });
2212 });
2213 }).rejects.toThrow('foo error');
2214 });
2215
2216 // @gate !disableLegacyMode
2217 it('handles errors that occur in before-mutation commit hook', async () => {
2218 const errors = [];
2219 class Parent extends React.Component {
2220 getSnapshotBeforeUpdate() {
2221 errors.push('parent sad');
2222 throw new Error('parent sad');
2223 }
2224 componentDidUpdate() {}
2225 render() {
2226 return <Child {...this.props} />;
2227 }
2228 }
2229 class Child extends React.Component {
2230 getSnapshotBeforeUpdate() {
2231 errors.push('child sad');
2232 throw new Error('child sad');
2233 }
2234 componentDidUpdate() {}
2235 render() {
2236 return <div />;
2237 }
2238 }
2239
2240 const container = document.createElement('div');
2241 await act(() => {
2242 ReactDOM.render(<Parent value={1} />, container);
2243 });
2244
2245 await expect(async () => {
2246 await act(() => {
2247 ReactDOM.render(<Parent value={2} />, container);
2248 });
2249 }).rejects.toThrow(
2250 expect.objectContaining({
2251 errors: [
2252 expect.objectContaining({message: 'child sad'}),
2253 expect.objectContaining({message: 'parent sad'}),
2254 ],
2255 }),
2256 );
2257
2258 expect(errors).toEqual(['child sad', 'parent sad']);
2259 // Error should be the first thrown
2260 });
2261 });