Skip to content

Commit

Permalink
Merge pull request #2027 from peanutenthusiast/2020/cdu-on-setprops
Browse files Browse the repository at this point in the history
[Fix] `shallow`: ensure that if gDSFP exists, cDU is called.

Fixes #2020.
  • Loading branch information
ljharb committed Mar 15, 2019
2 parents 82b9da8 + 275b0b3 commit ebdd4e7
Show file tree
Hide file tree
Showing 3 changed files with 248 additions and 5 deletions.
123 changes: 121 additions & 2 deletions packages/enzyme-test-suite/test/ReactWrapper-spec.jsx
Expand Up @@ -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);
Expand Down Expand Up @@ -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 (
<p>
{counter}
{state}
</p>
);
}
}

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(<DummyComp changeState={false} counter={0} />);

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(<DummyComp changeState counter={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', () => {
Expand Down
123 changes: 121 additions & 2 deletions packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx
Expand Up @@ -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);
Expand Down Expand Up @@ -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 (
<p>
{counter}
{state}
</p>
);
}
}

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(<DummyComp changeState={false} counter={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 = shallow(<DummyComp changeState counter={0} />);

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', () => {
Expand Down
7 changes: 6 additions & 1 deletion packages/enzyme/src/ShallowWrapper.js
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down

0 comments on commit ebdd4e7

Please sign in to comment.