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();