Add tests for recovery from errors thrown in the reconciler (#8462)
Test that errors in the reconciler can be caught by error boundaries, and that we can still schedule updates if they are uncaught.
Dan Abramov committed
Nov 30, 2016 at 17:00 UTC
b1c988d0de5cdc479db4fa4ea9414ca79cd9073c
2 files changed
+96
scripts/fiber/tests-passing.txt
+3
@@ -1087,6 +1087,9 @@ src/renderers/shared/fiber/__tests__/ReactIncrementalErrorHandling-test.js
1087
* can schedule updates after uncaught error during umounting
1088
* continues work on other roots despite caught errors
1089
* continues work on other roots despite uncaught errors
1090
+* catches reconciler errors in a boundary during mounting
1091
+* catches reconciler errors in a boundary during update
1092
+* recovers from uncaught reconciler errors
1093
1094
src/renderers/shared/fiber/__tests__/ReactIncrementalReflection-test.js
1095
* handles isMounted even when the initial render is deferred
src/renderers/shared/fiber/__tests__/ReactIncrementalErrorHandling-test.js
+93
@@ -275,4 +275,97 @@ describe('ReactIncrementalErrorHandling', () => {
275
expect(ReactNoop.getChildren('e')).toEqual(null);
276
expect(ReactNoop.getChildren('f')).toEqual(null);
277
});
278
+
279
+ it('catches reconciler errors in a boundary during mounting', () => {
280
+ spyOn(console, 'error');
281
+
282
+ class ErrorBoundary extends React.Component {
283
+ state = {error: null};
284
+ unstable_handleError(error) {
285
+ this.setState({error});
286
+ }
287
+ render() {
288
+ if (this.state.error) {
289
+ return <span prop={this.state.error.message} />;
290
+ }
291
+ return this.props.children;
292
+ }
293
+ }
294
+
295
+ const InvalidType = undefined;
296
+ const brokenElement = <InvalidType />;
297
+ function BrokenRender(props) {
298
+ return brokenElement;
299
+ }
300
+
301
+ ReactNoop.render(
302
+ <ErrorBoundary>
303
+ <BrokenRender />
304
+ </ErrorBoundary>
305
+ );
306
+ ReactNoop.flush();
307
+ expect(ReactNoop.getChildren()).toEqual([span(
308
+ 'Element type is invalid: expected a string (for built-in components) or ' +
309
+ 'a class/function (for composite components) but got: undefined.'
310
+ )]);
311
+ expect(console.error.calls.count()).toBe(1);
312
+ });
313
+
314
+ it('catches reconciler errors in a boundary during update', () => {
315
+ spyOn(console, 'error');
316
+
317
+ class ErrorBoundary extends React.Component {
318
+ state = {error: null};
319
+ unstable_handleError(error) {
320
+ this.setState({error});
321
+ }
322
+ render() {
323
+ if (this.state.error) {
324
+ return <span prop={this.state.error.message} />;
325
+ }
326
+ return this.props.children;
327
+ }
328
+ }
329
+
330
+ const InvalidType = undefined;
331
+ const brokenElement = <InvalidType />;
332
+ function BrokenRender(props) {
333
+ return props.fail ? brokenElement : <span />;
334
+ }
335
+
336
+ ReactNoop.render(
337
+ <ErrorBoundary>
338
+ <BrokenRender fail={false} />
339
+ </ErrorBoundary>
340
+ );
341
+ ReactNoop.flush();
342
+
343
+ ReactNoop.render(
344
+ <ErrorBoundary>
345
+ <BrokenRender fail={true} />
346
+ </ErrorBoundary>
347
+ );
348
+ ReactNoop.flush();
349
+ expect(ReactNoop.getChildren()).toEqual([span(
350
+ 'Element type is invalid: expected a string (for built-in components) or ' +
351
+ 'a class/function (for composite components) but got: undefined.'
352
+ )]);
353
+ expect(console.error.calls.count()).toBe(1);
354
+ });
355
+
356
+ it('recovers from uncaught reconciler errors', () => {
357
+ spyOn(console, 'error');
358
+ const InvalidType = undefined;
359
+ ReactNoop.render(<InvalidType />);
360
+ expect(() => {
361
+ ReactNoop.flush();
362
+ }).toThrowError(
363
+ 'Element type is invalid: expected a string (for built-in components) or ' +
364
+ 'a class/function (for composite components) but got: undefined.'
365
+ );
366
+
367
+ ReactNoop.render(<span prop="hi" />);
368
+ ReactNoop.flush();
369
+ expect(ReactNoop.getChildren()).toEqual([span('hi')]);
370
+ });
371
});