diff --git a/scripts/fiber/tests-passing.txt b/scripts/fiber/tests-passing.txt index 38057599f1844..a7c38adafe1a2 100644 --- a/scripts/fiber/tests-passing.txt +++ b/scripts/fiber/tests-passing.txt @@ -793,8 +793,19 @@ src/renderers/shared/fiber/__tests__/ReactIncremental-test.js * can nest batchedUpdates src/renderers/shared/fiber/__tests__/ReactIncrementalErrorHandling-test.js -* catches render error in a boundary during mounting -* propagates an error from a noop error boundary +* catches render error in a boundary during full deferred mounting +* catches render error in a boundary during partial deferred mounting +* catches render error in a boundary during animation mounting +* catches render error in a boundary during synchronous mounting +* catches render error in a boundary during batched mounting +* propagates an error from a noop error boundary during full deferred mounting +* propagates an error from a noop error boundary during partial deferred mounting +* propagates an error from a noop error boundary during animation mounting +* propagates an error from a noop error boundary during synchronous mounting +* propagates an error from a noop error boundary during batched mounting +* applies batched updates regardless despite errors in scheduling +* applies nested batched updates despite errors in scheduling +* applies sync updates regardless despite errors in scheduling * can schedule updates after uncaught error in render on mount * can schedule updates after uncaught error in render on update * can schedule updates after uncaught error during umounting diff --git a/src/renderers/shared/fiber/ReactFiberBeginWork.js b/src/renderers/shared/fiber/ReactFiberBeginWork.js index b35567478c742..9ce9824fb13f9 100644 --- a/src/renderers/shared/fiber/ReactFiberBeginWork.js +++ b/src/renderers/shared/fiber/ReactFiberBeginWork.js @@ -39,6 +39,7 @@ var { var { NoWork, OffscreenPriority, + TaskPriority, } = require('ReactPriorityLevel'); var { Placement, @@ -365,9 +366,12 @@ module.exports = function( workInProgress.firstEffect = null; workInProgress.lastEffect = null; - if (workInProgress.progressedPriority === priorityLevel) { + if (workInProgress.progressedPriority === priorityLevel || + priorityLevel === TaskPriority) { // If we have progressed work on this priority level already, we can // proceed this that as the child. + // We also can reuse the child if we're doing Task work. This avoids + // having the error boundaries doing the failed work twice before mount. workInProgress.child = workInProgress.progressedChild; } diff --git a/src/renderers/shared/fiber/__tests__/ReactIncrementalErrorHandling-test.js b/src/renderers/shared/fiber/__tests__/ReactIncrementalErrorHandling-test.js index 0f55886650227..6466ba790f2c1 100644 --- a/src/renderers/shared/fiber/__tests__/ReactIncrementalErrorHandling-test.js +++ b/src/renderers/shared/fiber/__tests__/ReactIncrementalErrorHandling-test.js @@ -25,7 +25,7 @@ describe('ReactIncrementalErrorHandling', () => { return { type: 'span', children: [], prop }; } - it('catches render error in a boundary during mounting', () => { + it('catches render error in a boundary during full deferred mounting', () => { class ErrorBoundary extends React.Component { state = {error: null}; unstable_handleError(error) { @@ -48,31 +48,424 @@ describe('ReactIncrementalErrorHandling', () => { ); - ReactNoop.flush(); + ReactNoop.flushDeferredPri(); + expect(ReactNoop.getChildren()).toEqual([span('Caught an error: Hello.')]); + }); + + it('catches render error in a boundary during partial deferred mounting', () => { + var ops = []; + class ErrorBoundary extends React.Component { + state = {error: null}; + unstable_handleError(error) { + ops.push('ErrorBoundary unstable_handleError'); + this.setState({error}); + } + render() { + if (this.state.error) { + ops.push('ErrorBoundary render error'); + return ; + } + ops.push('ErrorBoundary render success'); + return this.props.children; + } + } + + function BrokenRender(props) { + ops.push('BrokenRender'); + throw new Error('Hello'); + } + + ReactNoop.render( + + + + ); + + ReactNoop.flushDeferredPri(15); + expect(ops).toEqual([ + 'ErrorBoundary render success', + ]); + expect(ReactNoop.getChildren()).toEqual([]); + + ops.length = 0; + ReactNoop.flushDeferredPri(10); + expect(ops).toEqual([ + 'BrokenRender', + 'ErrorBoundary unstable_handleError', + 'ErrorBoundary render error', + ]); + expect(ReactNoop.getChildren()).toEqual([span('Caught an error: Hello.')]); + }); + + it('catches render error in a boundary during animation mounting', () => { + var ops = []; + class ErrorBoundary extends React.Component { + state = {error: null}; + unstable_handleError(error) { + ops.push('ErrorBoundary unstable_handleError'); + this.setState({error}); + } + render() { + if (this.state.error) { + ops.push('ErrorBoundary render error'); + return ; + } + ops.push('ErrorBoundary render success'); + return this.props.children; + } + } + + function BrokenRender(props) { + ops.push('BrokenRender'); + throw new Error('Hello'); + } + + ReactNoop.performAnimationWork(() => { + ReactNoop.render( + + + + ); + }); + + ReactNoop.flushAnimationPri(); + expect(ops).toEqual([ + 'ErrorBoundary render success', + 'BrokenRender', + 'ErrorBoundary unstable_handleError', + 'ErrorBoundary render error', + ]); expect(ReactNoop.getChildren()).toEqual([span('Caught an error: Hello.')]); }); - it('propagates an error from a noop error boundary', () => { + it('catches render error in a boundary during synchronous mounting', () => { + var ops = []; + class ErrorBoundary extends React.Component { + state = {error: null}; + unstable_handleError(error) { + ops.push('ErrorBoundary unstable_handleError'); + this.setState({error}); + } + render() { + if (this.state.error) { + ops.push('ErrorBoundary render error'); + return ; + } + ops.push('ErrorBoundary render success'); + return this.props.children; + } + } + + function BrokenRender(props) { + ops.push('BrokenRender'); + throw new Error('Hello'); + } + + ReactNoop.syncUpdates(() => { + ReactNoop.render( + + + + ); + }); + + expect(ops).toEqual([ + 'ErrorBoundary render success', + 'BrokenRender', + 'ErrorBoundary unstable_handleError', + 'ErrorBoundary render error', + ]); + expect(ReactNoop.getChildren()).toEqual([span('Caught an error: Hello.')]); + }); + + it('catches render error in a boundary during batched mounting', () => { + var ops = []; + class ErrorBoundary extends React.Component { + state = {error: null}; + unstable_handleError(error) { + ops.push('ErrorBoundary unstable_handleError'); + this.setState({error}); + } + render() { + if (this.state.error) { + ops.push('ErrorBoundary render error'); + return ; + } + ops.push('ErrorBoundary render success'); + return this.props.children; + } + } + + function BrokenRender(props) { + ops.push('BrokenRender'); + throw new Error('Hello'); + } + + ReactNoop.syncUpdates(() => { + ReactNoop.batchedUpdates(() => { + ReactNoop.render( + + Before the storm. + + ); + ReactNoop.render( + + + + ); + }); + }); + + expect(ops).toEqual([ + 'ErrorBoundary render success', + 'BrokenRender', + 'ErrorBoundary unstable_handleError', + 'ErrorBoundary render error', + ]); + expect(ReactNoop.getChildren()).toEqual([span('Caught an error: Hello.')]); + }); + + it('propagates an error from a noop error boundary during full deferred mounting', () => { + var ops = []; + class NoopBoundary extends React.Component { + unstable_handleError() { + ops.push('NoopErrorBoundary unstable_handleError'); + // Noop + } + render() { + ops.push('NoopErrorBoundary render'); + return this.props.children; + } + } + + function BrokenRender() { + ops.push('BrokenRender'); + throw new Error('Hello'); + } + + ReactNoop.render( + + + + ); + + expect(() => { + ReactNoop.flush(); + }).toThrow('Hello'); + expect(ops).toEqual([ + 'NoopErrorBoundary render', + 'BrokenRender', + 'NoopErrorBoundary unstable_handleError', + 'NoopErrorBoundary render', + 'BrokenRender', + ]); + expect(ReactNoop.getChildren()).toEqual([]); + }); + + it('propagates an error from a noop error boundary during partial deferred mounting', () => { + var ops = []; class NoopBoundary extends React.Component { unstable_handleError() { + ops.push('NoopErrorBoundary unstable_handleError'); // Noop } render() { + ops.push('NoopErrorBoundary render'); return this.props.children; } } - function RenderError() { - throw new Error('render error'); + function BrokenRender() { + ops.push('BrokenRender'); + throw new Error('Hello'); } ReactNoop.render( - + ); - expect(ReactNoop.flush).toThrow('render error'); + ReactNoop.flushDeferredPri(15); + expect(ops).toEqual([ + 'NoopErrorBoundary render', + ]); + + ops.length = []; + expect(() => { + ReactNoop.flushDeferredPri(10); + }).toThrow('Hello'); + expect(ops).toEqual([ + 'BrokenRender', + 'NoopErrorBoundary unstable_handleError', + 'NoopErrorBoundary render', + 'BrokenRender', + ]); + expect(ReactNoop.getChildren()).toEqual([]); + }); + + it('propagates an error from a noop error boundary during animation mounting', () => { + var ops = []; + class NoopBoundary extends React.Component { + unstable_handleError() { + ops.push('NoopErrorBoundary unstable_handleError'); + // Noop + } + render() { + ops.push('NoopErrorBoundary render'); + return this.props.children; + } + } + + function BrokenRender() { + ops.push('BrokenRender'); + throw new Error('Hello'); + } + + ReactNoop.performAnimationWork(() => { + ReactNoop.render( + + + + ); + }); + + expect(() => { + ReactNoop.flushAnimationPri(); + }).toThrow('Hello'); + expect(ops).toEqual([ + 'NoopErrorBoundary render', + 'BrokenRender', + 'NoopErrorBoundary unstable_handleError', + 'NoopErrorBoundary render', + 'BrokenRender', + ]); + expect(ReactNoop.getChildren()).toEqual([]); + }); + + it('propagates an error from a noop error boundary during synchronous mounting', () => { + var ops = []; + class NoopBoundary extends React.Component { + unstable_handleError() { + ops.push('NoopErrorBoundary unstable_handleError'); + // Noop + } + render() { + ops.push('NoopErrorBoundary render'); + return this.props.children; + } + } + + function BrokenRender() { + ops.push('BrokenRender'); + throw new Error('Hello'); + } + + expect(() => { + ReactNoop.syncUpdates(() => { + ReactNoop.render( + + + + ); + }); + }).toThrow('Hello'); + expect(ops).toEqual([ + 'NoopErrorBoundary render', + 'BrokenRender', + 'NoopErrorBoundary unstable_handleError', + 'NoopErrorBoundary render', + 'BrokenRender', + ]); + expect(ReactNoop.getChildren()).toEqual([]); + }); + + it('propagates an error from a noop error boundary during batched mounting', () => { + var ops = []; + class NoopBoundary extends React.Component { + unstable_handleError() { + ops.push('NoopErrorBoundary unstable_handleError'); + // Noop + } + render() { + ops.push('NoopErrorBoundary render'); + return this.props.children; + } + } + + function BrokenRender() { + ops.push('BrokenRender'); + throw new Error('Hello'); + } + + expect(() => { + ReactNoop.syncUpdates(() => { + ReactNoop.batchedUpdates(() => { + ReactNoop.render( + + Before the storm. + + ); + ReactNoop.render( + + + + ); + }); + }); + }).toThrow('Hello'); + expect(ops).toEqual([ + 'NoopErrorBoundary render', + 'BrokenRender', + 'NoopErrorBoundary unstable_handleError', + 'NoopErrorBoundary render', + 'BrokenRender', + ]); + expect(ReactNoop.getChildren()).toEqual([]); + }); + + it('applies batched updates regardless despite errors in scheduling', () => { + ReactNoop.render(); + expect(() => { + ReactNoop.batchedUpdates(() => { + ReactNoop.render(); + ReactNoop.render(); + throw new Error('Hello'); + }); + }).toThrow('Hello'); + ReactNoop.flush(); + expect(ReactNoop.getChildren()).toEqual([span('a:3')]); + }); + + it('applies nested batched updates despite errors in scheduling', () => { + ReactNoop.render(); + expect(() => { + ReactNoop.batchedUpdates(() => { + ReactNoop.render(); + ReactNoop.render(); + ReactNoop.batchedUpdates(() => { + ReactNoop.render(); + ReactNoop.render(); + throw new Error('Hello'); + }); + }); + }).toThrow('Hello'); + ReactNoop.flush(); + expect(ReactNoop.getChildren()).toEqual([span('a:5')]); + }); + + it('applies sync updates regardless despite errors in scheduling', () => { + ReactNoop.render(); + expect(() => { + ReactNoop.syncUpdates(() => { + ReactNoop.batchedUpdates(() => { + ReactNoop.render(); + ReactNoop.render(); + throw new Error('Hello'); + }); + }); + }).toThrow('Hello'); + expect(ReactNoop.getChildren()).toEqual([span('a:3')]); }); it('can schedule updates after uncaught error in render on mount', () => {