Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix to prevent updates with PureComponent #1786

Merged
merged 1 commit into from
Aug 22, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/enzyme-test-suite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"enzyme": "^3.3.0",
"enzyme-adapter-utils": "^1.5.0",
"jsdom": "^6.5.1",
"lodash.isequal": "^4.5.0",
"mocha-wrap": "^2.1.2",
"object.assign": "^4.1.0",
"object-inspect": "^1.6.0",
Expand Down
74 changes: 74 additions & 0 deletions packages/enzyme-test-suite/test/ReactWrapper-spec.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
import { expect } from 'chai';
import sinon from 'sinon';
import wrap from 'mocha-wrap';
import isEqual from 'lodash.isequal';
import {
mount,
render,
Expand All @@ -27,6 +28,7 @@ import {
createRef,
Fragment,
forwardRef,
PureComponent,
} from './_helpers/react-compat';
import {
describeWithDOM,
Expand Down Expand Up @@ -5128,6 +5130,78 @@ describeWithDOM('mount', () => {
});
});

describeIf(is('>= 15.3'), 'PureComponent', () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's also add corresponding tests for non-pure components that implement their own shouldComponentUpdate (or flesh out the existing one)

it('does not update when state and props did not change', () => {
class Foo extends PureComponent {
constructor(props) {
super(props);
this.state = {
foo: 'init',
};
}

componentDidUpdate() {}

render() {
return (
<div>
{this.state.foo}
</div>
);
}
}
const spy = sinon.spy(Foo.prototype, 'componentDidUpdate');
const wrapper = mount(<Foo id={1} />);
wrapper.setState({ foo: 'update' });
expect(spy).to.have.property('callCount', 1);
wrapper.setState({ foo: 'update' });
expect(spy).to.have.property('callCount', 1);

wrapper.setProps({ id: 2 });
expect(spy).to.have.property('callCount', 2);
wrapper.setProps({ id: 2 });
expect(spy).to.have.property('callCount', 2);
});
});

describe('Own PureComponent implementation', () => {
it('does not update when state and props did not change', () => {
class Foo extends React.Component {
constructor(props) {
super(props);
this.state = {
foo: 'init',
};
}

shouldComponentUpdate(nextProps, nextState) {
return !isEqual(this.props, nextProps) || !isEqual(this.state, nextState);
}

componentDidUpdate() {}

render() {
return (
<div>
{this.state.foo}
</div>
);
}
}
const spy = sinon.spy(Foo.prototype, 'componentDidUpdate');
const wrapper = mount(<Foo id={1} />);
wrapper.setState({ foo: 'update' });
expect(spy).to.have.property('callCount', 1);
wrapper.setState({ foo: 'update' });
expect(spy).to.have.property('callCount', 1);

wrapper.setProps({ id: 2 });
expect(spy).to.have.property('callCount', 2);
wrapper.setProps({ id: 2 });
expect(spy).to.have.property('callCount', 2);
});
});

describeIf(is('>= 16.3'), 'support getSnapshotBeforeUpdate', () => {
it('calls getSnapshotBeforeUpdate and pass snapshot to componentDidUpdate', () => {
const spy = sinon.spy();
Expand Down
74 changes: 74 additions & 0 deletions packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import { expect } from 'chai';
import sinon from 'sinon';
import wrap from 'mocha-wrap';
import isEqual from 'lodash.isequal';
import {
shallow,
render,
Expand All @@ -27,6 +28,7 @@ import {
createRef,
Fragment,
forwardRef,
PureComponent,
} from './_helpers/react-compat';
import {
describeIf,
Expand Down Expand Up @@ -5499,6 +5501,78 @@ describe('shallow', () => {
});
});

describeIf(is('>= 15.3'), 'PureComponent', () => {
it('does not update when state and props did not change', () => {
class Foo extends PureComponent {
constructor(props) {
super(props);
this.state = {
foo: 'init',
};
}

componentDidUpdate() {}

render() {
return (
<div>
{this.state.foo}
</div>
);
}
}
const spy = sinon.spy(Foo.prototype, 'componentDidUpdate');
const wrapper = shallow(<Foo id={1} />);
wrapper.setState({ foo: 'update' });
expect(spy).to.have.property('callCount', 1);
wrapper.setState({ foo: 'update' });
expect(spy).to.have.property('callCount', 1);

wrapper.setProps({ id: 2 });
expect(spy).to.have.property('callCount', 2);
wrapper.setProps({ id: 2 });
expect(spy).to.have.property('callCount', 2);
});
});

describe('Own PureComponent implementation', () => {
it('does not update when state and props did not change', () => {
class Foo extends React.Component {
constructor(props) {
super(props);
this.state = {
foo: 'init',
};
}

shouldComponentUpdate(nextProps, nextState) {
return !isEqual(this.props, nextProps) || !isEqual(this.state, nextState);
}

componentDidUpdate() {}

render() {
return (
<div>
{this.state.foo}
</div>
);
}
}
const spy = sinon.spy(Foo.prototype, 'componentDidUpdate');
const wrapper = mount(<Foo id={1} />);
wrapper.setState({ foo: 'update' });
expect(spy).to.have.property('callCount', 1);
wrapper.setState({ foo: 'update' });
expect(spy).to.have.property('callCount', 1);

wrapper.setProps({ id: 2 });
expect(spy).to.have.property('callCount', 2);
wrapper.setProps({ id: 2 });
expect(spy).to.have.property('callCount', 2);
});
});

describeIf(is('>= 16.3'), 'support getSnapshotBeforeUpdate', () => {
it('calls getSnapshotBeforeUpdate and pass snapshot to componentDidUpdate', () => {
const spy = sinon.spy();
Expand Down
8 changes: 8 additions & 0 deletions packages/enzyme-test-suite/test/_helpers/react-compat.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ let Fragment;
let StrictMode;
let AsyncMode;
let Profiler;
let PureComponent;

if (is('>=15.5 || ^16.0.0-alpha || ^16.3.0-alpha')) {
// eslint-disable-next-line import/no-extraneous-dependencies
Expand All @@ -37,6 +38,12 @@ if (is('^16.0.0-0 || ^16.3.0-0')) {
createPortal = null;
}

if (is('>=15.3')) {
({ PureComponent } = require('react'));
} else {
PureComponent = null;
}

if (is('^16.2.0-0')) {
({ Fragment } = require('react'));
} else {
Expand Down Expand Up @@ -78,4 +85,5 @@ export {
StrictMode,
AsyncMode,
Profiler,
PureComponent,
};
22 changes: 22 additions & 0 deletions packages/enzyme/src/ShallowWrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,14 @@ function privateSetNodes(wrapper, nodes) {
privateSet(wrapper, 'length', wrapper[NODES].length);
}

function pureComponentShouldComponentUpdate(prevProps, props, prevState, state) {
return !isEqual(prevProps, props) || !isEqual(prevState, state);
}

function isPureComponent(instance) {
return instance && instance.isPureReactComponent;
}

/**
* @class ShallowWrapper
*/
Expand Down Expand Up @@ -342,6 +350,13 @@ class ShallowWrapper {
&& typeof instance.shouldComponentUpdate === 'function'
) {
spy = spyMethod(instance, 'shouldComponentUpdate');
} else if (isPureComponent(instance)) {
shouldRender = pureComponentShouldComponentUpdate(
prevProps,
props,
state,
instance.state,
);
}
if (props) this[UNRENDERED] = cloneElement(adapter, this[UNRENDERED], props);
this[RENDERER].render(this[UNRENDERED], nextContext);
Expand Down Expand Up @@ -472,6 +487,13 @@ class ShallowWrapper {
&& typeof instance.shouldComponentUpdate === 'function'
) {
spy = spyMethod(instance, 'shouldComponentUpdate');
} else if (isPureComponent(instance)) {
shouldRender = pureComponentShouldComponentUpdate(
prevProps,
instance.props,
prevState,
statePayload,
);
}
// We don't pass the setState callback here
// to guarantee to call the callback after finishing the render
Expand Down