diff --git a/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx b/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx index c01f4c056..cc3c97d23 100644 --- a/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx +++ b/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx @@ -2502,8 +2502,8 @@ describeWithDOM('mount', () => { ]); }); - describe('setProps should not call componentDidUpdate twice', () => { - it('first test case', () => { + describe('setProps does not call componentDidUpdate twice', () => { + it('when setState is called in cWRP', () => { class Dummy extends React.Component { constructor(...args) { super(...args); @@ -6946,6 +6946,125 @@ describeWithDOM('mount', () => { wrapper.instance().setDeepDifferentState(); expect(updateSpy).to.have.property('callCount', 1); }); + + describeIf(is('>= 16.3'), 'setProps calls `componentDidUpdate` when `getDerivedStateFromProps` is defined', () => { + class DummyComp extends PureComponent { + constructor(...args) { + super(...args); + this.state = { state: -1 }; + } + + static getDerivedStateFromProps({ changeState, counter }) { + return changeState ? { state: counter * 10 } : null; + } + + componentDidUpdate() {} + + render() { + const { counter } = this.props; + const { state } = this.state; + return ( +

+ {counter} + {state} +

+ ); + } + } + + const cDU = sinon.spy(DummyComp.prototype, 'componentDidUpdate'); + const gDSFP = sinon.spy(DummyComp, 'getDerivedStateFromProps'); + + beforeEach(() => { // eslint-disable-line mocha/no-sibling-hooks + cDU.resetHistory(); + gDSFP.resetHistory(); + }); + + it('with no state changes, calls both methods with a sync and async setProps', () => { + const wrapper = mount(); + + expect(cDU).to.have.property('callCount', 0); + expect(gDSFP).to.have.property('callCount', 1); + const [firstCall] = gDSFP.args; + expect(firstCall).to.eql([{ + changeState: false, + counter: 0, + }, { + state: -1, + }]); + expect(wrapper.state()).to.eql({ state: -1 }); + + wrapper.setProps({ counter: 1 }); + + expect(cDU).to.have.property('callCount', 1); + expect(gDSFP).to.have.property('callCount', 2); + const [, secondCall] = gDSFP.args; + expect(secondCall).to.eql([{ + changeState: false, + counter: 1, + }, { + state: -1, + }]); + expect(wrapper.state()).to.eql({ state: -1 }); + + return new Promise((resolve) => { + wrapper.setProps({ counter: 2 }, resolve); + }).then(() => { + expect(cDU).to.have.property('callCount', 2); + expect(gDSFP).to.have.property('callCount', 3); + const [, , thirdCall] = gDSFP.args; + expect(thirdCall).to.eql([{ + changeState: false, + counter: 2, + }, { + state: -1, + }]); + expect(wrapper.state()).to.eql({ state: -1 }); + }); + }); + + it('with a state changes, calls both methods with a sync and async setProps', () => { + const wrapper = mount(); + + expect(gDSFP).to.have.property('callCount', 1); + const [firstCall] = gDSFP.args; + expect(firstCall).to.eql([{ + changeState: true, + counter: 0, + }, { + state: -1, + }]); + expect(wrapper.state()).to.eql({ state: 0 }); + + wrapper.setProps({ counter: 1 }); + + expect(cDU).to.have.property('callCount', 1); + expect(gDSFP).to.have.property('callCount', 2); + const [, secondCall] = gDSFP.args; + expect(secondCall).to.eql([{ + changeState: true, + counter: 1, + }, { + state: 0, + }]); + expect(wrapper.state()).to.eql({ state: 10 }); + + return new Promise((resolve) => { + wrapper.setProps({ counter: 2 }, resolve); + }).then(() => { + expect(cDU).to.have.property('callCount', 2); + expect(gDSFP).to.have.property('callCount', 3); + const [, , thirdCall] = gDSFP.args; + expect(thirdCall).to.eql([{ + changeState: true, + counter: 2, + }, { + state: 10, + }]); + expect(wrapper.state()).to.eql({ state: 20 }); + }); + }); + }); }); describe('Own PureComponent implementation', () => { diff --git a/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx b/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx index ccf776123..b20b598f0 100644 --- a/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx +++ b/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx @@ -2505,8 +2505,8 @@ describe('shallow', () => { ]); }); - describe('setProps should not call componentDidUpdate twice', () => { - it('first test case', () => { + describe('setProps does not call componentDidUpdate twice', () => { + it('when setState is called in cWRP', () => { class Dummy extends React.Component { constructor(...args) { super(...args); @@ -7235,6 +7235,125 @@ describe('shallow', () => { wrapper.instance().setDeepDifferentState(); expect(updateSpy).to.have.property('callCount', 1); }); + + describeIf(is('>= 16.3'), 'setProps calls `componentDidUpdate` when `getDerivedStateFromProps` is defined', () => { + class DummyComp extends PureComponent { + constructor(...args) { + super(...args); + this.state = { state: -1 }; + } + + static getDerivedStateFromProps({ changeState, counter }) { + return changeState ? { state: counter * 10 } : null; + } + + componentDidUpdate() {} + + render() { + const { counter } = this.props; + const { state } = this.state; + return ( +

+ {counter} + {state} +

+ ); + } + } + + const cDU = sinon.spy(DummyComp.prototype, 'componentDidUpdate'); + const gDSFP = sinon.spy(DummyComp, 'getDerivedStateFromProps'); + + beforeEach(() => { // eslint-disable-line mocha/no-sibling-hooks + cDU.resetHistory(); + gDSFP.resetHistory(); + }); + + it('with no state changes, calls both methods with a sync and async setProps', () => { + const wrapper = shallow(); + + expect(gDSFP).to.have.property('callCount', 1); + const [firstCall] = gDSFP.args; + expect(firstCall).to.eql([{ + changeState: false, + counter: 0, + }, { + state: -1, + }]); + expect(wrapper.state()).to.eql({ state: -1 }); + + wrapper.setProps({ counter: 1 }); + + expect(cDU).to.have.property('callCount', 1); + expect(gDSFP).to.have.property('callCount', 2); + const [, secondCall] = gDSFP.args; + expect(secondCall).to.eql([{ + changeState: false, + counter: 1, + }, { + state: -1, + }]); + expect(wrapper.state()).to.eql({ state: -1 }); + + return new Promise((resolve) => { + wrapper.setProps({ counter: 2 }, resolve); + }).then(() => { + expect(cDU).to.have.property('callCount', 2); + expect(gDSFP).to.have.property('callCount', 3); + const [, , thirdCall] = gDSFP.args; + expect(thirdCall).to.eql([{ + changeState: false, + counter: 2, + }, { + state: -1, + }]); + expect(wrapper.state()).to.eql({ state: -1 }); + }); + }); + + it('with a state changes, calls both methods with a sync and async setProps', () => { + const wrapper = shallow(); + + expect(cDU).to.have.property('callCount', 0); + expect(gDSFP).to.have.property('callCount', 1); + const [firstCall] = gDSFP.args; + expect(firstCall).to.eql([{ + changeState: true, + counter: 0, + }, { + state: -1, + }]); + expect(wrapper.state()).to.eql({ state: 0 }); + + wrapper.setProps({ counter: 1 }); + + expect(cDU).to.have.property('callCount', 1); + expect(gDSFP).to.have.property('callCount', 2); + const [, secondCall] = gDSFP.args; + expect(secondCall).to.eql([{ + changeState: true, + counter: 1, + }, { + state: 0, + }]); + expect(wrapper.state()).to.eql({ state: 10 }); + + return new Promise((resolve) => { + wrapper.setProps({ counter: 2 }, resolve); + }).then(() => { + expect(cDU).to.have.property('callCount', 2); + expect(gDSFP).to.have.property('callCount', 3); + const [, , thirdCall] = gDSFP.args; + expect(thirdCall).to.eql([{ + changeState: true, + counter: 2, + }, { + state: 10, + }]); + expect(wrapper.state()).to.eql({ state: 20 }); + }); + }); + }); }); describe('Own PureComponent implementation', () => { diff --git a/packages/enzyme/src/ShallowWrapper.js b/packages/enzyme/src/ShallowWrapper.js index fca3632d6..7885e5d12 100644 --- a/packages/enzyme/src/ShallowWrapper.js +++ b/packages/enzyme/src/ShallowWrapper.js @@ -462,6 +462,7 @@ class ShallowWrapper { // this case, state will be undefined, but props/context will exist. const node = this[RENDERER].getNode(); const instance = node.instance || {}; + const type = node.type || {}; const { state } = instance; const prevProps = instance.props || this[UNRENDERED].props; const prevContext = instance.context || this[OPTIONS].context; @@ -522,7 +523,11 @@ class ShallowWrapper { if ( lifecycles.componentDidUpdate && typeof instance.componentDidUpdate === 'function' - && (!state || shallowEqual(state, this.instance().state)) + && ( + !state + || shallowEqual(state, this.instance().state) + || typeof type.getDerivedStateFromProps === 'function' + ) ) { instance.componentDidUpdate(prevProps, state, snapshot); }