main
js 2,867 lines 91.9 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 ReactDOMClient;
16 let act;
17 let Scheduler;
18
19 describe('ReactErrorBoundaries', () => {
20 let BrokenConstructor;
21 let BrokenComponentWillMount;
22 let BrokenComponentDidMount;
23 let BrokenComponentWillReceiveProps;
24 let BrokenComponentWillUpdate;
25 let BrokenComponentDidUpdate;
26 let BrokenComponentWillUnmount;
27 let BrokenRenderErrorBoundary;
28 let BrokenComponentWillMountErrorBoundary;
29 let BrokenComponentDidMountErrorBoundary;
30 let BrokenRender;
31 let BrokenUseEffect;
32 let BrokenUseLayoutEffect;
33 let ErrorBoundary;
34 let ErrorMessage;
35 let NoopErrorBoundary;
36 let RetryErrorBoundary;
37 let Normal;
38 let assertLog;
39 let assertConsoleErrorDev;
40
41 beforeEach(() => {
42 jest.useFakeTimers();
43 jest.resetModules();
44 PropTypes = require('prop-types');
45 ReactDOM = require('react-dom');
46 ReactDOMClient = require('react-dom/client');
47 React = require('react');
48 act = require('internal-test-utils').act;
49 Scheduler = require('scheduler');
50
51 ({assertLog, assertConsoleErrorDev} = require('internal-test-utils'));
52
53 BrokenConstructor = class extends React.Component {
54 constructor(props) {
55 super(props);
56 Scheduler.log('BrokenConstructor constructor [!]');
57 throw new Error('Hello');
58 }
59 render() {
60 Scheduler.log('BrokenConstructor render');
61 return <div>{this.props.children}</div>;
62 }
63 UNSAFE_componentWillMount() {
64 Scheduler.log('BrokenConstructor componentWillMount');
65 }
66 componentDidMount() {
67 Scheduler.log('BrokenConstructor componentDidMount');
68 }
69 UNSAFE_componentWillReceiveProps() {
70 Scheduler.log('BrokenConstructor componentWillReceiveProps');
71 }
72 UNSAFE_componentWillUpdate() {
73 Scheduler.log('BrokenConstructor componentWillUpdate');
74 }
75 componentDidUpdate() {
76 Scheduler.log('BrokenConstructor componentDidUpdate');
77 }
78 componentWillUnmount() {
79 Scheduler.log('BrokenConstructor componentWillUnmount');
80 }
81 };
82
83 BrokenComponentWillMount = class extends React.Component {
84 constructor(props) {
85 super(props);
86 Scheduler.log('BrokenComponentWillMount constructor');
87 }
88 render() {
89 Scheduler.log('BrokenComponentWillMount render');
90 return <div>{this.props.children}</div>;
91 }
92 UNSAFE_componentWillMount() {
93 Scheduler.log('BrokenComponentWillMount componentWillMount [!]');
94 throw new Error('Hello');
95 }
96 componentDidMount() {
97 Scheduler.log('BrokenComponentWillMount componentDidMount');
98 }
99 UNSAFE_componentWillReceiveProps() {
100 Scheduler.log('BrokenComponentWillMount componentWillReceiveProps');
101 }
102 UNSAFE_componentWillUpdate() {
103 Scheduler.log('BrokenComponentWillMount componentWillUpdate');
104 }
105 componentDidUpdate() {
106 Scheduler.log('BrokenComponentWillMount componentDidUpdate');
107 }
108 componentWillUnmount() {
109 Scheduler.log('BrokenComponentWillMount componentWillUnmount');
110 }
111 };
112
113 BrokenComponentDidMount = class extends React.Component {
114 constructor(props) {
115 super(props);
116 Scheduler.log('BrokenComponentDidMount constructor');
117 }
118 render() {
119 Scheduler.log('BrokenComponentDidMount render');
120 return <div>{this.props.children}</div>;
121 }
122 UNSAFE_componentWillMount() {
123 Scheduler.log('BrokenComponentDidMount componentWillMount');
124 }
125 componentDidMount() {
126 Scheduler.log('BrokenComponentDidMount componentDidMount [!]');
127 throw new Error('Hello');
128 }
129 UNSAFE_componentWillReceiveProps() {
130 Scheduler.log('BrokenComponentDidMount componentWillReceiveProps');
131 }
132 UNSAFE_componentWillUpdate() {
133 Scheduler.log('BrokenComponentDidMount componentWillUpdate');
134 }
135 componentDidUpdate() {
136 Scheduler.log('BrokenComponentDidMount componentDidUpdate');
137 }
138 componentWillUnmount() {
139 Scheduler.log('BrokenComponentDidMount componentWillUnmount');
140 }
141 };
142
143 BrokenComponentWillReceiveProps = class extends React.Component {
144 constructor(props) {
145 super(props);
146 Scheduler.log('BrokenComponentWillReceiveProps constructor');
147 }
148 render() {
149 Scheduler.log('BrokenComponentWillReceiveProps render');
150 return <div>{this.props.children}</div>;
151 }
152 UNSAFE_componentWillMount() {
153 Scheduler.log('BrokenComponentWillReceiveProps componentWillMount');
154 }
155 componentDidMount() {
156 Scheduler.log('BrokenComponentWillReceiveProps componentDidMount');
157 }
158 UNSAFE_componentWillReceiveProps() {
159 Scheduler.log(
160 'BrokenComponentWillReceiveProps componentWillReceiveProps [!]',
161 );
162 throw new Error('Hello');
163 }
164 UNSAFE_componentWillUpdate() {
165 Scheduler.log('BrokenComponentWillReceiveProps componentWillUpdate');
166 }
167 componentDidUpdate() {
168 Scheduler.log('BrokenComponentWillReceiveProps componentDidUpdate');
169 }
170 componentWillUnmount() {
171 Scheduler.log('BrokenComponentWillReceiveProps componentWillUnmount');
172 }
173 };
174
175 BrokenComponentWillUpdate = class extends React.Component {
176 constructor(props) {
177 super(props);
178 Scheduler.log('BrokenComponentWillUpdate constructor');
179 }
180 render() {
181 Scheduler.log('BrokenComponentWillUpdate render');
182 return <div>{this.props.children}</div>;
183 }
184 UNSAFE_componentWillMount() {
185 Scheduler.log('BrokenComponentWillUpdate componentWillMount');
186 }
187 componentDidMount() {
188 Scheduler.log('BrokenComponentWillUpdate componentDidMount');
189 }
190 UNSAFE_componentWillReceiveProps() {
191 Scheduler.log('BrokenComponentWillUpdate componentWillReceiveProps');
192 }
193 UNSAFE_componentWillUpdate() {
194 Scheduler.log('BrokenComponentWillUpdate componentWillUpdate [!]');
195 throw new Error('Hello');
196 }
197 componentDidUpdate() {
198 Scheduler.log('BrokenComponentWillUpdate componentDidUpdate');
199 }
200 componentWillUnmount() {
201 Scheduler.log('BrokenComponentWillUpdate componentWillUnmount');
202 }
203 };
204
205 BrokenComponentDidUpdate = class extends React.Component {
206 static defaultProps = {
207 errorText: 'Hello',
208 };
209 constructor(props) {
210 super(props);
211 Scheduler.log('BrokenComponentDidUpdate constructor');
212 }
213 render() {
214 Scheduler.log('BrokenComponentDidUpdate render');
215 return <div>{this.props.children}</div>;
216 }
217 UNSAFE_componentWillMount() {
218 Scheduler.log('BrokenComponentDidUpdate componentWillMount');
219 }
220 componentDidMount() {
221 Scheduler.log('BrokenComponentDidUpdate componentDidMount');
222 }
223 UNSAFE_componentWillReceiveProps() {
224 Scheduler.log('BrokenComponentDidUpdate componentWillReceiveProps');
225 }
226 UNSAFE_componentWillUpdate() {
227 Scheduler.log('BrokenComponentDidUpdate componentWillUpdate');
228 }
229 componentDidUpdate() {
230 Scheduler.log('BrokenComponentDidUpdate componentDidUpdate [!]');
231 throw new Error(this.props.errorText);
232 }
233 componentWillUnmount() {
234 Scheduler.log('BrokenComponentDidUpdate componentWillUnmount');
235 }
236 };
237
238 BrokenComponentWillUnmount = class extends React.Component {
239 static defaultProps = {
240 errorText: 'Hello',
241 };
242 constructor(props) {
243 super(props);
244 Scheduler.log('BrokenComponentWillUnmount constructor');
245 }
246 render() {
247 Scheduler.log('BrokenComponentWillUnmount render');
248 return <div>{this.props.children}</div>;
249 }
250 UNSAFE_componentWillMount() {
251 Scheduler.log('BrokenComponentWillUnmount componentWillMount');
252 }
253 componentDidMount() {
254 Scheduler.log('BrokenComponentWillUnmount componentDidMount');
255 }
256 UNSAFE_componentWillReceiveProps() {
257 Scheduler.log('BrokenComponentWillUnmount componentWillReceiveProps');
258 }
259 UNSAFE_componentWillUpdate() {
260 Scheduler.log('BrokenComponentWillUnmount componentWillUpdate');
261 }
262 componentDidUpdate() {
263 Scheduler.log('BrokenComponentWillUnmount componentDidUpdate');
264 }
265 componentWillUnmount() {
266 Scheduler.log('BrokenComponentWillUnmount componentWillUnmount [!]');
267 throw new Error(this.props.errorText);
268 }
269 };
270
271 BrokenComponentWillMountErrorBoundary = class extends React.Component {
272 constructor(props) {
273 super(props);
274 this.state = {error: null};
275 Scheduler.log('BrokenComponentWillMountErrorBoundary constructor');
276 }
277 render() {
278 if (this.state.error) {
279 Scheduler.log('BrokenComponentWillMountErrorBoundary render error');
280 return <div>Caught an error: {this.state.error.message}.</div>;
281 }
282 Scheduler.log('BrokenComponentWillMountErrorBoundary render success');
283 return <div>{this.props.children}</div>;
284 }
285 UNSAFE_componentWillMount() {
286 Scheduler.log(
287 'BrokenComponentWillMountErrorBoundary componentWillMount [!]',
288 );
289 throw new Error('Hello');
290 }
291 componentDidMount() {
292 Scheduler.log(
293 'BrokenComponentWillMountErrorBoundary componentDidMount',
294 );
295 }
296 componentWillUnmount() {
297 Scheduler.log(
298 'BrokenComponentWillMountErrorBoundary componentWillUnmount',
299 );
300 }
301 static getDerivedStateFromError(error) {
302 Scheduler.log(
303 'BrokenComponentWillMountErrorBoundary static getDerivedStateFromError',
304 );
305 return {error};
306 }
307 };
308
309 BrokenComponentDidMountErrorBoundary = class extends React.Component {
310 constructor(props) {
311 super(props);
312 this.state = {error: null};
313 Scheduler.log('BrokenComponentDidMountErrorBoundary constructor');
314 }
315 render() {
316 if (this.state.error) {
317 Scheduler.log('BrokenComponentDidMountErrorBoundary render error');
318 return <div>Caught an error: {this.state.error.message}.</div>;
319 }
320 Scheduler.log('BrokenComponentDidMountErrorBoundary render success');
321 return <div>{this.props.children}</div>;
322 }
323 UNSAFE_componentWillMount() {
324 Scheduler.log(
325 'BrokenComponentDidMountErrorBoundary componentWillMount',
326 );
327 }
328 componentDidMount() {
329 Scheduler.log(
330 'BrokenComponentDidMountErrorBoundary componentDidMount [!]',
331 );
332 throw new Error('Hello');
333 }
334 componentWillUnmount() {
335 Scheduler.log(
336 'BrokenComponentDidMountErrorBoundary componentWillUnmount',
337 );
338 }
339 static getDerivedStateFromError(error) {
340 Scheduler.log(
341 'BrokenComponentDidMountErrorBoundary static getDerivedStateFromError',
342 );
343 return {error};
344 }
345 };
346
347 BrokenRenderErrorBoundary = class extends React.Component {
348 constructor(props) {
349 super(props);
350 this.state = {error: null};
351 Scheduler.log('BrokenRenderErrorBoundary constructor');
352 }
353 render() {
354 if (this.state.error) {
355 Scheduler.log('BrokenRenderErrorBoundary render error [!]');
356 throw new Error('Hello');
357 }
358 Scheduler.log('BrokenRenderErrorBoundary render success');
359 return <div>{this.props.children}</div>;
360 }
361 UNSAFE_componentWillMount() {
362 Scheduler.log('BrokenRenderErrorBoundary componentWillMount');
363 }
364 componentDidMount() {
365 Scheduler.log('BrokenRenderErrorBoundary componentDidMount');
366 }
367 componentWillUnmount() {
368 Scheduler.log('BrokenRenderErrorBoundary componentWillUnmount');
369 }
370 static getDerivedStateFromError(error) {
371 Scheduler.log(
372 'BrokenRenderErrorBoundary static getDerivedStateFromError',
373 );
374 return {error};
375 }
376 };
377
378 BrokenRender = class extends React.Component {
379 constructor(props) {
380 super(props);
381 Scheduler.log('BrokenRender constructor');
382 }
383 render() {
384 Scheduler.log('BrokenRender render [!]');
385 throw new Error('Hello');
386 }
387 UNSAFE_componentWillMount() {
388 Scheduler.log('BrokenRender componentWillMount');
389 }
390 componentDidMount() {
391 Scheduler.log('BrokenRender componentDidMount');
392 }
393 UNSAFE_componentWillReceiveProps() {
394 Scheduler.log('BrokenRender componentWillReceiveProps');
395 }
396 UNSAFE_componentWillUpdate() {
397 Scheduler.log('BrokenRender componentWillUpdate');
398 }
399 componentDidUpdate() {
400 Scheduler.log('BrokenRender componentDidUpdate');
401 }
402 componentWillUnmount() {
403 Scheduler.log('BrokenRender componentWillUnmount');
404 }
405 };
406
407 BrokenUseEffect = ({children}) => {
408 Scheduler.log('BrokenUseEffect render');
409
410 React.useEffect(() => {
411 Scheduler.log('BrokenUseEffect useEffect [!]');
412 throw new Error('Hello');
413 });
414
415 return children;
416 };
417
418 BrokenUseLayoutEffect = ({children}) => {
419 Scheduler.log('BrokenUseLayoutEffect render');
420
421 React.useLayoutEffect(() => {
422 Scheduler.log('BrokenUseLayoutEffect useLayoutEffect [!]');
423 throw new Error('Hello');
424 });
425
426 return children;
427 };
428
429 NoopErrorBoundary = class extends React.Component {
430 constructor(props) {
431 super(props);
432 Scheduler.log('NoopErrorBoundary constructor');
433 }
434 render() {
435 Scheduler.log('NoopErrorBoundary render');
436 return <BrokenRender />;
437 }
438 UNSAFE_componentWillMount() {
439 Scheduler.log('NoopErrorBoundary componentWillMount');
440 }
441 componentDidMount() {
442 Scheduler.log('NoopErrorBoundary componentDidMount');
443 }
444 componentWillUnmount() {
445 Scheduler.log('NoopErrorBoundary componentWillUnmount');
446 }
447 static getDerivedStateFromError() {
448 Scheduler.log('NoopErrorBoundary static getDerivedStateFromError');
449 }
450 };
451
452 Normal = class extends React.Component {
453 static defaultProps = {
454 logName: 'Normal',
455 };
456 constructor(props) {
457 super(props);
458 Scheduler.log(`${this.props.logName} constructor`);
459 }
460 render() {
461 Scheduler.log(`${this.props.logName} render`);
462 return <div>{this.props.children}</div>;
463 }
464 UNSAFE_componentWillMount() {
465 Scheduler.log(`${this.props.logName} componentWillMount`);
466 }
467 componentDidMount() {
468 Scheduler.log(`${this.props.logName} componentDidMount`);
469 }
470 UNSAFE_componentWillReceiveProps() {
471 Scheduler.log(`${this.props.logName} componentWillReceiveProps`);
472 }
473 UNSAFE_componentWillUpdate() {
474 Scheduler.log(`${this.props.logName} componentWillUpdate`);
475 }
476 componentDidUpdate() {
477 Scheduler.log(`${this.props.logName} componentDidUpdate`);
478 }
479 componentWillUnmount() {
480 Scheduler.log(`${this.props.logName} componentWillUnmount`);
481 }
482 };
483
484 ErrorBoundary = class extends React.Component {
485 constructor(props) {
486 super(props);
487 this.state = {error: null};
488 Scheduler.log(`${this.props.logName} constructor`);
489 }
490 render() {
491 if (this.state.error && !this.props.forceRetry) {
492 Scheduler.log(`${this.props.logName} render error`);
493 return this.props.renderError(this.state.error, this.props);
494 }
495 Scheduler.log(`${this.props.logName} render success`);
496 return <div>{this.props.children}</div>;
497 }
498 static getDerivedStateFromError(error) {
499 Scheduler.log('ErrorBoundary static getDerivedStateFromError');
500 return {error};
501 }
502 UNSAFE_componentWillMount() {
503 Scheduler.log(`${this.props.logName} componentWillMount`);
504 }
505 componentDidMount() {
506 Scheduler.log(`${this.props.logName} componentDidMount`);
507 }
508 UNSAFE_componentWillReceiveProps() {
509 Scheduler.log(`${this.props.logName} componentWillReceiveProps`);
510 }
511 UNSAFE_componentWillUpdate() {
512 Scheduler.log(`${this.props.logName} componentWillUpdate`);
513 }
514 componentDidUpdate() {
515 Scheduler.log(`${this.props.logName} componentDidUpdate`);
516 }
517 componentWillUnmount() {
518 Scheduler.log(`${this.props.logName} componentWillUnmount`);
519 }
520 };
521 ErrorBoundary.defaultProps = {
522 logName: 'ErrorBoundary',
523 renderError(error, props) {
524 return (
525 <div ref={props.errorMessageRef}>
526 Caught an error: {error.message}.
527 </div>
528 );
529 },
530 };
531
532 RetryErrorBoundary = class extends React.Component {
533 constructor(props) {
534 super(props);
535 Scheduler.log('RetryErrorBoundary constructor');
536 }
537 render() {
538 Scheduler.log('RetryErrorBoundary render');
539 return <BrokenRender />;
540 }
541 UNSAFE_componentWillMount() {
542 Scheduler.log('RetryErrorBoundary componentWillMount');
543 }
544 componentDidMount() {
545 Scheduler.log('RetryErrorBoundary componentDidMount');
546 }
547 componentWillUnmount() {
548 Scheduler.log('RetryErrorBoundary componentWillUnmount');
549 }
550 static getDerivedStateFromError(error) {
551 Scheduler.log('RetryErrorBoundary static getDerivedStateFromError [!]');
552 // In Fiber, calling setState() (and failing) is treated as a rethrow.
553 return {};
554 }
555 };
556
557 ErrorMessage = class extends React.Component {
558 constructor(props) {
559 super(props);
560 Scheduler.log('ErrorMessage constructor');
561 }
562 UNSAFE_componentWillMount() {
563 Scheduler.log('ErrorMessage componentWillMount');
564 }
565 componentDidMount() {
566 Scheduler.log('ErrorMessage componentDidMount');
567 }
568 componentWillUnmount() {
569 Scheduler.log('ErrorMessage componentWillUnmount');
570 }
571 render() {
572 Scheduler.log('ErrorMessage render');
573 return <div>Caught an error: {this.props.message}.</div>;
574 }
575 };
576 });
577
578 it('does not swallow exceptions on mounting without boundaries', async () => {
579 let container = document.createElement('div');
580 let root = ReactDOMClient.createRoot(container);
581 await expect(async () => {
582 await act(async () => {
583 root.render(<BrokenRender />);
584 });
585 }).rejects.toThrow('Hello');
586
587 Scheduler.unstable_clearLog();
588 container = document.createElement('div');
589 root = ReactDOMClient.createRoot(container);
590 await expect(async () => {
591 await act(async () => {
592 root.render(<BrokenComponentWillMount />);
593 });
594 }).rejects.toThrow('Hello');
595
596 Scheduler.unstable_clearLog();
597 container = document.createElement('div');
598 root = ReactDOMClient.createRoot(container);
599 await expect(async () => {
600 await act(async () => {
601 root.render(<BrokenComponentDidMount />);
602 });
603 }).rejects.toThrow('Hello');
604 });
605
606 it('does not swallow exceptions on updating without boundaries', async () => {
607 let container = document.createElement('div');
608 let root = ReactDOMClient.createRoot(container);
609 await act(async () => {
610 root.render(<BrokenComponentWillUpdate />);
611 });
612 Scheduler.unstable_clearLog();
613 await expect(async () => {
614 await act(async () => {
615 root.render(<BrokenComponentWillUpdate />);
616 });
617 }).rejects.toThrow('Hello');
618
619 Scheduler.unstable_clearLog();
620 container = document.createElement('div');
621 root = ReactDOMClient.createRoot(container);
622 await act(async () => {
623 root.render(<BrokenComponentWillReceiveProps />);
624 });
625 Scheduler.unstable_clearLog();
626 await expect(async () => {
627 await act(async () => {
628 root.render(<BrokenComponentWillReceiveProps />);
629 });
630 }).rejects.toThrow('Hello');
631 Scheduler.unstable_clearLog();
632 container = document.createElement('div');
633 root = ReactDOMClient.createRoot(container);
634 await act(async () => {
635 root.render(<BrokenComponentDidUpdate />);
636 });
637 Scheduler.unstable_clearLog();
638 await expect(async () => {
639 await act(async () => {
640 root.render(<BrokenComponentDidUpdate />);
641 });
642 }).rejects.toThrow('Hello');
643 });
644
645 it('does not swallow exceptions on unmounting without boundaries', async () => {
646 const container = document.createElement('div');
647 const root = ReactDOMClient.createRoot(container);
648 await act(async () => {
649 root.render(<BrokenComponentWillUnmount />);
650 });
651 Scheduler.unstable_clearLog();
652 await expect(async () => {
653 await act(() => root.unmount());
654 }).rejects.toThrow('Hello');
655 });
656
657 it('prevents errors from leaking into other roots', async () => {
658 const container1 = document.createElement('div');
659 const root1 = ReactDOMClient.createRoot(container1);
660 const container2 = document.createElement('div');
661 const root2 = ReactDOMClient.createRoot(container2);
662 const container3 = document.createElement('div');
663 const root3 = ReactDOMClient.createRoot(container3);
664
665 await act(async () => {
666 root1.render(<span>Before 1</span>);
667 });
668 await expect(async () => {
669 await act(async () => {
670 root2.render(<BrokenRender />);
671 });
672 }).rejects.toThrow('Hello');
673
674 assertLog([
675 'BrokenRender constructor',
676 'BrokenRender componentWillMount',
677 'BrokenRender render [!]',
678 'BrokenRender constructor',
679 'BrokenRender componentWillMount',
680 'BrokenRender render [!]',
681 ]);
682 await act(async () => {
683 root3.render(
684 <ErrorBoundary>
685 <BrokenRender />
686 </ErrorBoundary>,
687 );
688 });
689 expect(container1.firstChild.textContent).toBe('Before 1');
690 expect(container2.firstChild).toBe(null);
691 expect(container3.firstChild.textContent).toBe('Caught an error: Hello.');
692
693 Scheduler.unstable_clearLog();
694 await act(async () => {
695 root1.render(<span>After 1</span>);
696 });
697 Scheduler.unstable_clearLog();
698 await act(async () => {
699 root2.render(<span>After 2</span>);
700 });
701 Scheduler.unstable_clearLog();
702 await act(async () => {
703 root3.render(<ErrorBoundary forceRetry={true}>After 3</ErrorBoundary>);
704 });
705 expect(container1.firstChild.textContent).toBe('After 1');
706 expect(container2.firstChild.textContent).toBe('After 2');
707 expect(container3.firstChild.textContent).toBe('After 3');
708 root1.unmount();
709 root2.unmount();
710 root3.unmount();
711 expect(container1.firstChild).toBe(null);
712 expect(container2.firstChild).toBe(null);
713 expect(container3.firstChild).toBe(null);
714 });
715
716 it('logs a single error when using error boundary', async () => {
717 const container = document.createElement('div');
718 const root = ReactDOMClient.createRoot(container);
719 spyOnDev(console, 'error');
720 await act(async () => {
721 root.render(
722 <ErrorBoundary>
723 <BrokenRender />
724 </ErrorBoundary>,
725 );
726 });
727 if (__DEV__) {
728 expect(console.error).toHaveBeenCalledTimes(1);
729 expect(console.error.mock.calls[0][2]).toContain(
730 'The above error occurred in the <BrokenRender> component',
731 );
732 }
733
734 expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
735 assertLog([
736 'ErrorBoundary constructor',
737 'ErrorBoundary componentWillMount',
738 'ErrorBoundary render success',
739 'BrokenRender constructor',
740 'BrokenRender componentWillMount',
741 'BrokenRender render [!]',
742 // Catch and render an error message
743 'ErrorBoundary static getDerivedStateFromError',
744 'ErrorBoundary componentWillMount',
745 'ErrorBoundary render error',
746 // logs for error retry
747 'ErrorBoundary constructor',
748 'ErrorBoundary componentWillMount',
749 'ErrorBoundary render success',
750 'BrokenRender constructor',
751 'BrokenRender componentWillMount',
752 'BrokenRender render [!]',
753 'ErrorBoundary static getDerivedStateFromError',
754 'ErrorBoundary componentWillMount',
755 'ErrorBoundary render error',
756 'ErrorBoundary componentDidMount',
757 ]);
758
759 root.unmount();
760 assertLog(['ErrorBoundary componentWillUnmount']);
761 });
762
763 it('renders an error state if child throws in render', async () => {
764 const container = document.createElement('div');
765 const root = ReactDOMClient.createRoot(container);
766 await act(async () => {
767 root.render(
768 <ErrorBoundary>
769 <BrokenRender />
770 </ErrorBoundary>,
771 );
772 });
773 expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
774 assertLog([
775 'ErrorBoundary constructor',
776 'ErrorBoundary componentWillMount',
777 'ErrorBoundary render success',
778 'BrokenRender constructor',
779 'BrokenRender componentWillMount',
780 'BrokenRender render [!]',
781 // Catch and render an error message
782 'ErrorBoundary static getDerivedStateFromError',
783 'ErrorBoundary componentWillMount',
784 'ErrorBoundary render error',
785 // logs for error retry
786 'ErrorBoundary constructor',
787 'ErrorBoundary componentWillMount',
788 'ErrorBoundary render success',
789 'BrokenRender constructor',
790 'BrokenRender componentWillMount',
791 'BrokenRender render [!]',
792 'ErrorBoundary static getDerivedStateFromError',
793 'ErrorBoundary componentWillMount',
794 'ErrorBoundary render error',
795 'ErrorBoundary componentDidMount',
796 ]);
797
798 root.unmount();
799 assertLog(['ErrorBoundary componentWillUnmount']);
800 });
801
802 it('renders an error state if child throws in constructor', async () => {
803 const container = document.createElement('div');
804 const root = ReactDOMClient.createRoot(container);
805 await act(async () => {
806 root.render(
807 <ErrorBoundary>
808 <BrokenConstructor />
809 </ErrorBoundary>,
810 );
811 });
812 expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
813 assertLog([
814 'ErrorBoundary constructor',
815 'ErrorBoundary componentWillMount',
816 'ErrorBoundary render success',
817 'BrokenConstructor constructor [!]',
818 // Catch and render an error message
819 'ErrorBoundary static getDerivedStateFromError',
820 'ErrorBoundary componentWillMount',
821 'ErrorBoundary render error',
822 // logs for error retry
823 'ErrorBoundary constructor',
824 'ErrorBoundary componentWillMount',
825 'ErrorBoundary render success',
826 'BrokenConstructor constructor [!]',
827 'ErrorBoundary static getDerivedStateFromError',
828 'ErrorBoundary componentWillMount',
829 'ErrorBoundary render error',
830 'ErrorBoundary componentDidMount',
831 ]);
832
833 root.unmount();
834 assertLog(['ErrorBoundary componentWillUnmount']);
835 });
836
837 it('renders an error state if child throws in componentWillMount', async () => {
838 const container = document.createElement('div');
839 const root = ReactDOMClient.createRoot(container);
840 await act(async () => {
841 root.render(
842 <ErrorBoundary>
843 <BrokenComponentWillMount />
844 </ErrorBoundary>,
845 );
846 });
847 expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
848 assertLog([
849 'ErrorBoundary constructor',
850 'ErrorBoundary componentWillMount',
851 'ErrorBoundary render success',
852 'BrokenComponentWillMount constructor',
853 'BrokenComponentWillMount componentWillMount [!]',
854 // Catch and render an error message
855 'ErrorBoundary static getDerivedStateFromError',
856 'ErrorBoundary componentWillMount',
857 'ErrorBoundary render error',
858 // logs for error retry
859 'ErrorBoundary constructor',
860 'ErrorBoundary componentWillMount',
861 'ErrorBoundary render success',
862 'BrokenComponentWillMount constructor',
863 'BrokenComponentWillMount componentWillMount [!]',
864 'ErrorBoundary static getDerivedStateFromError',
865 'ErrorBoundary componentWillMount',
866 'ErrorBoundary render error',
867 'ErrorBoundary componentDidMount',
868 ]);
869
870 root.unmount();
871 assertLog(['ErrorBoundary componentWillUnmount']);
872 });
873
874 // @gate !disableLegacyContext || !__DEV__
875 it('renders an error state if context provider throws in componentWillMount', async () => {
876 class BrokenComponentWillMountWithContext extends React.Component {
877 static childContextTypes = {foo: PropTypes.number};
878 getChildContext() {
879 return {foo: 42};
880 }
881 render() {
882 return <div>{this.props.children}</div>;
883 }
884 UNSAFE_componentWillMount() {
885 throw new Error('Hello');
886 }
887 }
888
889 const container = document.createElement('div');
890 const root = ReactDOMClient.createRoot(container);
891 await act(async () => {
892 root.render(
893 <ErrorBoundary>
894 <BrokenComponentWillMountWithContext />
895 </ErrorBoundary>,
896 );
897 });
898 assertConsoleErrorDev([
899 'BrokenComponentWillMountWithContext uses the legacy childContextTypes API which will soon be removed. ' +
900 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
901 ' in BrokenComponentWillMountWithContext (at **)',
902 ]);
903 expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
904 });
905
906 it('mounts the error message if mounting fails', async () => {
907 function renderError(error) {
908 return <ErrorMessage message={error.message} />;
909 }
910
911 const container = document.createElement('div');
912 const root = ReactDOMClient.createRoot(container);
913 await act(async () => {
914 root.render(
915 <ErrorBoundary renderError={renderError}>
916 <BrokenRender />
917 </ErrorBoundary>,
918 );
919 });
920 assertLog([
921 'ErrorBoundary constructor',
922 'ErrorBoundary componentWillMount',
923 'ErrorBoundary render success',
924 'BrokenRender constructor',
925 'BrokenRender componentWillMount',
926 'BrokenRender render [!]',
927 'ErrorBoundary static getDerivedStateFromError',
928 'ErrorBoundary componentWillMount',
929 'ErrorBoundary render error',
930 'ErrorMessage constructor',
931 'ErrorMessage componentWillMount',
932 'ErrorMessage render',
933 // logs for error retry
934 'ErrorBoundary constructor',
935 'ErrorBoundary componentWillMount',
936 'ErrorBoundary render success',
937 'BrokenRender constructor',
938 'BrokenRender componentWillMount',
939 'BrokenRender render [!]',
940 'ErrorBoundary static getDerivedStateFromError',
941 'ErrorBoundary componentWillMount',
942 'ErrorBoundary render error',
943 'ErrorMessage constructor',
944 'ErrorMessage componentWillMount',
945 'ErrorMessage render',
946 'ErrorMessage componentDidMount',
947 'ErrorBoundary componentDidMount',
948 ]);
949
950 root.unmount();
951 assertLog([
952 'ErrorBoundary componentWillUnmount',
953 'ErrorMessage componentWillUnmount',
954 ]);
955 });
956
957 it('propagates errors on retry on mounting', async () => {
958 const container = document.createElement('div');
959 const root = ReactDOMClient.createRoot(container);
960 await act(async () => {
961 root.render(
962 <ErrorBoundary>
963 <RetryErrorBoundary>
964 <BrokenRender />
965 </RetryErrorBoundary>
966 </ErrorBoundary>,
967 );
968 });
969 expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
970 assertLog([
971 'ErrorBoundary constructor',
972 'ErrorBoundary componentWillMount',
973 'ErrorBoundary render success',
974 'RetryErrorBoundary constructor',
975 'RetryErrorBoundary componentWillMount',
976 'RetryErrorBoundary render',
977 'BrokenRender constructor',
978 'BrokenRender componentWillMount',
979 'BrokenRender render [!]',
980 // Retry
981 'RetryErrorBoundary static getDerivedStateFromError [!]',
982 'RetryErrorBoundary componentWillMount',
983 'RetryErrorBoundary render',
984 'BrokenRender constructor',
985 'BrokenRender componentWillMount',
986 'BrokenRender render [!]',
987 // This time, the error propagates to the higher boundary
988 'ErrorBoundary static getDerivedStateFromError',
989 'ErrorBoundary componentWillMount',
990 'ErrorBoundary render error',
991 // logs for error retry
992 'ErrorBoundary constructor',
993 'ErrorBoundary componentWillMount',
994 'ErrorBoundary render success',
995 'RetryErrorBoundary constructor',
996 'RetryErrorBoundary componentWillMount',
997 'RetryErrorBoundary render',
998 'BrokenRender constructor',
999 'BrokenRender componentWillMount',
1000 'BrokenRender render [!]',
1001 'RetryErrorBoundary static getDerivedStateFromError [!]',
1002 'RetryErrorBoundary componentWillMount',
1003 'RetryErrorBoundary render',
1004 'BrokenRender constructor',
1005 'BrokenRender componentWillMount',
1006 'BrokenRender render [!]',
1007 'ErrorBoundary static getDerivedStateFromError',
1008 'ErrorBoundary componentWillMount',
1009 'ErrorBoundary render error',
1010 'ErrorBoundary componentDidMount',
1011 ]);
1012
1013 root.unmount();
1014 assertLog(['ErrorBoundary componentWillUnmount']);
1015 });
1016
1017 it('propagates errors inside boundary during componentWillMount', async () => {
1018 const container = document.createElement('div');
1019 const root = ReactDOMClient.createRoot(container);
1020 await act(async () => {
1021 root.render(
1022 <ErrorBoundary>
1023 <BrokenComponentWillMountErrorBoundary />
1024 </ErrorBoundary>,
1025 );
1026 });
1027 expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
1028 assertLog([
1029 'ErrorBoundary constructor',
1030 'ErrorBoundary componentWillMount',
1031 'ErrorBoundary render success',
1032 'BrokenComponentWillMountErrorBoundary constructor',
1033 'BrokenComponentWillMountErrorBoundary componentWillMount [!]',
1034 // The error propagates to the higher boundary
1035 'ErrorBoundary static getDerivedStateFromError',
1036 'ErrorBoundary componentWillMount',
1037 'ErrorBoundary render error',
1038 // logs for error retry
1039 'ErrorBoundary constructor',
1040 'ErrorBoundary componentWillMount',
1041 'ErrorBoundary render success',
1042 'BrokenComponentWillMountErrorBoundary constructor',
1043 'BrokenComponentWillMountErrorBoundary componentWillMount [!]',
1044 'ErrorBoundary static getDerivedStateFromError',
1045 'ErrorBoundary componentWillMount',
1046 'ErrorBoundary render error',
1047 'ErrorBoundary componentDidMount',
1048 ]);
1049
1050 root.unmount();
1051 assertLog(['ErrorBoundary componentWillUnmount']);
1052 });
1053
1054 it('propagates errors inside boundary while rendering error state', async () => {
1055 const container = document.createElement('div');
1056 const root = ReactDOMClient.createRoot(container);
1057 await act(async () => {
1058 root.render(
1059 <ErrorBoundary>
1060 <BrokenRenderErrorBoundary>
1061 <BrokenRender />
1062 </BrokenRenderErrorBoundary>
1063 </ErrorBoundary>,
1064 );
1065 });
1066 expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
1067 assertLog([
1068 'ErrorBoundary constructor',
1069 'ErrorBoundary componentWillMount',
1070 'ErrorBoundary render success',
1071 'BrokenRenderErrorBoundary constructor',
1072 'BrokenRenderErrorBoundary componentWillMount',
1073 'BrokenRenderErrorBoundary render success',
1074 'BrokenRender constructor',
1075 'BrokenRender componentWillMount',
1076 'BrokenRender render [!]',
1077 // Attempt to handle the error
1078 'BrokenRenderErrorBoundary static getDerivedStateFromError',
1079 'BrokenRenderErrorBoundary componentWillMount',
1080 'BrokenRenderErrorBoundary render error [!]',
1081 // Attempt to handle the error again
1082 'ErrorBoundary static getDerivedStateFromError',
1083 'ErrorBoundary componentWillMount',
1084 'ErrorBoundary render error',
1085 // logs for error retry
1086 'ErrorBoundary constructor',
1087 'ErrorBoundary componentWillMount',
1088 'ErrorBoundary render success',
1089 'BrokenRenderErrorBoundary constructor',
1090 'BrokenRenderErrorBoundary componentWillMount',
1091 'BrokenRenderErrorBoundary render success',
1092 'BrokenRender constructor',
1093 'BrokenRender componentWillMount',
1094 'BrokenRender render [!]',
1095 'BrokenRenderErrorBoundary static getDerivedStateFromError',
1096 'BrokenRenderErrorBoundary componentWillMount',
1097 'BrokenRenderErrorBoundary render error [!]',
1098 'ErrorBoundary static getDerivedStateFromError',
1099 'ErrorBoundary componentWillMount',
1100 'ErrorBoundary render error',
1101 'ErrorBoundary componentDidMount',
1102 ]);
1103
1104 root.unmount();
1105 assertLog(['ErrorBoundary componentWillUnmount']);
1106 });
1107
1108 it('does not call componentWillUnmount when aborting initial mount', async () => {
1109 const container = document.createElement('div');
1110 const root = ReactDOMClient.createRoot(container);
1111 await act(async () => {
1112 root.render(
1113 <ErrorBoundary>
1114 <Normal />
1115 <BrokenRender />
1116 <Normal />
1117 </ErrorBoundary>,
1118 );
1119 });
1120 expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
1121 assertLog([
1122 'ErrorBoundary constructor',
1123 'ErrorBoundary componentWillMount',
1124 'ErrorBoundary render success',
1125 // Render first child
1126 'Normal constructor',
1127 'Normal componentWillMount',
1128 'Normal render',
1129 // Render second child (it throws)
1130 'BrokenRender constructor',
1131 'BrokenRender componentWillMount',
1132 'BrokenRender render [!]',
1133 // Skip the remaining siblings
1134 // Handle the error
1135 'ErrorBoundary static getDerivedStateFromError',
1136 'ErrorBoundary componentWillMount',
1137 'ErrorBoundary render error',
1138 // logs for error retry
1139 'ErrorBoundary constructor',
1140 'ErrorBoundary componentWillMount',
1141 'ErrorBoundary render success',
1142 'Normal constructor',
1143 'Normal componentWillMount',
1144 'Normal render',
1145 'BrokenRender constructor',
1146 'BrokenRender componentWillMount',
1147 'BrokenRender render [!]',
1148 'ErrorBoundary static getDerivedStateFromError',
1149 'ErrorBoundary componentWillMount',
1150 'ErrorBoundary render error',
1151 'ErrorBoundary componentDidMount',
1152 ]);
1153
1154 root.unmount();
1155 assertLog(['ErrorBoundary componentWillUnmount']);
1156 });
1157
1158 it('resets callback refs if mounting aborts', async () => {
1159 function childRef(x) {
1160 Scheduler.log('Child ref is set to ' + x);
1161 }
1162 function errorMessageRef(x) {
1163 Scheduler.log('Error message ref is set to ' + x);
1164 }
1165
1166 const container = document.createElement('div');
1167 const root = ReactDOMClient.createRoot(container);
1168 await act(async () => {
1169 root.render(
1170 <ErrorBoundary errorMessageRef={errorMessageRef}>
1171 <div ref={childRef} />
1172 <BrokenRender />
1173 </ErrorBoundary>,
1174 );
1175 });
1176 expect(container.textContent).toBe('Caught an error: Hello.');
1177 assertLog([
1178 'ErrorBoundary constructor',
1179 'ErrorBoundary componentWillMount',
1180 'ErrorBoundary render success',
1181 'BrokenRender constructor',
1182 'BrokenRender componentWillMount',
1183 'BrokenRender render [!]',
1184 // Handle the error
1185 'ErrorBoundary static getDerivedStateFromError',
1186 'ErrorBoundary componentWillMount',
1187 'ErrorBoundary render error',
1188 // logs for error retry
1189 'ErrorBoundary constructor',
1190 'ErrorBoundary componentWillMount',
1191 'ErrorBoundary render success',
1192 'BrokenRender constructor',
1193 'BrokenRender componentWillMount',
1194 'BrokenRender render [!]',
1195 'ErrorBoundary static getDerivedStateFromError',
1196 'ErrorBoundary componentWillMount',
1197 'ErrorBoundary render error',
1198 'Error message ref is set to [object HTMLDivElement]',
1199 'ErrorBoundary componentDidMount',
1200 ]);
1201
1202 root.unmount();
1203 assertLog([
1204 'ErrorBoundary componentWillUnmount',
1205 'Error message ref is set to null',
1206 ]);
1207 });
1208
1209 it('resets object refs if mounting aborts', async () => {
1210 const childRef = React.createRef();
1211 const errorMessageRef = React.createRef();
1212
1213 const container = document.createElement('div');
1214 const root = ReactDOMClient.createRoot(container);
1215 await act(async () => {
1216 root.render(
1217 <ErrorBoundary errorMessageRef={errorMessageRef}>
1218 <div ref={childRef} />
1219 <BrokenRender />
1220 </ErrorBoundary>,
1221 );
1222 });
1223 expect(container.textContent).toBe('Caught an error: Hello.');
1224 assertLog([
1225 'ErrorBoundary constructor',
1226 'ErrorBoundary componentWillMount',
1227 'ErrorBoundary render success',
1228 'BrokenRender constructor',
1229 'BrokenRender componentWillMount',
1230 'BrokenRender render [!]',
1231 // Handle the error
1232 'ErrorBoundary static getDerivedStateFromError',
1233 'ErrorBoundary componentWillMount',
1234 'ErrorBoundary render error',
1235 // logs for error retry
1236 'ErrorBoundary constructor',
1237 'ErrorBoundary componentWillMount',
1238 'ErrorBoundary render success',
1239 'BrokenRender constructor',
1240 'BrokenRender componentWillMount',
1241 'BrokenRender render [!]',
1242 'ErrorBoundary static getDerivedStateFromError',
1243 'ErrorBoundary componentWillMount',
1244 'ErrorBoundary render error',
1245 'ErrorBoundary componentDidMount',
1246 ]);
1247 expect(errorMessageRef.current.toString()).toEqual(
1248 '[object HTMLDivElement]',
1249 );
1250
1251 root.unmount();
1252 assertLog(['ErrorBoundary componentWillUnmount']);
1253 expect(errorMessageRef.current).toEqual(null);
1254 });
1255
1256 it('successfully mounts if no error occurs', async () => {
1257 const container = document.createElement('div');
1258 const root = ReactDOMClient.createRoot(container);
1259 await act(async () => {
1260 root.render(
1261 <ErrorBoundary>
1262 <div>Mounted successfully.</div>
1263 </ErrorBoundary>,
1264 );
1265 });
1266 expect(container.firstChild.textContent).toBe('Mounted successfully.');
1267 assertLog([
1268 'ErrorBoundary constructor',
1269 'ErrorBoundary componentWillMount',
1270 'ErrorBoundary render success',
1271 'ErrorBoundary componentDidMount',
1272 ]);
1273
1274 root.unmount();
1275 assertLog(['ErrorBoundary componentWillUnmount']);
1276 });
1277
1278 it('catches if child throws in constructor during update', async () => {
1279 const container = document.createElement('div');
1280 const root = ReactDOMClient.createRoot(container);
1281 await act(async () => {
1282 root.render(
1283 <ErrorBoundary>
1284 <Normal />
1285 </ErrorBoundary>,
1286 );
1287 });
1288 Scheduler.unstable_clearLog();
1289 await act(async () => {
1290 root.render(
1291 <ErrorBoundary>
1292 <Normal />
1293 <Normal logName="Normal2" />
1294 <BrokenConstructor />
1295 </ErrorBoundary>,
1296 );
1297 });
1298 expect(container.textContent).toBe('Caught an error: Hello.');
1299 assertLog([
1300 'ErrorBoundary componentWillReceiveProps',
1301 'ErrorBoundary componentWillUpdate',
1302 'ErrorBoundary render success',
1303 'Normal componentWillReceiveProps',
1304 'Normal componentWillUpdate',
1305 'Normal render',
1306 // Normal2 will attempt to mount:
1307 'Normal2 constructor',
1308 'Normal2 componentWillMount',
1309 'Normal2 render',
1310 // BrokenConstructor will abort rendering:
1311 'BrokenConstructor constructor [!]',
1312 // Handle the error
1313 'ErrorBoundary static getDerivedStateFromError',
1314 // Render the error message
1315 'ErrorBoundary componentWillUpdate',
1316 'ErrorBoundary render error',
1317 // logs for error retry
1318 'ErrorBoundary componentWillReceiveProps',
1319 'ErrorBoundary componentWillUpdate',
1320 'ErrorBoundary render success',
1321 'Normal componentWillReceiveProps',
1322 'Normal componentWillUpdate',
1323 'Normal render',
1324 'Normal2 constructor',
1325 'Normal2 componentWillMount',
1326 'Normal2 render',
1327 'BrokenConstructor constructor [!]',
1328 'ErrorBoundary static getDerivedStateFromError',
1329 'ErrorBoundary componentWillUpdate',
1330 'ErrorBoundary render error',
1331 'Normal componentWillUnmount',
1332 'ErrorBoundary componentDidUpdate',
1333 ]);
1334
1335 root.unmount();
1336 assertLog(['ErrorBoundary componentWillUnmount']);
1337 });
1338
1339 it('catches if child throws in componentWillMount during update', async () => {
1340 const container = document.createElement('div');
1341 const root = ReactDOMClient.createRoot(container);
1342 await act(async () => {
1343 root.render(
1344 <ErrorBoundary>
1345 <Normal />
1346 </ErrorBoundary>,
1347 );
1348 });
1349
1350 Scheduler.unstable_clearLog();
1351 await act(async () => {
1352 root.render(
1353 <ErrorBoundary>
1354 <Normal />
1355 <Normal logName="Normal2" />
1356 <BrokenComponentWillMount />
1357 </ErrorBoundary>,
1358 );
1359 });
1360 expect(container.textContent).toBe('Caught an error: Hello.');
1361 assertLog([
1362 'ErrorBoundary componentWillReceiveProps',
1363 'ErrorBoundary componentWillUpdate',
1364 'ErrorBoundary render success',
1365 'Normal componentWillReceiveProps',
1366 'Normal componentWillUpdate',
1367 'Normal render',
1368 // Normal2 will attempt to mount:
1369 'Normal2 constructor',
1370 'Normal2 componentWillMount',
1371 'Normal2 render',
1372 // BrokenComponentWillMount will abort rendering:
1373 'BrokenComponentWillMount constructor',
1374 'BrokenComponentWillMount componentWillMount [!]',
1375 // Handle the error
1376 'ErrorBoundary static getDerivedStateFromError',
1377 // Render the error message
1378 'ErrorBoundary componentWillUpdate',
1379 'ErrorBoundary render error',
1380 // logs for error retry
1381 'ErrorBoundary componentWillReceiveProps',
1382 'ErrorBoundary componentWillUpdate',
1383 'ErrorBoundary render success',
1384 'Normal componentWillReceiveProps',
1385 'Normal componentWillUpdate',
1386 'Normal render',
1387 'Normal2 constructor',
1388 'Normal2 componentWillMount',
1389 'Normal2 render',
1390 'BrokenComponentWillMount constructor',
1391 'BrokenComponentWillMount componentWillMount [!]',
1392 'ErrorBoundary static getDerivedStateFromError',
1393 'ErrorBoundary componentWillUpdate',
1394 'ErrorBoundary render error',
1395 'Normal componentWillUnmount',
1396 'ErrorBoundary componentDidUpdate',
1397 ]);
1398
1399 root.unmount();
1400 assertLog(['ErrorBoundary componentWillUnmount']);
1401 });
1402
1403 it('catches if child throws in componentWillReceiveProps during update', async () => {
1404 const container = document.createElement('div');
1405 const root = ReactDOMClient.createRoot(container);
1406 await act(async () => {
1407 root.render(
1408 <ErrorBoundary>
1409 <Normal />
1410 <BrokenComponentWillReceiveProps />
1411 </ErrorBoundary>,
1412 );
1413 });
1414
1415 Scheduler.unstable_clearLog();
1416 await act(async () => {
1417 root.render(
1418 <ErrorBoundary>
1419 <Normal />
1420 <BrokenComponentWillReceiveProps />
1421 </ErrorBoundary>,
1422 );
1423 });
1424 expect(container.textContent).toBe('Caught an error: Hello.');
1425 assertLog([
1426 'ErrorBoundary componentWillReceiveProps',
1427 'ErrorBoundary componentWillUpdate',
1428 'ErrorBoundary render success',
1429 'Normal componentWillReceiveProps',
1430 'Normal componentWillUpdate',
1431 'Normal render',
1432 // BrokenComponentWillReceiveProps will abort rendering:
1433 'BrokenComponentWillReceiveProps componentWillReceiveProps [!]',
1434 // Handle the error
1435 'ErrorBoundary static getDerivedStateFromError',
1436 // Render the error message
1437 'ErrorBoundary componentWillUpdate',
1438 'ErrorBoundary render error',
1439 // logs for error retry
1440 'ErrorBoundary componentWillReceiveProps',
1441 'ErrorBoundary componentWillUpdate',
1442 'ErrorBoundary render success',
1443 'Normal componentWillReceiveProps',
1444 'Normal componentWillUpdate',
1445 'Normal render',
1446 'BrokenComponentWillReceiveProps componentWillReceiveProps [!]',
1447 'ErrorBoundary static getDerivedStateFromError',
1448 'ErrorBoundary componentWillUpdate',
1449 'ErrorBoundary render error',
1450 'Normal componentWillUnmount',
1451 'BrokenComponentWillReceiveProps componentWillUnmount',
1452 'ErrorBoundary componentDidUpdate',
1453 ]);
1454
1455 root.unmount();
1456 assertLog(['ErrorBoundary componentWillUnmount']);
1457 });
1458
1459 it('catches if child throws in componentWillUpdate during update', async () => {
1460 const container = document.createElement('div');
1461 const root = ReactDOMClient.createRoot(container);
1462 await act(async () => {
1463 root.render(
1464 <ErrorBoundary>
1465 <Normal />
1466 <BrokenComponentWillUpdate />
1467 </ErrorBoundary>,
1468 );
1469 });
1470
1471 Scheduler.unstable_clearLog();
1472 await act(async () => {
1473 root.render(
1474 <ErrorBoundary>
1475 <Normal />
1476 <BrokenComponentWillUpdate />
1477 </ErrorBoundary>,
1478 );
1479 });
1480 expect(container.textContent).toBe('Caught an error: Hello.');
1481 assertLog([
1482 'ErrorBoundary componentWillReceiveProps',
1483 'ErrorBoundary componentWillUpdate',
1484 'ErrorBoundary render success',
1485 'Normal componentWillReceiveProps',
1486 'Normal componentWillUpdate',
1487 'Normal render',
1488 // BrokenComponentWillUpdate will abort rendering:
1489 'BrokenComponentWillUpdate componentWillReceiveProps',
1490 'BrokenComponentWillUpdate componentWillUpdate [!]',
1491 // Handle the error
1492 'ErrorBoundary static getDerivedStateFromError',
1493 'ErrorBoundary componentWillUpdate',
1494 'ErrorBoundary render error',
1495 // logs for error retry
1496 'ErrorBoundary componentWillReceiveProps',
1497 'ErrorBoundary componentWillUpdate',
1498 'ErrorBoundary render success',
1499 'Normal componentWillReceiveProps',
1500 'Normal componentWillUpdate',
1501 'Normal render',
1502 'BrokenComponentWillUpdate componentWillReceiveProps',
1503 'BrokenComponentWillUpdate componentWillUpdate [!]',
1504 'ErrorBoundary static getDerivedStateFromError',
1505 'ErrorBoundary componentWillUpdate',
1506 'ErrorBoundary render error',
1507 'Normal componentWillUnmount',
1508 'BrokenComponentWillUpdate componentWillUnmount',
1509 'ErrorBoundary componentDidUpdate',
1510 ]);
1511
1512 root.unmount();
1513 assertLog(['ErrorBoundary componentWillUnmount']);
1514 });
1515
1516 it('catches if child throws in render during update', async () => {
1517 const container = document.createElement('div');
1518 const root = ReactDOMClient.createRoot(container);
1519 await act(async () => {
1520 root.render(
1521 <ErrorBoundary>
1522 <Normal />
1523 </ErrorBoundary>,
1524 );
1525 });
1526
1527 Scheduler.unstable_clearLog();
1528 await act(async () => {
1529 root.render(
1530 <ErrorBoundary>
1531 <Normal />
1532 <Normal logName="Normal2" />
1533 <BrokenRender />
1534 </ErrorBoundary>,
1535 );
1536 });
1537 expect(container.textContent).toBe('Caught an error: Hello.');
1538 assertLog([
1539 'ErrorBoundary componentWillReceiveProps',
1540 'ErrorBoundary componentWillUpdate',
1541 'ErrorBoundary render success',
1542 'Normal componentWillReceiveProps',
1543 'Normal componentWillUpdate',
1544 'Normal render',
1545 // Normal2 will attempt to mount:
1546 'Normal2 constructor',
1547 'Normal2 componentWillMount',
1548 'Normal2 render',
1549 // BrokenRender will abort rendering:
1550 'BrokenRender constructor',
1551 'BrokenRender componentWillMount',
1552 'BrokenRender render [!]',
1553 // Handle the error
1554 'ErrorBoundary static getDerivedStateFromError',
1555 'ErrorBoundary componentWillUpdate',
1556 'ErrorBoundary render error',
1557 // logs for error retry
1558 'ErrorBoundary componentWillReceiveProps',
1559 'ErrorBoundary componentWillUpdate',
1560 'ErrorBoundary render success',
1561 'Normal componentWillReceiveProps',
1562 'Normal componentWillUpdate',
1563 'Normal render',
1564 'Normal2 constructor',
1565 'Normal2 componentWillMount',
1566 'Normal2 render',
1567 'BrokenRender constructor',
1568 'BrokenRender componentWillMount',
1569 'BrokenRender render [!]',
1570 'ErrorBoundary static getDerivedStateFromError',
1571 'ErrorBoundary componentWillUpdate',
1572 'ErrorBoundary render error',
1573 'Normal componentWillUnmount',
1574 'ErrorBoundary componentDidUpdate',
1575 ]);
1576
1577 root.unmount();
1578 assertLog(['ErrorBoundary componentWillUnmount']);
1579 });
1580
1581 it('keeps refs up-to-date during updates', async () => {
1582 function child1Ref(x) {
1583 Scheduler.log('Child1 ref is set to ' + x);
1584 }
1585 function child2Ref(x) {
1586 Scheduler.log('Child2 ref is set to ' + x);
1587 }
1588 function errorMessageRef(x) {
1589 Scheduler.log('Error message ref is set to ' + x);
1590 }
1591
1592 const container = document.createElement('div');
1593 const root = ReactDOMClient.createRoot(container);
1594 await act(async () => {
1595 root.render(
1596 <ErrorBoundary errorMessageRef={errorMessageRef}>
1597 <div ref={child1Ref} />
1598 </ErrorBoundary>,
1599 );
1600 });
1601 assertLog([
1602 'ErrorBoundary constructor',
1603 'ErrorBoundary componentWillMount',
1604 'ErrorBoundary render success',
1605 'Child1 ref is set to [object HTMLDivElement]',
1606 'ErrorBoundary componentDidMount',
1607 ]);
1608
1609 await act(async () => {
1610 root.render(
1611 <ErrorBoundary errorMessageRef={errorMessageRef}>
1612 <div ref={child1Ref} />
1613 <div ref={child2Ref} />
1614 <BrokenRender />
1615 </ErrorBoundary>,
1616 );
1617 });
1618 expect(container.textContent).toBe('Caught an error: Hello.');
1619 assertLog([
1620 'ErrorBoundary componentWillReceiveProps',
1621 'ErrorBoundary componentWillUpdate',
1622 'ErrorBoundary render success',
1623 // BrokenRender will abort rendering:
1624 'BrokenRender constructor',
1625 'BrokenRender componentWillMount',
1626 'BrokenRender render [!]',
1627 // Handle the error
1628 'ErrorBoundary static getDerivedStateFromError',
1629 'ErrorBoundary componentWillUpdate',
1630 'ErrorBoundary render error',
1631 // logs for error retry
1632 'ErrorBoundary componentWillReceiveProps',
1633 'ErrorBoundary componentWillUpdate',
1634 'ErrorBoundary render success',
1635 'BrokenRender constructor',
1636 'BrokenRender componentWillMount',
1637 'BrokenRender render [!]',
1638 'ErrorBoundary static getDerivedStateFromError',
1639 'ErrorBoundary componentWillUpdate',
1640 'ErrorBoundary render error',
1641 // Update Child1 ref since Child1 has been unmounted
1642 // Child2 ref is never set because its mounting aborted
1643 'Child1 ref is set to null',
1644 'Error message ref is set to [object HTMLDivElement]',
1645 'ErrorBoundary componentDidUpdate',
1646 ]);
1647
1648 root.unmount();
1649 assertLog([
1650 'ErrorBoundary componentWillUnmount',
1651 'Error message ref is set to null',
1652 ]);
1653 });
1654
1655 it('recovers from componentWillUnmount errors on update', async () => {
1656 const container = document.createElement('div');
1657 const root = ReactDOMClient.createRoot(container);
1658 await act(async () => {
1659 root.render(
1660 <ErrorBoundary>
1661 <BrokenComponentWillUnmount />
1662 <BrokenComponentWillUnmount />
1663 <Normal />
1664 </ErrorBoundary>,
1665 );
1666 });
1667
1668 Scheduler.unstable_clearLog();
1669 await act(async () => {
1670 root.render(
1671 <ErrorBoundary>
1672 <BrokenComponentWillUnmount />
1673 </ErrorBoundary>,
1674 );
1675 });
1676 expect(container.textContent).toBe('Caught an error: Hello.');
1677 assertLog([
1678 'ErrorBoundary componentWillReceiveProps',
1679 'ErrorBoundary componentWillUpdate',
1680 'ErrorBoundary render success',
1681 // Update existing child:
1682 'BrokenComponentWillUnmount componentWillReceiveProps',
1683 'BrokenComponentWillUnmount componentWillUpdate',
1684 'BrokenComponentWillUnmount render',
1685 // Unmounting throws:
1686 'BrokenComponentWillUnmount componentWillUnmount [!]',
1687 // Fiber proceeds with lifecycles despite errors
1688 'Normal componentWillUnmount',
1689 // The components have updated in this phase
1690 'BrokenComponentWillUnmount componentDidUpdate',
1691 'ErrorBoundary componentDidUpdate',
1692 // The initial render was aborted, so
1693 // Fiber retries from the root.
1694 'ErrorBoundary static getDerivedStateFromError',
1695 'ErrorBoundary componentWillUpdate',
1696 'ErrorBoundary render error',
1697 'BrokenComponentWillUnmount componentWillUnmount [!]',
1698 'ErrorBoundary componentDidUpdate',
1699 // The second willUnmount error should be captured and logged, too.
1700 'ErrorBoundary static getDerivedStateFromError',
1701 'ErrorBoundary componentWillUpdate',
1702 // Render an error now (stack will do it later)
1703 'ErrorBoundary render error',
1704 // Attempt to unmount previous child:
1705 // Done
1706 'ErrorBoundary componentDidUpdate',
1707 ]);
1708
1709 root.unmount();
1710 assertLog(['ErrorBoundary componentWillUnmount']);
1711 });
1712
1713 it('recovers from nested componentWillUnmount errors on update', async () => {
1714 const container = document.createElement('div');
1715 const root = ReactDOMClient.createRoot(container);
1716 await act(async () => {
1717 root.render(
1718 <ErrorBoundary>
1719 <Normal>
1720 <BrokenComponentWillUnmount />
1721 </Normal>
1722 <BrokenComponentWillUnmount />
1723 </ErrorBoundary>,
1724 );
1725 });
1726
1727 Scheduler.unstable_clearLog();
1728 await act(async () => {
1729 root.render(
1730 <ErrorBoundary>
1731 <Normal>
1732 <BrokenComponentWillUnmount />
1733 </Normal>
1734 </ErrorBoundary>,
1735 );
1736 });
1737 expect(container.textContent).toBe('Caught an error: Hello.');
1738 assertLog([
1739 'ErrorBoundary componentWillReceiveProps',
1740 'ErrorBoundary componentWillUpdate',
1741 'ErrorBoundary render success',
1742 // Update existing children:
1743 'Normal componentWillReceiveProps',
1744 'Normal componentWillUpdate',
1745 'Normal render',
1746 'BrokenComponentWillUnmount componentWillReceiveProps',
1747 'BrokenComponentWillUnmount componentWillUpdate',
1748 'BrokenComponentWillUnmount render',
1749 // Unmounting throws:
1750 'BrokenComponentWillUnmount componentWillUnmount [!]',
1751 // Fiber proceeds with lifecycles despite errors
1752 'BrokenComponentWillUnmount componentDidUpdate',
1753 'Normal componentDidUpdate',
1754 'ErrorBoundary componentDidUpdate',
1755 // Now that commit phase is done, Fiber handles errors
1756 'ErrorBoundary static getDerivedStateFromError',
1757 'ErrorBoundary componentWillUpdate',
1758 'ErrorBoundary render error',
1759 'Normal componentWillUnmount',
1760 'BrokenComponentWillUnmount componentWillUnmount [!]',
1761 'ErrorBoundary componentDidUpdate',
1762 // The second willUnmount error should be captured and logged, too.
1763 'ErrorBoundary static getDerivedStateFromError',
1764 'ErrorBoundary componentWillUpdate',
1765 // Render an error now (stack will do it later)
1766 'ErrorBoundary render error',
1767 // Done
1768 'ErrorBoundary componentDidUpdate',
1769 ]);
1770
1771 root.unmount();
1772 assertLog(['ErrorBoundary componentWillUnmount']);
1773 });
1774
1775 it('picks the right boundary when handling unmounting errors', async () => {
1776 function renderInnerError(error) {
1777 return <div>Caught an inner error: {error.message}.</div>;
1778 }
1779 function renderOuterError(error) {
1780 return <div>Caught an outer error: {error.message}.</div>;
1781 }
1782
1783 const container = document.createElement('div');
1784 const root = ReactDOMClient.createRoot(container);
1785 await act(async () => {
1786 root.render(
1787 <ErrorBoundary
1788 logName="OuterErrorBoundary"
1789 renderError={renderOuterError}>
1790 <ErrorBoundary
1791 logName="InnerErrorBoundary"
1792 renderError={renderInnerError}>
1793 <BrokenComponentWillUnmount />
1794 </ErrorBoundary>
1795 </ErrorBoundary>,
1796 );
1797 });
1798
1799 Scheduler.unstable_clearLog();
1800 await act(async () => {
1801 root.render(
1802 <ErrorBoundary
1803 logName="OuterErrorBoundary"
1804 renderError={renderOuterError}>
1805 <ErrorBoundary
1806 logName="InnerErrorBoundary"
1807 renderError={renderInnerError}
1808 />
1809 </ErrorBoundary>,
1810 );
1811 });
1812 expect(container.textContent).toBe('Caught an inner error: Hello.');
1813 assertLog([
1814 // Update outer boundary
1815 'OuterErrorBoundary componentWillReceiveProps',
1816 'OuterErrorBoundary componentWillUpdate',
1817 'OuterErrorBoundary render success',
1818 // Update inner boundary
1819 'InnerErrorBoundary componentWillReceiveProps',
1820 'InnerErrorBoundary componentWillUpdate',
1821 'InnerErrorBoundary render success',
1822 // Try unmounting child
1823 'BrokenComponentWillUnmount componentWillUnmount [!]',
1824 // Now that commit phase is done, Fiber handles errors
1825 // Only inner boundary receives the error:
1826 'InnerErrorBoundary componentDidUpdate',
1827 'OuterErrorBoundary componentDidUpdate',
1828 'ErrorBoundary static getDerivedStateFromError',
1829 'InnerErrorBoundary componentWillUpdate',
1830 // Render an error now
1831 'InnerErrorBoundary render error',
1832 // In Fiber, this was a local update to the
1833 // inner boundary so only its hook fires
1834 'InnerErrorBoundary componentDidUpdate',
1835 ]);
1836
1837 root.unmount();
1838 assertLog([
1839 'OuterErrorBoundary componentWillUnmount',
1840 'InnerErrorBoundary componentWillUnmount',
1841 ]);
1842 });
1843
1844 it('can recover from error state', async () => {
1845 const container = document.createElement('div');
1846 const root = ReactDOMClient.createRoot(container);
1847 await act(async () => {
1848 root.render(
1849 <ErrorBoundary>
1850 <BrokenRender />
1851 </ErrorBoundary>,
1852 );
1853 });
1854
1855 Scheduler.unstable_clearLog();
1856 await act(async () => {
1857 root.render(
1858 <ErrorBoundary>
1859 <Normal />
1860 </ErrorBoundary>,
1861 );
1862 });
1863 // Error boundary doesn't retry by itself:
1864 expect(container.textContent).toBe('Caught an error: Hello.');
1865
1866 // Force the success path:
1867 Scheduler.unstable_clearLog();
1868 await act(async () => {
1869 root.render(
1870 <ErrorBoundary forceRetry={true}>
1871 <Normal />
1872 </ErrorBoundary>,
1873 );
1874 });
1875 expect(container.textContent).not.toContain('Caught an error');
1876 assertLog([
1877 'ErrorBoundary componentWillReceiveProps',
1878 'ErrorBoundary componentWillUpdate',
1879 'ErrorBoundary render success',
1880 // Mount children:
1881 'Normal constructor',
1882 'Normal componentWillMount',
1883 'Normal render',
1884 // Finalize updates:
1885 'Normal componentDidMount',
1886 'ErrorBoundary componentDidUpdate',
1887 ]);
1888
1889 root.unmount();
1890 assertLog([
1891 'ErrorBoundary componentWillUnmount',
1892 'Normal componentWillUnmount',
1893 ]);
1894 });
1895
1896 it('can update multiple times in error state', async () => {
1897 const container = document.createElement('div');
1898 const root = ReactDOMClient.createRoot(container);
1899 await act(async () => {
1900 root.render(
1901 <ErrorBoundary>
1902 <BrokenRender />
1903 </ErrorBoundary>,
1904 );
1905 });
1906 expect(container.textContent).toBe('Caught an error: Hello.');
1907 Scheduler.unstable_clearLog();
1908
1909 await act(async () => {
1910 root.render(
1911 <ErrorBoundary>
1912 <BrokenRender />
1913 </ErrorBoundary>,
1914 );
1915 });
1916 expect(container.textContent).toBe('Caught an error: Hello.');
1917 Scheduler.unstable_clearLog();
1918
1919 await act(async () => {
1920 root.render(<div>Other screen</div>);
1921 });
1922 expect(container.textContent).toBe('Other screen');
1923
1924 root.unmount();
1925 });
1926
1927 it("doesn't get into inconsistent state during removals", async () => {
1928 const container = document.createElement('div');
1929 const root = ReactDOMClient.createRoot(container);
1930 await act(async () => {
1931 root.render(
1932 <ErrorBoundary>
1933 <Normal />
1934 <BrokenComponentWillUnmount />
1935 <Normal />
1936 </ErrorBoundary>,
1937 );
1938 });
1939 Scheduler.unstable_clearLog();
1940 await act(async () => {
1941 root.render(<ErrorBoundary />);
1942 });
1943 expect(container.textContent).toBe('Caught an error: Hello.');
1944
1945 Scheduler.unstable_clearLog();
1946 root.unmount();
1947 assertLog(['ErrorBoundary componentWillUnmount']);
1948 });
1949
1950 it("doesn't get into inconsistent state during additions", async () => {
1951 const container = document.createElement('div');
1952 const root = ReactDOMClient.createRoot(container);
1953 await act(async () => {
1954 root.render(<ErrorBoundary />);
1955 });
1956 assertLog([
1957 'ErrorBoundary constructor',
1958 'ErrorBoundary componentWillMount',
1959 'ErrorBoundary render success',
1960 'ErrorBoundary componentDidMount',
1961 ]);
1962 await act(async () => {
1963 root.render(
1964 <ErrorBoundary>
1965 <Normal />
1966 <BrokenRender />
1967 <Normal />
1968 </ErrorBoundary>,
1969 );
1970 });
1971 expect(container.textContent).toBe('Caught an error: Hello.');
1972
1973 Scheduler.unstable_clearLog();
1974 root.unmount();
1975 assertLog(['ErrorBoundary componentWillUnmount']);
1976 });
1977
1978 it("doesn't get into inconsistent state during reorders", async () => {
1979 function getAMixOfNormalAndBrokenRenderElements() {
1980 const elements = [];
1981 for (let i = 0; i < 100; i++) {
1982 elements.push(<Normal key={i} />);
1983 }
1984 elements.push(<MaybeBrokenRender key={100} />);
1985
1986 let currentIndex = elements.length;
1987 while (0 !== currentIndex) {
1988 const randomIndex = Math.floor(Math.random() * currentIndex);
1989 currentIndex -= 1;
1990 const temporaryValue = elements[currentIndex];
1991 elements[currentIndex] = elements[randomIndex];
1992 elements[randomIndex] = temporaryValue;
1993 }
1994 return elements;
1995 }
1996
1997 class MaybeBrokenRender extends React.Component {
1998 render() {
1999 if (fail) {
2000 throw new Error('Hello');
2001 }
2002 return <div>{this.props.children}</div>;
2003 }
2004 }
2005
2006 let fail = false;
2007 const container = document.createElement('div');
2008 const root = ReactDOMClient.createRoot(container);
2009 await act(async () => {
2010 root.render(
2011 <ErrorBoundary>
2012 {getAMixOfNormalAndBrokenRenderElements()}
2013 </ErrorBoundary>,
2014 );
2015 });
2016 expect(container.textContent).not.toContain('Caught an error');
2017
2018 fail = true;
2019 Scheduler.unstable_clearLog();
2020 await act(async () => {
2021 root.render(
2022 <ErrorBoundary>
2023 {getAMixOfNormalAndBrokenRenderElements()}
2024 </ErrorBoundary>,
2025 );
2026 });
2027 expect(container.textContent).toBe('Caught an error: Hello.');
2028
2029 Scheduler.unstable_clearLog();
2030 root.unmount();
2031 assertLog(['ErrorBoundary componentWillUnmount']);
2032 });
2033
2034 it('catches errors originating downstream', async () => {
2035 let fail = false;
2036 class Stateful extends React.Component {
2037 state = {shouldThrow: false};
2038
2039 render() {
2040 if (fail) {
2041 Scheduler.log('Stateful render [!]');
2042 throw new Error('Hello');
2043 }
2044 return <div>{this.props.children}</div>;
2045 }
2046 }
2047
2048 let statefulInst;
2049 const container = document.createElement('div');
2050 const root = ReactDOMClient.createRoot(container);
2051 await act(async () => {
2052 root.render(
2053 <ErrorBoundary>
2054 <Stateful ref={inst => (statefulInst = inst)} />
2055 </ErrorBoundary>,
2056 );
2057 });
2058
2059 Scheduler.unstable_clearLog();
2060 expect(() => {
2061 fail = true;
2062 statefulInst.forceUpdate();
2063 }).not.toThrow();
2064 root.unmount();
2065 assertLog(['ErrorBoundary componentWillUnmount']);
2066 });
2067
2068 it('catches errors in componentDidMount', async () => {
2069 const container = document.createElement('div');
2070 const root = ReactDOMClient.createRoot(container);
2071 await act(async () => {
2072 root.render(
2073 <ErrorBoundary>
2074 <BrokenComponentWillUnmount>
2075 <Normal />
2076 </BrokenComponentWillUnmount>
2077 <BrokenComponentDidMount />
2078 <Normal logName="LastChild" />
2079 </ErrorBoundary>,
2080 );
2081 });
2082 assertLog([
2083 'ErrorBoundary constructor',
2084 'ErrorBoundary componentWillMount',
2085 'ErrorBoundary render success',
2086 'BrokenComponentWillUnmount constructor',
2087 'BrokenComponentWillUnmount componentWillMount',
2088 'BrokenComponentWillUnmount render',
2089 'Normal constructor',
2090 'Normal componentWillMount',
2091 'Normal render',
2092 'BrokenComponentDidMount constructor',
2093 'BrokenComponentDidMount componentWillMount',
2094 'BrokenComponentDidMount render',
2095 'LastChild constructor',
2096 'LastChild componentWillMount',
2097 'LastChild render',
2098 // Start flushing didMount queue
2099 'Normal componentDidMount',
2100 'BrokenComponentWillUnmount componentDidMount',
2101 'BrokenComponentDidMount componentDidMount [!]',
2102 // Continue despite the error
2103 'LastChild componentDidMount',
2104 // Now we are ready to handle the error
2105 'ErrorBoundary componentDidMount',
2106 'ErrorBoundary static getDerivedStateFromError',
2107 'ErrorBoundary componentWillUpdate',
2108 'ErrorBoundary render error',
2109 // Safely unmount every child
2110 'BrokenComponentWillUnmount componentWillUnmount [!]',
2111 // Continue unmounting safely despite any errors
2112 'Normal componentWillUnmount',
2113 'BrokenComponentDidMount componentWillUnmount',
2114 'LastChild componentWillUnmount',
2115 // The willUnmount error should be captured and logged, too.
2116 'ErrorBoundary componentDidUpdate',
2117 'ErrorBoundary static getDerivedStateFromError',
2118 'ErrorBoundary componentWillUpdate',
2119 'ErrorBoundary render error',
2120 // The update has finished
2121 'ErrorBoundary componentDidUpdate',
2122 ]);
2123
2124 root.unmount();
2125 assertLog(['ErrorBoundary componentWillUnmount']);
2126 });
2127
2128 it('catches errors in componentDidUpdate', async () => {
2129 const container = document.createElement('div');
2130 const root = ReactDOMClient.createRoot(container);
2131 await act(async () => {
2132 root.render(
2133 <ErrorBoundary>
2134 <BrokenComponentDidUpdate />
2135 </ErrorBoundary>,
2136 );
2137 });
2138
2139 Scheduler.unstable_clearLog();
2140 await act(async () => {
2141 root.render(
2142 <ErrorBoundary>
2143 <BrokenComponentDidUpdate />
2144 </ErrorBoundary>,
2145 );
2146 });
2147 assertLog([
2148 'ErrorBoundary componentWillReceiveProps',
2149 'ErrorBoundary componentWillUpdate',
2150 'ErrorBoundary render success',
2151 'BrokenComponentDidUpdate componentWillReceiveProps',
2152 'BrokenComponentDidUpdate componentWillUpdate',
2153 'BrokenComponentDidUpdate render',
2154 // All lifecycles run
2155 'BrokenComponentDidUpdate componentDidUpdate [!]',
2156 'ErrorBoundary componentDidUpdate',
2157 // Then, error is handled
2158 'ErrorBoundary static getDerivedStateFromError',
2159 'ErrorBoundary componentWillUpdate',
2160 'ErrorBoundary render error',
2161 'BrokenComponentDidUpdate componentWillUnmount',
2162 'ErrorBoundary componentDidUpdate',
2163 ]);
2164
2165 root.unmount();
2166 assertLog(['ErrorBoundary componentWillUnmount']);
2167 });
2168
2169 it('catches errors in useEffect', async () => {
2170 const container = document.createElement('div');
2171 const root = ReactDOMClient.createRoot(container);
2172 await act(() => {
2173 root.render(
2174 <ErrorBoundary>
2175 <BrokenUseEffect>Initial value</BrokenUseEffect>
2176 </ErrorBoundary>,
2177 );
2178 });
2179
2180 // verify flushed passive effects and handle the error
2181 assertLog([
2182 // logs for error retry
2183 'ErrorBoundary constructor',
2184 'ErrorBoundary componentWillMount',
2185 'ErrorBoundary render success',
2186 'BrokenUseEffect render',
2187 'ErrorBoundary componentDidMount',
2188 'BrokenUseEffect useEffect [!]',
2189 // Handle the error
2190 'ErrorBoundary static getDerivedStateFromError',
2191 'ErrorBoundary componentWillUpdate',
2192 'ErrorBoundary render error',
2193 'ErrorBoundary componentDidUpdate',
2194 ]);
2195 expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
2196 });
2197
2198 it('catches errors in useLayoutEffect', async () => {
2199 const container = document.createElement('div');
2200 const root = ReactDOMClient.createRoot(container);
2201 await act(async () => {
2202 root.render(
2203 <ErrorBoundary>
2204 <BrokenUseLayoutEffect>Initial value</BrokenUseLayoutEffect>
2205 </ErrorBoundary>,
2206 );
2207 });
2208 assertLog([
2209 'ErrorBoundary constructor',
2210 'ErrorBoundary componentWillMount',
2211 'ErrorBoundary render success',
2212 'BrokenUseLayoutEffect render',
2213 'BrokenUseLayoutEffect useLayoutEffect [!]',
2214 // Fiber proceeds with the hooks
2215 'ErrorBoundary componentDidMount',
2216 // The error propagates to the higher boundary
2217 'ErrorBoundary static getDerivedStateFromError',
2218 // Fiber retries from the root
2219 'ErrorBoundary componentWillUpdate',
2220 'ErrorBoundary render error',
2221 'ErrorBoundary componentDidUpdate',
2222 ]);
2223
2224 expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
2225 });
2226
2227 it('propagates errors inside boundary during componentDidMount', async () => {
2228 const container = document.createElement('div');
2229 const root = ReactDOMClient.createRoot(container);
2230 await act(async () => {
2231 root.render(
2232 <ErrorBoundary>
2233 <BrokenComponentDidMountErrorBoundary
2234 renderError={error => (
2235 <div>We should never catch our own error: {error.message}.</div>
2236 )}
2237 />
2238 </ErrorBoundary>,
2239 );
2240 });
2241 expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
2242 assertLog([
2243 'ErrorBoundary constructor',
2244 'ErrorBoundary componentWillMount',
2245 'ErrorBoundary render success',
2246 'BrokenComponentDidMountErrorBoundary constructor',
2247 'BrokenComponentDidMountErrorBoundary componentWillMount',
2248 'BrokenComponentDidMountErrorBoundary render success',
2249 'BrokenComponentDidMountErrorBoundary componentDidMount [!]',
2250 // Fiber proceeds with the hooks
2251 'ErrorBoundary componentDidMount',
2252 // The error propagates to the higher boundary
2253 'ErrorBoundary static getDerivedStateFromError',
2254 // Fiber retries from the root
2255 'ErrorBoundary componentWillUpdate',
2256 'ErrorBoundary render error',
2257 'BrokenComponentDidMountErrorBoundary componentWillUnmount',
2258 'ErrorBoundary componentDidUpdate',
2259 ]);
2260
2261 root.unmount();
2262 assertLog(['ErrorBoundary componentWillUnmount']);
2263 });
2264
2265 it('calls static getDerivedStateFromError for each error that is captured', async () => {
2266 function renderUnmountError(error) {
2267 return <div>Caught an unmounting error: {error.message}.</div>;
2268 }
2269 function renderUpdateError(error) {
2270 return <div>Caught an updating error: {error.message}.</div>;
2271 }
2272
2273 const container = document.createElement('div');
2274 const root = ReactDOMClient.createRoot(container);
2275 await act(async () => {
2276 root.render(
2277 <ErrorBoundary logName="OuterErrorBoundary">
2278 <ErrorBoundary
2279 logName="InnerUnmountBoundary"
2280 renderError={renderUnmountError}>
2281 <BrokenComponentWillUnmount errorText="E1" />
2282 <BrokenComponentWillUnmount errorText="E2" />
2283 </ErrorBoundary>
2284 <ErrorBoundary
2285 logName="InnerUpdateBoundary"
2286 renderError={renderUpdateError}>
2287 <BrokenComponentDidUpdate errorText="E3" />
2288 <BrokenComponentDidUpdate errorText="E4" />
2289 </ErrorBoundary>
2290 </ErrorBoundary>,
2291 );
2292 });
2293
2294 Scheduler.unstable_clearLog();
2295 await act(async () => {
2296 root.render(
2297 <ErrorBoundary logName="OuterErrorBoundary">
2298 <ErrorBoundary
2299 logName="InnerUnmountBoundary"
2300 renderError={renderUnmountError}
2301 />
2302 <ErrorBoundary
2303 logName="InnerUpdateBoundary"
2304 renderError={renderUpdateError}>
2305 <BrokenComponentDidUpdate errorText="E3" />
2306 <BrokenComponentDidUpdate errorText="E4" />
2307 </ErrorBoundary>
2308 </ErrorBoundary>,
2309 );
2310 });
2311
2312 expect(container.firstChild.textContent).toBe(
2313 'Caught an unmounting error: E2.' + 'Caught an updating error: E4.',
2314 );
2315 assertLog([
2316 // Begin update phase
2317 'OuterErrorBoundary componentWillReceiveProps',
2318 'OuterErrorBoundary componentWillUpdate',
2319 'OuterErrorBoundary render success',
2320 'InnerUnmountBoundary componentWillReceiveProps',
2321 'InnerUnmountBoundary componentWillUpdate',
2322 'InnerUnmountBoundary render success',
2323 'InnerUpdateBoundary componentWillReceiveProps',
2324 'InnerUpdateBoundary componentWillUpdate',
2325 'InnerUpdateBoundary render success',
2326 // First come the updates
2327 'BrokenComponentDidUpdate componentWillReceiveProps',
2328 'BrokenComponentDidUpdate componentWillUpdate',
2329 'BrokenComponentDidUpdate render',
2330 'BrokenComponentDidUpdate componentWillReceiveProps',
2331 'BrokenComponentDidUpdate componentWillUpdate',
2332 'BrokenComponentDidUpdate render',
2333 // We're in commit phase now, deleting
2334 'BrokenComponentWillUnmount componentWillUnmount [!]',
2335 'BrokenComponentWillUnmount componentWillUnmount [!]',
2336 // Continue despite errors, handle them after commit is done
2337 'InnerUnmountBoundary componentDidUpdate',
2338 // We're still in commit phase, now calling update lifecycles
2339 'BrokenComponentDidUpdate componentDidUpdate [!]',
2340 // Again, continue despite errors, we'll handle them later
2341 'BrokenComponentDidUpdate componentDidUpdate [!]',
2342 'InnerUpdateBoundary componentDidUpdate',
2343 'OuterErrorBoundary componentDidUpdate',
2344 // After the commit phase, attempt to recover from any errors that
2345 // were captured
2346 'ErrorBoundary static getDerivedStateFromError',
2347 'ErrorBoundary static getDerivedStateFromError',
2348 'InnerUnmountBoundary componentWillUpdate',
2349 'InnerUnmountBoundary render error',
2350 'ErrorBoundary static getDerivedStateFromError',
2351 'ErrorBoundary static getDerivedStateFromError',
2352 'InnerUpdateBoundary componentWillUpdate',
2353 'InnerUpdateBoundary render error',
2354 'BrokenComponentDidUpdate componentWillUnmount',
2355 'BrokenComponentDidUpdate componentWillUnmount',
2356 'InnerUnmountBoundary componentDidUpdate',
2357 'InnerUpdateBoundary componentDidUpdate',
2358 ]);
2359
2360 root.unmount();
2361 assertLog([
2362 'OuterErrorBoundary componentWillUnmount',
2363 'InnerUnmountBoundary componentWillUnmount',
2364 'InnerUpdateBoundary componentWillUnmount',
2365 ]);
2366 });
2367
2368 it('discards a bad root if the root component fails', async () => {
2369 const X = null;
2370 const Y = undefined;
2371
2372 await expect(async () => {
2373 const container = document.createElement('div');
2374 const root = ReactDOMClient.createRoot(container);
2375 await act(async () => {
2376 root.render(<X />);
2377 });
2378 }).rejects.toThrow(
2379 'Element type is invalid: ' +
2380 'expected a string (for built-in components) or a ' +
2381 'class/function (for composite components) but got: null.',
2382 );
2383
2384 await expect(async () => {
2385 const container = document.createElement('div');
2386 const root = ReactDOMClient.createRoot(container);
2387 await act(async () => {
2388 root.render(<Y />);
2389 });
2390 }).rejects.toThrow(
2391 'Element type is invalid: ' +
2392 'expected a string (for built-in components) or a ' +
2393 'class/function (for composite components) but got: undefined.',
2394 );
2395 });
2396
2397 it('renders empty output if error boundary does not handle the error', async () => {
2398 const container = document.createElement('div');
2399 const root = ReactDOMClient.createRoot(container);
2400
2401 await expect(async () => {
2402 await act(async () => {
2403 root.render(
2404 <div>
2405 Sibling
2406 <NoopErrorBoundary>
2407 <BrokenRender />
2408 </NoopErrorBoundary>
2409 </div>,
2410 );
2411 });
2412 }).rejects.toThrow('Hello');
2413
2414 expect(container.innerHTML).toBe('');
2415 assertLog([
2416 'NoopErrorBoundary constructor',
2417 'NoopErrorBoundary componentWillMount',
2418 'NoopErrorBoundary render',
2419 'BrokenRender constructor',
2420 'BrokenRender componentWillMount',
2421 'BrokenRender render [!]',
2422 // Noop error boundaries retry render (and fail again)
2423 'NoopErrorBoundary static getDerivedStateFromError',
2424 'NoopErrorBoundary render',
2425 'BrokenRender constructor',
2426 'BrokenRender componentWillMount',
2427 'BrokenRender render [!]',
2428 // logs for error retry
2429 'NoopErrorBoundary constructor',
2430 'NoopErrorBoundary componentWillMount',
2431 'NoopErrorBoundary render',
2432 'BrokenRender constructor',
2433 'BrokenRender componentWillMount',
2434 'BrokenRender render [!]',
2435 'NoopErrorBoundary static getDerivedStateFromError',
2436 'NoopErrorBoundary render',
2437 'BrokenRender constructor',
2438 'BrokenRender componentWillMount',
2439 'BrokenRender render [!]',
2440 ]);
2441 });
2442
2443 it('passes an aggregate error when two errors happen in commit', async () => {
2444 const errors = [];
2445 let caughtError;
2446 class Parent extends React.Component {
2447 render() {
2448 return <Child />;
2449 }
2450 componentDidMount() {
2451 errors.push('parent sad');
2452 throw new Error('parent sad');
2453 }
2454 }
2455 class Child extends React.Component {
2456 render() {
2457 return <div />;
2458 }
2459 componentDidMount() {
2460 errors.push('child sad');
2461 throw new Error('child sad');
2462 }
2463 }
2464
2465 const container = document.createElement('div');
2466 const root = ReactDOMClient.createRoot(container);
2467 try {
2468 // Here, we test the behavior where there is no error boundary and we
2469 // delegate to the host root.
2470 await act(async () => {
2471 root.render(<Parent />);
2472 });
2473 } catch (e) {
2474 caughtError = e;
2475 }
2476
2477 expect(errors).toEqual(['child sad', 'parent sad']);
2478 expect(caughtError.errors).toEqual([
2479 expect.objectContaining({message: 'child sad'}),
2480 expect.objectContaining({message: 'parent sad'}),
2481 ]);
2482 });
2483
2484 it('propagates uncaught error inside unbatched initial mount', async () => {
2485 function Foo() {
2486 throw new Error('foo error');
2487 }
2488 const container = document.createElement('div');
2489 const root = ReactDOMClient.createRoot(container);
2490 await expect(async () => {
2491 await ReactDOM.unstable_batchedUpdates(async () => {
2492 await act(async () => {
2493 root.render(<Foo />);
2494 });
2495 });
2496 }).rejects.toThrow('foo error');
2497 });
2498
2499 it('handles errors that occur in before-mutation commit hook', async () => {
2500 const errors = [];
2501 let caughtError;
2502 class Parent extends React.Component {
2503 getSnapshotBeforeUpdate() {
2504 errors.push('parent sad');
2505 throw new Error('parent sad');
2506 }
2507 componentDidUpdate() {}
2508 render() {
2509 return <Child {...this.props} />;
2510 }
2511 }
2512 class Child extends React.Component {
2513 getSnapshotBeforeUpdate() {
2514 errors.push('child sad');
2515 throw new Error('child sad');
2516 }
2517 componentDidUpdate() {}
2518 render() {
2519 return <div />;
2520 }
2521 }
2522
2523 const container = document.createElement('div');
2524 const root = ReactDOMClient.createRoot(container);
2525 await act(async () => {
2526 root.render(<Parent value={1} />);
2527 });
2528 try {
2529 await act(async () => {
2530 root.render(<Parent value={2} />);
2531 });
2532 } catch (e) {
2533 caughtError = e;
2534 }
2535
2536 expect(errors).toEqual(['child sad', 'parent sad']);
2537 expect(caughtError.errors).toEqual([
2538 expect.objectContaining({message: 'child sad'}),
2539 expect.objectContaining({message: 'parent sad'}),
2540 ]);
2541 });
2542
2543 it('should warn if an error boundary with only componentDidCatch does not update state', async () => {
2544 class InvalidErrorBoundary extends React.Component {
2545 componentDidCatch(error, info) {
2546 // This component does not define getDerivedStateFromError().
2547 // It also doesn't call setState().
2548 // So it would swallow errors (which is probably unintentional).
2549 }
2550 render() {
2551 return this.props.children;
2552 }
2553 }
2554
2555 const Throws = () => {
2556 throw new Error('expected');
2557 };
2558
2559 const container = document.createElement('div');
2560 const root = ReactDOMClient.createRoot(container);
2561 await act(async () => {
2562 root.render(
2563 <InvalidErrorBoundary>
2564 <Throws />
2565 </InvalidErrorBoundary>,
2566 );
2567 });
2568 assertConsoleErrorDev([
2569 'InvalidErrorBoundary: Error boundaries should implement getDerivedStateFromError(). ' +
2570 'In that method, return a state update to display an error message or fallback UI.\n' +
2571 ' in InvalidErrorBoundary (at **)',
2572 ]);
2573 expect(container.textContent).toBe('');
2574 });
2575
2576 it('should call both componentDidCatch and getDerivedStateFromError if both exist on a component', async () => {
2577 let componentDidCatchError, getDerivedStateFromErrorError;
2578 class ErrorBoundaryWithBothMethods extends React.Component {
2579 state = {error: null};
2580 static getDerivedStateFromError(error) {
2581 getDerivedStateFromErrorError = error;
2582 return {error};
2583 }
2584 componentDidCatch(error, info) {
2585 componentDidCatchError = error;
2586 }
2587 render() {
2588 return this.state.error ? 'ErrorBoundary' : this.props.children;
2589 }
2590 }
2591
2592 const thrownError = new Error('expected');
2593 const Throws = () => {
2594 throw thrownError;
2595 };
2596
2597 const container = document.createElement('div');
2598 const root = ReactDOMClient.createRoot(container);
2599 await act(async () => {
2600 root.render(
2601 <ErrorBoundaryWithBothMethods>
2602 <Throws />
2603 </ErrorBoundaryWithBothMethods>,
2604 );
2605 });
2606 expect(container.textContent).toBe('ErrorBoundary');
2607 expect(componentDidCatchError).toBe(thrownError);
2608 expect(getDerivedStateFromErrorError).toBe(thrownError);
2609 });
2610
2611 it('should catch errors from invariants in completion phase', async () => {
2612 const container = document.createElement('div');
2613 const root = ReactDOMClient.createRoot(container);
2614 await act(async () => {
2615 root.render(
2616 <ErrorBoundary>
2617 <input>
2618 <div />
2619 </input>
2620 </ErrorBoundary>,
2621 );
2622 });
2623 expect(container.textContent).toContain(
2624 'Caught an error: input is a void element tag',
2625 );
2626 });
2627
2628 it('should catch errors from errors in the throw phase from boundaries', async () => {
2629 const container = document.createElement('div');
2630 const root = ReactDOMClient.createRoot(container);
2631
2632 const thrownError = new Error('original error');
2633 const Throws = () => {
2634 throw thrownError;
2635 };
2636
2637 class EvilErrorBoundary extends React.Component {
2638 get componentDidCatch() {
2639 throw new Error('gotta catch em all');
2640 }
2641 render() {
2642 return this.props.children;
2643 }
2644 }
2645
2646 await act(async () => {
2647 root.render(
2648 <ErrorBoundary>
2649 <EvilErrorBoundary>
2650 <Throws />
2651 </EvilErrorBoundary>
2652 </ErrorBoundary>,
2653 );
2654 });
2655
2656 expect(container.textContent).toContain(
2657 'Caught an error: gotta catch em all',
2658 );
2659 });
2660
2661 it('should protect errors from errors in the stack generation', async () => {
2662 const container = document.createElement('div');
2663 const root = ReactDOMClient.createRoot(container);
2664
2665 const evilError = {
2666 message: 'gotta catch em all',
2667 get stack() {
2668 throw new Error('gotta catch em all');
2669 },
2670 };
2671 const Throws = () => {
2672 throw evilError;
2673 };
2674 Object.defineProperty(Throws, 'displayName', {
2675 get: function () {
2676 throw new Error('gotta catch em all');
2677 },
2678 });
2679
2680 function Wrapper() {
2681 return <Throws />;
2682 }
2683
2684 await expect(async () => {
2685 await act(async () => {
2686 root.render(
2687 <ErrorBoundary>
2688 <Wrapper />
2689 </ErrorBoundary>,
2690 );
2691 });
2692 }).rejects.toThrow('gotta catch em all');
2693
2694 expect(container.textContent).toContain(
2695 'Caught an error: gotta catch em all.',
2696 );
2697 });
2698
2699 it('catches errors thrown in componentWillUnmount', async () => {
2700 class LocalErrorBoundary extends React.Component {
2701 state = {error: null};
2702 static getDerivedStateFromError(error) {
2703 Scheduler.log(`ErrorBoundary static getDerivedStateFromError`);
2704 return {error};
2705 }
2706 render() {
2707 const {children, id, fallbackID} = this.props;
2708 const {error} = this.state;
2709 if (error) {
2710 Scheduler.log(`${id} render error`);
2711 return <Component id={fallbackID} />;
2712 }
2713 Scheduler.log(`${id} render success`);
2714 return children || null;
2715 }
2716 }
2717
2718 class Component extends React.Component {
2719 render() {
2720 const {id} = this.props;
2721 Scheduler.log('Component render ' + id);
2722 return id;
2723 }
2724 }
2725
2726 class LocalBrokenComponentWillUnmount extends React.Component {
2727 componentWillUnmount() {
2728 Scheduler.log('BrokenComponentWillUnmount componentWillUnmount');
2729 throw Error('Expected');
2730 }
2731
2732 render() {
2733 Scheduler.log('BrokenComponentWillUnmount render');
2734 return 'broken';
2735 }
2736 }
2737
2738 const container = document.createElement('div');
2739 const root = ReactDOMClient.createRoot(container);
2740
2741 await act(async () => {
2742 root.render(
2743 <LocalErrorBoundary id="OuterBoundary" fallbackID="OuterFallback">
2744 <Component id="sibling" />
2745 <LocalErrorBoundary id="InnerBoundary" fallbackID="InnerFallback">
2746 <LocalBrokenComponentWillUnmount />
2747 </LocalErrorBoundary>
2748 </LocalErrorBoundary>,
2749 );
2750 });
2751
2752 expect(container.firstChild.textContent).toBe('sibling');
2753 expect(container.lastChild.textContent).toBe('broken');
2754 assertLog([
2755 'OuterBoundary render success',
2756 'Component render sibling',
2757 'InnerBoundary render success',
2758 'BrokenComponentWillUnmount render',
2759 ]);
2760
2761 await act(async () => {
2762 root.render(
2763 <LocalErrorBoundary id="OuterBoundary" fallbackID="OuterFallback">
2764 <Component id="sibling" />
2765 </LocalErrorBoundary>,
2766 );
2767 });
2768
2769 // React should skip over the unmounting boundary and find the nearest still-mounted boundary.
2770 expect(container.firstChild.textContent).toBe('OuterFallback');
2771 expect(container.lastChild.textContent).toBe('OuterFallback');
2772 assertLog([
2773 'OuterBoundary render success',
2774 'Component render sibling',
2775 'BrokenComponentWillUnmount componentWillUnmount',
2776 'ErrorBoundary static getDerivedStateFromError',
2777 'OuterBoundary render error',
2778 'Component render OuterFallback',
2779 ]);
2780 });
2781
2782 it('catches errors thrown while detaching refs', async () => {
2783 class LocalErrorBoundary extends React.Component {
2784 state = {error: null};
2785 static getDerivedStateFromError(error) {
2786 Scheduler.log(`ErrorBoundary static getDerivedStateFromError`);
2787 return {error};
2788 }
2789 render() {
2790 const {children, id, fallbackID} = this.props;
2791 const {error} = this.state;
2792 if (error) {
2793 Scheduler.log(`${id} render error`);
2794 return <Component id={fallbackID} />;
2795 }
2796 Scheduler.log(`${id} render success`);
2797 return children || null;
2798 }
2799 }
2800
2801 class Component extends React.Component {
2802 render() {
2803 const {id} = this.props;
2804 Scheduler.log('Component render ' + id);
2805 return id;
2806 }
2807 }
2808
2809 class LocalBrokenCallbackRef extends React.Component {
2810 _ref = ref => {
2811 Scheduler.log('LocalBrokenCallbackRef ref ' + !!ref);
2812 if (ref === null) {
2813 throw Error('Expected');
2814 }
2815 };
2816
2817 render() {
2818 Scheduler.log('LocalBrokenCallbackRef render');
2819 return <div ref={this._ref}>ref</div>;
2820 }
2821 }
2822
2823 const container = document.createElement('div');
2824 const root = ReactDOMClient.createRoot(container);
2825
2826 await act(async () => {
2827 root.render(
2828 <LocalErrorBoundary id="OuterBoundary" fallbackID="OuterFallback">
2829 <Component id="sibling" />
2830 <LocalErrorBoundary id="InnerBoundary" fallbackID="InnerFallback">
2831 <LocalBrokenCallbackRef />
2832 </LocalErrorBoundary>
2833 </LocalErrorBoundary>,
2834 );
2835 });
2836
2837 expect(container.firstChild.textContent).toBe('sibling');
2838 expect(container.lastChild.textContent).toBe('ref');
2839 assertLog([
2840 'OuterBoundary render success',
2841 'Component render sibling',
2842 'InnerBoundary render success',
2843 'LocalBrokenCallbackRef render',
2844 'LocalBrokenCallbackRef ref true',
2845 ]);
2846
2847 await act(async () => {
2848 root.render(
2849 <LocalErrorBoundary id="OuterBoundary" fallbackID="OuterFallback">
2850 <Component id="sibling" />
2851 </LocalErrorBoundary>,
2852 );
2853 });
2854
2855 // React should skip over the unmounting boundary and find the nearest still-mounted boundary.
2856 expect(container.firstChild.textContent).toBe('OuterFallback');
2857 expect(container.lastChild.textContent).toBe('OuterFallback');
2858 assertLog([
2859 'OuterBoundary render success',
2860 'Component render sibling',
2861 'LocalBrokenCallbackRef ref false',
2862 'ErrorBoundary static getDerivedStateFromError',
2863 'OuterBoundary render error',
2864 'Component render OuterFallback',
2865 ]);
2866 });
2867 });