diff --git a/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx b/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx index dafe4dd20..739fe1d40 100644 --- a/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx +++ b/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx @@ -4905,4 +4905,73 @@ describeWithDOM('mount', () => { expect(root.childAt(0).children().debug()).to.equal('\n\n\n'); }); }); + + describe('lifecycles', () => { + it('should call `componentDidUpdate` when component’s `setState` is called', () => { + class Foo extends React.Component { + constructor(props) { + super(props); + this.state = { + foo: 'init', + }; + } + + componentDidUpdate() {} + + onChange() { + // enzyme can't handle the update because `this` is a ReactComponent instance, + // not a ShallowWrapper instance. + this.setState({ foo: 'onChange update' }); + } + + render() { + return
{this.state.foo}
; + } + } + const spy = sinon.spy(Foo.prototype, 'componentDidUpdate'); + + const wrapper = mount(); + wrapper.setState({ foo: 'wrapper setState update' }); + expect(wrapper.state('foo')).to.equal('wrapper setState update'); + expect(spy).to.have.property('callCount', 1); + wrapper.instance().onChange(); + expect(wrapper.state('foo')).to.equal('onChange update'); + expect(spy).to.have.property('callCount', 2); + }); + + it('should call `componentDidUpdate` when component’s `setState` is called through a bound method', () => { + class Foo extends React.Component { + constructor(props) { + super(props); + this.state = { + foo: 'init', + }; + this.onChange = this.onChange.bind(this); + } + + componentDidUpdate() {} + + onChange() { + // enzyme can't handle the update because `this` is a ReactComponent instance, + // not a ShallowWrapper instance. + this.setState({ foo: 'onChange update' }); + } + + render() { + return ( +
+ {this.state.foo} + +
+ ); + } + } + const spy = sinon.spy(Foo.prototype, 'componentDidUpdate'); + + const wrapper = mount(); + wrapper.find('button').prop('onClick')(); + expect(wrapper.state('foo')).to.equal('onChange update'); + expect(spy).to.have.property('callCount', 1); + }); + }); }); diff --git a/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx b/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx index 2278d4501..bd46b378c 100644 --- a/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx +++ b/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx @@ -4763,6 +4763,75 @@ describe('shallow', () => { }); }); + context('component instance', () => { + it('should call `componentDidUpdate` when component’s `setState` is called', () => { + class Foo extends React.Component { + constructor(props) { + super(props); + this.state = { + foo: 'init', + }; + } + + componentDidUpdate() {} + + onChange() { + // enzyme can't handle the update because `this` is a ReactComponent instance, + // not a ShallowWrapper instance. + this.setState({ foo: 'onChange update' }); + } + + render() { + return
{this.state.foo}
; + } + } + const spy = sinon.spy(Foo.prototype, 'componentDidUpdate'); + + const wrapper = shallow(); + wrapper.setState({ foo: 'wrapper setState update' }); + expect(wrapper.state('foo')).to.equal('wrapper setState update'); + expect(spy).to.have.property('callCount', 1); + wrapper.instance().onChange(); + expect(wrapper.state('foo')).to.equal('onChange update'); + expect(spy).to.have.property('callCount', 2); + }); + + it('should call `componentDidUpdate` when component’s `setState` is called through a bound method', () => { + class Foo extends React.Component { + constructor(props) { + super(props); + this.state = { + foo: 'init', + }; + this.onChange = this.onChange.bind(this); + } + + componentDidUpdate() {} + + onChange() { + // enzyme can't handle the update because `this` is a ReactComponent instance, + // not a ShallowWrapper instance. + this.setState({ foo: 'onChange update' }); + } + + render() { + return ( +
+ {this.state.foo} + +
+ ); + } + } + const spy = sinon.spy(Foo.prototype, 'componentDidUpdate'); + + const wrapper = shallow(); + wrapper.find('button').prop('onClick')(); + expect(wrapper.state('foo')).to.equal('onChange update'); + expect(spy).to.have.property('callCount', 1); + }); + }); + describeIf(is('>= 16'), 'support getSnapshotBeforeUpdate', () => { it('should call getSnapshotBeforeUpdate and pass snapshot to componentDidUpdate', () => { const spy = sinon.spy(); diff --git a/packages/enzyme/src/ShallowWrapper.js b/packages/enzyme/src/ShallowWrapper.js index b355e7a9b..8c417144a 100644 --- a/packages/enzyme/src/ShallowWrapper.js +++ b/packages/enzyme/src/ShallowWrapper.js @@ -38,6 +38,7 @@ const RENDERER = sym('__renderer__'); const UNRENDERED = sym('__unrendered__'); const ROOT = sym('__root__'); const OPTIONS = sym('__options__'); +const SET_STATE = sym('__setState__'); /** * Finds all nodes in the current wrapper nodes' render trees that match the provided predicate * function. @@ -170,6 +171,13 @@ class ShallowWrapper { privateSet(this, RENDERER, renderer); this[RENDERER].render(nodes, options.context); const { instance } = this[RENDERER].getNode(); + const adapter = getAdapter(this[OPTIONS]); + const lifecycles = getAdapterLifecycles(adapter); + // Ensure to call componentDidUpdate when instance.setState is called + if (instance && lifecycles.componentDidUpdate.onSetState && !instance[SET_STATE]) { + privateSet(instance, SET_STATE, instance.setState); + instance.setState = (...args) => this.setState(...args); + } if ( !options.disableLifecycleMethods && instance @@ -438,7 +446,11 @@ class ShallowWrapper { } // We don't pass the setState callback here // to guarantee to call the callback after finishing the render - instance.setState(state); + if (instance[SET_STATE]) { + instance[SET_STATE](state); + } else { + instance.setState(state); + } if (spy) { shouldRender = spy.getLastReturnValue(); spy.restore();