From ebdcce779bb2473341ccee877804017154ba1202 Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Mon, 18 Mar 2019 22:54:23 -0700 Subject: [PATCH 1/3] [Tests] properly install the right renderer version --- env.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/env.js b/env.js index 0bec32a35..a63c93a2a 100755 --- a/env.js +++ b/env.js @@ -126,10 +126,11 @@ Promise.resolve() const peerDeps = adapterJson.peerDependencies; const installs = Object.keys(peerDeps) .filter(key => !key.startsWith('enzyme')) - .map((key) => { - const peerVersion = (key === 'react-test-renderer' && process.env.RENDERER) ? process.env.RENDERER : peerDeps[key]; - return `${key}@${key.startsWith('react') ? reactVersion : peerVersion}`; - }); + .map(key => `${key}@${key.startsWith('react') ? reactVersion : peerDeps[key]}`); + + if (process.env.RENDERER) { + installs.push(`react-test-renderer@${process.env.RENDERER}`); + } // eslint-disable-next-line no-param-reassign testJson.dependencies[adapterName] = adapterJson.version; From e76ea4faf8fd05cf68c7180916bb43c51940b4e3 Mon Sep 17 00:00:00 2001 From: Maciej Barelkowski Date: Wed, 13 Mar 2019 09:45:35 +0100 Subject: [PATCH 2/3] [enzyme-adapter-react-16] [New] add `getDerivedStateFromError` support --- .../src/ReactSixteenAdapter.js | 16 +- packages/enzyme-adapter-utils/src/Utils.js | 24 +- .../test/ReactWrapper-spec.jsx | 384 +++++++++++++++++- .../test/ShallowWrapper-spec.jsx | 350 +++++++++++++++- 4 files changed, 760 insertions(+), 14 deletions(-) diff --git a/packages/enzyme-adapter-react-16/src/ReactSixteenAdapter.js b/packages/enzyme-adapter-react-16/src/ReactSixteenAdapter.js index 76c2d5db9..76ad49ac0 100644 --- a/packages/enzyme-adapter-react-16/src/ReactSixteenAdapter.js +++ b/packages/enzyme-adapter-react-16/src/ReactSixteenAdapter.js @@ -316,6 +316,7 @@ class ReactSixteenAdapter extends EnzymeAdapter { getChildContext: { calledByRenderer: false, }, + getDerivedStateFromError: is166, }, }; } @@ -362,8 +363,17 @@ class ReactSixteenAdapter extends EnzymeAdapter { return instance ? toTree(instance._reactInternalFiber).rendered : null; }, simulateError(nodeHierarchy, rootNode, error) { - const { instance: catchingInstance } = nodeHierarchy - .find(x => x.instance && x.instance.componentDidCatch) || {}; + const isErrorBoundary = ({ instance: elInstance, type }) => { + if (is166 && type && type.getDerivedStateFromError) { + return true; + } + return elInstance && elInstance.componentDidCatch; + }; + + const { + instance: catchingInstance, + type: catchingType, + } = nodeHierarchy.find(isErrorBoundary) || {}; simulateError( error, @@ -372,6 +382,7 @@ class ReactSixteenAdapter extends EnzymeAdapter { nodeHierarchy, nodeTypeFromType, adapter.displayNameOfNode, + is166 ? catchingType : undefined, ); }, simulateEvent(node, event, mock) { @@ -482,6 +493,7 @@ class ReactSixteenAdapter extends EnzymeAdapter { nodeHierarchy.concat(cachedNode), nodeTypeFromType, adapter.displayNameOfNode, + is166 ? cachedNode.type : undefined, ); }, simulateEvent(node, event, ...args) { diff --git a/packages/enzyme-adapter-utils/src/Utils.js b/packages/enzyme-adapter-utils/src/Utils.js index bdd350e58..3a0b5bb72 100644 --- a/packages/enzyme-adapter-utils/src/Utils.js +++ b/packages/enzyme-adapter-utils/src/Utils.js @@ -266,19 +266,27 @@ export function simulateError( hierarchy, getNodeType = nodeTypeFromType, getDisplayName = displayNameOfNode, + catchingType = {}, ) { - const { componentDidCatch } = catchingInstance || {}; - if (!componentDidCatch) { + const instance = catchingInstance || {}; + + const { componentDidCatch } = instance; + + const { getDerivedStateFromError } = catchingType; + + if (!componentDidCatch && !getDerivedStateFromError) { throw error; } - const componentStack = getComponentStack( - hierarchy, - getNodeType, - getDisplayName, - ); + if (getDerivedStateFromError) { + const stateUpdate = getDerivedStateFromError.call(catchingType, error); + instance.setState(stateUpdate); + } - componentDidCatch.call(catchingInstance, error, { componentStack }); + if (componentDidCatch) { + const componentStack = getComponentStack(hierarchy, getNodeType, getDisplayName); + componentDidCatch.call(instance, error, { componentStack }); + } } export function getMaskedContext(contextTypes, unmaskedContext) { diff --git a/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx b/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx index fa91fc33c..4bd94d605 100644 --- a/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx +++ b/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx @@ -5946,14 +5946,18 @@ describeWithDOM('mount', () => { } render() { - const { throws } = this.state; + const { + didThrow, + throws, + } = this.state; + return (
- {this.state.didThrow ? 'HasThrown' : 'HasNotThrown'} + {didThrow ? 'HasThrown' : 'HasNotThrown'}
@@ -6081,6 +6085,382 @@ describeWithDOM('mount', () => { }); }); + describeIf(is('>= 16.6'), 'getDerivedStateFromError', () => { + describe('errors inside an error boundary', () => { + const errorToThrow = new EvalError('threw an error!'); + + function Thrower({ throws }) { + if (throws) { + throw errorToThrow; + } + return null; + } + + function getErrorBoundary() { + return class ErrorBoundary extends React.Component { + static getDerivedStateFromError() { + return { + throws: false, + didThrow: true, + }; + } + + constructor(props) { + super(props); + this.state = { + throws: false, + didThrow: false, + }; + } + + render() { + const { + didThrow, + throws, + } = this.state; + + return ( +
+ + + +
+ {didThrow ? 'HasThrown' : 'HasNotThrown'} +
+
+
+
+ ); + } + }; + } + + function ErrorSFC({ component }) { + return component(); + } + + describe('Thrower', () => { + it('does not throw when `throws` is `false`', () => { + expect(() => mount()).not.to.throw(); + }); + + it('throws when `throws` is `true`', () => { + expect(() => mount()).to.throw(); + try { + mount(); + expect(true).to.equal(false, 'this line should not be reached'); + } catch (error) { + expect(error).to.equal(errorToThrow); + } + }); + }); + + it('catches a simulated error', () => { + const ErrorBoundary = getErrorBoundary(); + + const spy = sinon.spy(ErrorBoundary, 'getDerivedStateFromError'); + const wrapper = mount(); + + expect(spy).to.have.property('callCount', 0); + + expect(() => wrapper.find(Thrower).simulateError(errorToThrow)).not.to.throw(); + + expect(spy).to.have.property('callCount', 1); + + expect(spy.args).to.be.an('array').and.have.lengthOf(1); + const [[actualError]] = spy.args; + expect(actualError).to.equal(errorToThrow); + }); + + it('rerenders on a simulated error', () => { + const ErrorBoundary = getErrorBoundary(); + + const wrapper = mount(); + + expect(wrapper.find({ children: 'HasThrown' })).to.have.lengthOf(0); + expect(wrapper.find({ children: 'HasNotThrown' })).to.have.lengthOf(1); + + expect(() => wrapper.find(Thrower).simulateError(errorToThrow)).not.to.throw(); + + expect(wrapper.find({ children: 'HasThrown' })).to.have.lengthOf(1); + expect(wrapper.find({ children: 'HasNotThrown' })).to.have.lengthOf(0); + }); + + it('rerenders on a simulated error with an SFC root', () => { + const ErrorBoundary = getErrorBoundary(); + + const wrapper = mount( } />); + + expect(wrapper.find({ children: 'HasThrown' })).to.have.lengthOf(0); + expect(wrapper.find({ children: 'HasNotThrown' })).to.have.lengthOf(1); + + expect(() => wrapper.find(Thrower).simulateError(errorToThrow)).not.to.throw(); + + expect(wrapper.find({ children: 'HasThrown' })).to.have.lengthOf(1); + expect(wrapper.find({ children: 'HasNotThrown' })).to.have.lengthOf(0); + }); + + it('catches errors during render', () => { + const ErrorBoundary = getErrorBoundary(); + + const spy = sinon.spy(ErrorBoundary, 'getDerivedStateFromError'); + const wrapper = mount(); + + expect(spy).to.have.property('callCount', 0); + + wrapper.setState({ throws: true }); + + expect(spy).to.have.property('callCount', 1); + + expect(spy.args).to.be.an('array').and.have.lengthOf(1); + const [[actualError]] = spy.args; + expect(actualError).to.equal(errorToThrow); + }); + + it('works when the root is an SFC', () => { + const ErrorBoundary = getErrorBoundary(); + + const spy = sinon.spy(ErrorBoundary, 'getDerivedStateFromError'); + const wrapper = mount( } />); + + expect(spy).to.have.property('callCount', 0); + + wrapper.find(ErrorBoundary).setState({ throws: true }); + + expect(spy).to.have.property('callCount', 1); + + expect(spy.args).to.be.an('array').and.have.lengthOf(1); + const [[actualError]] = spy.args; + expect(actualError).to.equal(errorToThrow); + }); + }); + }); + + describeIf(is('>= 16.6'), 'getDerivedStateFromError and componentDidCatch combined', () => { + + const errorToThrow = new EvalError('threw an error!'); + const expectedInfo = { + componentStack: ` + in Thrower (created by ErrorBoundary) + in div (created by ErrorBoundary) + in ErrorBoundary (created by WrapperComponent) + in WrapperComponent`, + }; + + function Thrower({ throws }) { + if (throws) { + throw errorToThrow; + } + return null; + } + + describe('errors inside error boundary when getDerivedStateFromProps returns update', () => { + let lifecycleSpy; + let stateSpy; + + beforeEach(() => { + lifecycleSpy = sinon.spy(); + stateSpy = sinon.spy(); + }); + + class ErrorBoundary extends React.Component { + static getDerivedStateFromError(error) { + lifecycleSpy('getDerivedStateFromError', error); + return { + didThrow: true, + throws: false, + }; + } + + constructor(props) { + super(props); + this.state = { + didThrow: false, + throws: false, + }; + + lifecycleSpy('constructor'); + } + + componentDidCatch(error, info) { + lifecycleSpy('componentDidCatch', error, info); + stateSpy({ ...this.state }); + } + + render() { + lifecycleSpy('render'); + + const { + throws, + } = this.state; + + return ( +
+ +
+ ); + } + } + + it('calls getDerivedStateFromError first and then componentDidCatch', () => { + const wrapper = mount(); + + expect(lifecycleSpy).to.have.property('callCount', 2); + expect(lifecycleSpy.args).to.deep.equal([ + ['constructor'], + ['render'], + ]); + + expect(stateSpy).to.have.property('callCount', 0); + + lifecycleSpy.resetHistory(); + + wrapper.setState({ throws: true }); + + expect(lifecycleSpy).to.have.property('callCount', 4); + expect(lifecycleSpy.args).to.deep.equal([ + ['render'], + ['getDerivedStateFromError', errorToThrow], + ['render'], + ['componentDidCatch', errorToThrow, expectedInfo], + ]); + + expect(stateSpy).to.have.property('callCount', 1); + expect(stateSpy.args).to.deep.equal([ + [{ + throws: false, + didThrow: true, + }], + ]); + }); + + it('calls getDerivedStateFromError first and then componentDidCatch on a simulated error', () => { + const wrapper = mount(); + + expect(lifecycleSpy).to.have.property('callCount', 2); + expect(lifecycleSpy.args).to.deep.equal([ + ['constructor'], + ['render'], + ]); + + expect(stateSpy).to.have.property('callCount', 0); + + lifecycleSpy.resetHistory(); + + expect(() => wrapper.find(Thrower).simulateError(errorToThrow)).not.to.throw(); + + expect(lifecycleSpy).to.have.property('callCount', 3); + expect(lifecycleSpy.args).to.deep.equal([ + ['getDerivedStateFromError', errorToThrow], + ['render'], + ['componentDidCatch', errorToThrow, expectedInfo], + ]); + + expect(stateSpy).to.have.property('callCount', 1); + expect(stateSpy.args).to.deep.equal([ + [{ + throws: false, + didThrow: true, + }], + ]); + }); + }); + + describe('errors inside error boundary when getDerivedStateFromError does not return update', () => { + let spy; + + beforeEach(() => { + spy = sinon.spy(); + }); + + class ErrorBoundary extends React.Component { + static getDerivedStateFromError(error) { + spy('getDerivedStateFromError', error); + return null; + } + + constructor(props) { + super(props); + this.state = { + didThrow: false, + throws: false, + }; + + spy('constructor'); + } + + componentDidCatch(error, info) { + spy('componentDidCatch', error, info); + + this.setState({ + didThrow: true, + throws: false, + }); + } + + render() { + spy('render'); + + const { + throws, + didThrow, + } = this.state; + + return ( +
+ +
+ {didThrow ? 'HasThrown' : 'HasNotThrown'} +
+
+ ); + } + } + + it('renders again without calling componentDidCatch and then fails', () => { + const wrapper = mount(); + + expect(spy).to.have.property('callCount', 2); + expect(spy.args).to.deep.equal([ + ['constructor'], + ['render'], + ]); + + spy.resetHistory(); + + expect(() => wrapper.setState({ throws: true })).to.throw(errorToThrow); + + expect(spy).to.have.property('callCount', 3); + expect(spy.args).to.deep.equal([ + ['render'], + ['getDerivedStateFromError', errorToThrow], + ['render'], + ]); + }); + + it('renders again on simulated error', () => { + const wrapper = mount(); + + expect(spy).to.have.property('callCount', 2); + expect(spy.args).to.deep.equal([ + ['constructor'], + ['render'], + ]); + + spy.resetHistory(); + + expect(() => wrapper.find(Thrower).simulateError(errorToThrow)).not.to.throw(); + + expect(spy).to.have.property('callCount', 3); + expect(spy.args).to.deep.equal([ + ['getDerivedStateFromError', errorToThrow], + ['componentDidCatch', errorToThrow, expectedInfo], + ['render'], + ]); + }); + }); + }); + context('mounting phase', () => { it('calls componentWillMount and componentDidMount', () => { const spy = sinon.spy(); diff --git a/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx b/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx index e66808fa1..f4b3d7e8d 100644 --- a/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx +++ b/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx @@ -6352,14 +6352,17 @@ describe('shallow', () => { } render() { - const { throws } = this.state; + const { + didThrow, + throws, + } = this.state; return (
- {this.state.didThrow ? 'HasThrown' : 'HasNotThrown'} + {didThrow ? 'HasThrown' : 'HasNotThrown'}
@@ -6438,6 +6441,349 @@ describe('shallow', () => { }); }); + describeIf(is('>= 16.6'), 'getDerivedStateFromError', () => { + describe('errors inside an error boundary', () => { + const errorToThrow = new EvalError('threw an error!'); + + function Thrower({ throws }) { + if (throws) { + throw errorToThrow; + } + return null; + } + + function getErrorBoundary() { + return class ErrorBoundary extends React.Component { + static getDerivedStateFromError() { + return { + throws: false, + didThrow: true, + }; + } + + constructor(props) { + super(props); + this.state = { + throws: false, + didThrow: false, + }; + } + + render() { + const { + didThrow, + throws, + } = this.state; + + return ( +
+ + + +
+ {didThrow ? 'HasThrown' : 'HasNotThrown'} +
+
+
+
+ ); + } + }; + } + + describe('Thrower', () => { + it('does not throw when `throws` is `false`', () => { + expect(() => shallow()).not.to.throw(); + }); + + it('throws when `throws` is `true`', () => { + expect(() => shallow()).to.throw(); + }); + }); + + it('catches a simulated error', () => { + const ErrorBoundary = getErrorBoundary(); + + const spy = sinon.spy(ErrorBoundary, 'getDerivedStateFromError'); + const wrapper = shallow(); + + expect(spy).to.have.property('callCount', 0); + + expect(() => wrapper.find(Thrower).simulateError(errorToThrow)).not.to.throw(); + + expect(spy).to.have.property('callCount', 1); + + expect(spy.args).to.be.an('array').and.have.lengthOf(1); + const [[actualError]] = spy.args; + expect(actualError).to.equal(errorToThrow); + }); + + it('rerenders on a simulated error', () => { + const ErrorBoundary = getErrorBoundary(); + + const wrapper = shallow(); + + expect(wrapper.find({ children: 'HasThrown' })).to.have.lengthOf(0); + expect(wrapper.find({ children: 'HasNotThrown' })).to.have.lengthOf(1); + + expect(() => wrapper.find(Thrower).simulateError(errorToThrow)).not.to.throw(); + + expect(wrapper.find({ children: 'HasThrown' })).to.have.lengthOf(1); + expect(wrapper.find({ children: 'HasNotThrown' })).to.have.lengthOf(0); + }); + + it('does not catch errors during shallow render', () => { + const ErrorBoundary = getErrorBoundary(); + + const spy = sinon.spy(ErrorBoundary, 'getDerivedStateFromError'); + const wrapper = shallow(); + + expect(spy).to.have.property('callCount', 0); + + wrapper.setState({ throws: true }); + + expect(spy).to.have.property('callCount', 0); + + const thrower = wrapper.find(Thrower); + expect(thrower).to.have.lengthOf(1); + expect(thrower.props()).to.have.property('throws', true); + + expect(() => thrower.dive()).to.throw(errorToThrow); + + expect(spy).to.have.property('callCount', 0); + + expect(wrapper.find({ children: 'HasThrown' })).to.have.lengthOf(0); + expect(wrapper.find({ children: 'HasNotThrown' })).to.have.lengthOf(1); + }); + }); + }); + + describeIf(is('>= 16.6'), 'getDerivedStateFromError and componentDidCatch combined', () => { + + const errorToThrow = new EvalError('threw an error!'); + const expectedInfo = { + componentStack: ` + in Thrower (created by ErrorBoundary) + in div (created by ErrorBoundary) + in ErrorBoundary (created by WrapperComponent) + in WrapperComponent`, + }; + + function Thrower({ throws }) { + if (throws) { + throw errorToThrow; + } + return null; + } + + describe('errors inside error boundary when getDerivedStateFromProps returns update', () => { + let lifecycleSpy; + let stateSpy; + + beforeEach(() => { + lifecycleSpy = sinon.spy(); + stateSpy = sinon.spy(); + }); + + class ErrorBoundary extends React.Component { + static getDerivedStateFromError(error) { + lifecycleSpy('getDerivedStateFromError', error); + return { + didThrow: true, + throws: false, + }; + } + + constructor(props) { + super(props); + this.state = { + didThrow: false, + throws: false, + }; + + lifecycleSpy('constructor'); + } + + componentDidCatch(error, info) { + lifecycleSpy('componentDidCatch', error, info); + stateSpy({ ...this.state }); + } + + render() { + lifecycleSpy('render'); + + const { + throws, + } = this.state; + + return ( +
+ +
+ ); + } + } + + it('does not catch errors during shallow render', () => { + const wrapper = shallow(); + + expect(lifecycleSpy).to.have.property('callCount', 2); + expect(lifecycleSpy.args).to.deep.equal([ + ['constructor'], + ['render'], + ]); + + expect(stateSpy).to.have.property('callCount', 0); + + lifecycleSpy.resetHistory(); + + wrapper.setState({ throws: true }); + + const thrower = wrapper.find(Thrower); + expect(thrower).to.have.lengthOf(1); + expect(thrower.props()).to.have.property('throws', true); + + expect(() => thrower.dive()).to.throw(errorToThrow); + + expect(lifecycleSpy).to.have.property('callCount', 1); + expect(lifecycleSpy.args).to.deep.equal([ + ['render'], + ]); + }); + + it('calls getDerivedStateFromError first and then componentDidCatch for simulated error', () => { + const wrapper = shallow(); + + expect(lifecycleSpy).to.have.property('callCount', 2); + expect(lifecycleSpy.args).to.deep.equal([ + ['constructor'], + ['render'], + ]); + + expect(stateSpy).to.have.property('callCount', 0); + + lifecycleSpy.resetHistory(); + + expect(() => wrapper.find(Thrower).simulateError(errorToThrow)).not.to.throw(); + + expect(lifecycleSpy).to.have.property('callCount', 3); + expect(lifecycleSpy.args).to.deep.equal([ + ['getDerivedStateFromError', errorToThrow], + ['render'], + ['componentDidCatch', errorToThrow, expectedInfo], + ]); + + expect(stateSpy).to.have.property('callCount', 1); + expect(stateSpy.args).to.deep.equal([ + [{ + throws: false, + didThrow: true, + }], + ]); + }); + }); + + describe('errors inside error boundary when getDerivedStateFromError does not return update', () => { + let spy; + + beforeEach(() => { + spy = sinon.spy(); + }); + + class ErrorBoundary extends React.Component { + static getDerivedStateFromError(error) { + spy('getDerivedStateFromError', error); + return null; + } + + constructor(props) { + super(props); + this.state = { + didThrow: false, + throws: false, + }; + + spy('constructor'); + } + + componentDidCatch(error, info) { + spy('componentDidCatch', error, info); + + this.setState({ + didThrow: true, + throws: false, + }); + } + + render() { + spy('render'); + + const { + didThrow, + throws, + } = this.state; + + return ( +
+ +
+ {didThrow ? 'HasThrown' : 'HasNotThrown'} +
+
+ ); + } + } + + it('does not catch errors during shallow render', () => { + const wrapper = shallow(); + + expect(spy).to.have.property('callCount', 2); + expect(spy.args).to.deep.equal([ + ['constructor'], + ['render'], + ]); + + spy.resetHistory(); + + wrapper.setState({ throws: true }); + + const thrower = wrapper.find(Thrower); + expect(thrower).to.have.lengthOf(1); + expect(thrower.props()).to.have.property('throws', true); + + expect(() => thrower.dive()).to.throw(errorToThrow); + + expect(spy).to.have.property('callCount', 1); + expect(spy.args).to.deep.equal([ + ['render'], + ]); + }); + + it('rerenders on a simulated error', () => { + const wrapper = shallow(); + + expect(spy).to.have.property('callCount', 2); + expect(spy.args).to.deep.equal([ + ['constructor'], + ['render'], + ]); + + spy.resetHistory(); + + const thrower = wrapper.find(Thrower); + + expect(() => thrower.simulateError(errorToThrow)).not.to.throw(errorToThrow); + + expect(spy).to.have.property('callCount', 3); + expect(spy.args).to.deep.equal([ + ['getDerivedStateFromError', errorToThrow], + ['componentDidCatch', errorToThrow, expectedInfo], + ['render'], + ]); + }); + }); + }); + context('mounting phase', () => { it('calls componentWillMount and componentDidMount', () => { const spy = sinon.spy(); From dfd128989f959a9a6d2d4358b36f23292fce0746 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Bare=C5=82kowski?= Date: Mon, 11 Mar 2019 22:28:01 +0100 Subject: [PATCH 3/3] [Docs] update `simulateError` with `getDerivedStateFromError` --- docs/api/ReactWrapper/simulateError.md | 17 +++++++++++++++-- docs/api/ShallowWrapper/simulateError.md | 17 +++++++++++++++-- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/docs/api/ReactWrapper/simulateError.md b/docs/api/ReactWrapper/simulateError.md index c34b8c2df..451618ab3 100644 --- a/docs/api/ReactWrapper/simulateError.md +++ b/docs/api/ReactWrapper/simulateError.md @@ -2,7 +2,7 @@ Simulate a component throwing an error as part of its rendering lifecycle. -This is particularly useful in combination with React 16 error boundaries (ie, the `componentDidCatch` lifecycle method). +This is particularly useful in combination with React 16 error boundaries (ie, the `componentDidCatch` and `static getDerivedStateFromError` lifecycle methods). #### Arguments @@ -26,6 +26,17 @@ function Something() { } class ErrorBoundary extends React.Component { + static getDerivedStateFromError(error) { + return { + hasError: true, + }; + } + + constructor(props) { + super(props); + this.state = { hasError: false }; + } + componentDidCatch(error, info) { const { spy } = this.props; spy(error, info); @@ -33,9 +44,10 @@ class ErrorBoundary extends React.Component { render() { const { children } = this.props; + const { hasError } = this.state; return ( - {children} + {hasError ? 'Error' : children} ); } @@ -50,6 +62,7 @@ const wrapper = mount(); const error = new Error('hi!'); wrapper.find(Something).simulateError(error); +expect(wrapper.state()).to.have.property('hasError', true); expect(spy).to.have.property('callCount', 1); expect(spy.args).to.deep.equal([ error, diff --git a/docs/api/ShallowWrapper/simulateError.md b/docs/api/ShallowWrapper/simulateError.md index cdc0331d8..e8f7b71dd 100644 --- a/docs/api/ShallowWrapper/simulateError.md +++ b/docs/api/ShallowWrapper/simulateError.md @@ -2,7 +2,7 @@ Simulate a component throwing an error as part of its rendering lifecycle. -This is particularly useful in combination with React 16 error boundaries (ie, the `componentDidCatch` lifecycle method). +This is particularly useful in combination with React 16 error boundaries (ie, the `componentDidCatch` and `static getDerivedStateFromError` lifecycle methods). #### Arguments @@ -26,6 +26,17 @@ function Something() { } class ErrorBoundary extends React.Component { + static getDerivedStateFromError(error) { + return { + hasError: true, + }; + } + + constructor(props) { + super(props); + this.state = { hasError: false }; + } + componentDidCatch(error, info) { const { spy } = this.props; spy(error, info); @@ -33,9 +44,10 @@ class ErrorBoundary extends React.Component { render() { const { children } = this.props; + const { hasError } = this.state; return ( - {children} + {hasError ? 'Error' : children} ); } @@ -50,6 +62,7 @@ const wrapper = shallow(); const error = new Error('hi!'); wrapper.find(Something).simulateError(error); +expect(wrapper.state()).to.have.property('hasError', true); expect(spy).to.have.property('callCount', 1); expect(spy.args).to.deep.equal([ error,