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