Skip to content

Commit

Permalink
[Fix] mount/shallow: be stricter on the wrapper’s setState/setPro…
Browse files Browse the repository at this point in the history
…ps callback

However, be looser on the shallow setState stub - closes #1771.
  • Loading branch information
ljharb committed Aug 20, 2018
1 parent 89b39b6 commit ff11d22
Show file tree
Hide file tree
Showing 4 changed files with 134 additions and 14 deletions.
61 changes: 60 additions & 1 deletion packages/enzyme-test-suite/test/ReactWrapper-spec.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -1699,6 +1699,22 @@ describeWithDOM('mount', () => {
});

describe('.setProps(newProps[, callback])', () => {
it('throws on a non-function callback', () => {
class Foo extends React.Component {
render() {
return null;
}
}
const wrapper = mount(<Foo />);

expect(() => wrapper.setProps({}, undefined)).to.throw();
expect(() => wrapper.setProps({}, null)).to.throw();
expect(() => wrapper.setProps({}, false)).to.throw();
expect(() => wrapper.setProps({}, true)).to.throw();
expect(() => wrapper.setProps({}, [])).to.throw();
expect(() => wrapper.setProps({}, {})).to.throw();
});

it('should set props for a component multiple times', () => {
class Foo extends React.Component {
render() {
Expand Down Expand Up @@ -2361,6 +2377,22 @@ describeWithDOM('mount', () => {
});

describe('.setState(newState[, callback])', () => {
it('throws on a non-function callback', () => {
class Foo extends React.Component {
render() {
return null;
}
}
const wrapper = mount(<Foo />);

expect(() => wrapper.setState({}, undefined)).to.throw();
expect(() => wrapper.setState({}, null)).to.throw();
expect(() => wrapper.setState({}, false)).to.throw();
expect(() => wrapper.setState({}, true)).to.throw();
expect(() => wrapper.setState({}, [])).to.throw();
expect(() => wrapper.setState({}, {})).to.throw();
});

it('should set the state of the root node', () => {
class Foo extends React.Component {
constructor(props) {
Expand Down Expand Up @@ -2456,13 +2488,17 @@ describeWithDOM('mount', () => {
});
});

it('should throw error when cb is not a function', () => {
it('throws an error when cb is not a function', () => {
class Foo extends React.Component {
constructor(props) {
super(props);
this.state = { id: 'foo' };
}

setBadState() {
this.setState({}, 1);
}

render() {
return (
<div className={this.state.id} />
Expand All @@ -2472,6 +2508,29 @@ describeWithDOM('mount', () => {
const wrapper = mount(<Foo />);
expect(wrapper.state()).to.eql({ id: 'foo' });
expect(() => wrapper.setState({ id: 'bar' }, 1)).to.throw(Error);
expect(() => wrapper.instance().setBadState()).to.throw(Error);
});

it('does not throw with a null/undefined callback', () => {
class Foo extends React.Component {
constructor() {
super();

this.state = {};
}

setStateWithNullishCallback() {
this.setState({}, null);
this.setState({}, undefined);
}

render() {
return null;
}
}

const wrapper = mount(<Foo />);
expect(() => wrapper.instance().setStateWithNullishCallback()).not.to.throw();
});

it('should preserve the receiver', () => {
Expand Down
61 changes: 60 additions & 1 deletion packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -1514,6 +1514,22 @@ describe('shallow', () => {
});

describe('.setProps(newProps)', () => {
it('throws on a non-function callback', () => {
class Foo extends React.Component {
render() {
return null;
}
}
const wrapper = shallow(<Foo />);

expect(() => wrapper.setProps({}, undefined)).to.throw();
expect(() => wrapper.setProps({}, null)).to.throw();
expect(() => wrapper.setProps({}, false)).to.throw();
expect(() => wrapper.setProps({}, true)).to.throw();
expect(() => wrapper.setProps({}, [])).to.throw();
expect(() => wrapper.setProps({}, {})).to.throw();
});

it('should set props for a component multiple times', () => {
class Foo extends React.Component {
render() {
Expand Down Expand Up @@ -2309,6 +2325,22 @@ describe('shallow', () => {
});

describe('.setState(newState[, callback])', () => {
it('throws on a non-function callback', () => {
class Foo extends React.Component {
render() {
return null;
}
}
const wrapper = shallow(<Foo />);

expect(() => wrapper.setState({}, undefined)).to.throw();
expect(() => wrapper.setState({}, null)).to.throw();
expect(() => wrapper.setState({}, false)).to.throw();
expect(() => wrapper.setState({}, true)).to.throw();
expect(() => wrapper.setState({}, [])).to.throw();
expect(() => wrapper.setState({}, {})).to.throw();
});

it('should set the state of the root node', () => {
class Foo extends React.Component {
constructor(props) {
Expand Down Expand Up @@ -2402,13 +2434,17 @@ describe('shallow', () => {
});
});

it('should throw error when cb is not a function', () => {
it('throws an error when cb is not a function', () => {
class Foo extends React.Component {
constructor(props) {
super(props);
this.state = { id: 'foo' };
}

setBadState() {
this.setState({}, 1);
}

render() {
return (
<div className={this.state.id} />
Expand All @@ -2418,6 +2454,29 @@ describe('shallow', () => {
const wrapper = shallow(<Foo />);
expect(wrapper.state()).to.eql({ id: 'foo' });
expect(() => wrapper.setState({ id: 'bar' }, 1)).to.throw(Error);
expect(() => wrapper.instance().setBadState()).to.throw(Error);
});

it('does not throw with a null/undefined callback', () => {
class Foo extends React.Component {
constructor() {
super();

this.state = {};
}

setStateWithNullishCallback() {
this.setState({}, null);
this.setState({}, undefined);
}

render() {
return null;
}
}

const wrapper = shallow(<Foo />);
expect(() => wrapper.instance().setStateWithNullishCallback()).not.to.throw();
});

it('should preserve the receiver', () => {
Expand Down
10 changes: 5 additions & 5 deletions packages/enzyme/src/ReactWrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ import {

import { buildPredicate, reduceTreesBySelector } from './selectors';

const noop = () => {};

const NODE = sym('__node__');
const NODES = sym('__nodes__');
const RENDERER = sym('__renderer__');
Expand Down Expand Up @@ -273,18 +271,20 @@ class ReactWrapper {
* @param {Function} cb - callback function
* @returns {ReactWrapper}
*/
setProps(props, callback = noop) {
setProps(props, callback = undefined) {
if (this[ROOT] !== this) {
throw new Error('ReactWrapper::setProps() can only be called on the root');
}
if (typeof callback !== 'function') {
if (arguments.length > 1 && typeof callback !== 'function') {
throw new TypeError('ReactWrapper::setProps() expects a function as its second argument');
}
const adapter = getAdapter(this[OPTIONS]);
this[UNRENDERED] = cloneElement(adapter, this[UNRENDERED], props);
this[RENDERER].render(this[UNRENDERED], null, () => {
this.update();
callback();
if (callback) {
callback();
}
});
return this;
}
Expand Down
16 changes: 9 additions & 7 deletions packages/enzyme/src/ShallowWrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,6 @@ import {
} from './RSTTraversal';
import { buildPredicate, reduceTreesBySelector } from './selectors';

const noop = () => {};

const NODE = sym('__node__');
const NODES = sym('__nodes__');
const RENDERER = sym('__renderer__');
Expand Down Expand Up @@ -187,7 +185,9 @@ class ShallowWrapper {
// Ensure to call componentDidUpdate when instance.setState is called
if (lifecycles.componentDidUpdate.onSetState && !instance[SET_STATE]) {
privateSet(instance, SET_STATE, instance.setState);
instance.setState = (...args) => this.setState(...args);
instance.setState = (updater, callback = undefined) => this.setState(
...(callback == null ? [updater] : [updater, callback]),
);
}

if (typeof instance.componentDidMount === 'function') {
Expand Down Expand Up @@ -398,15 +398,17 @@ class ShallowWrapper {
* @param {Function} cb - callback function
* @returns {ShallowWrapper}
*/
setProps(props, callback = noop) {
setProps(props, callback = undefined) {
if (this[ROOT] !== this) {
throw new Error('ShallowWrapper::setProps() can only be called on the root');
}
if (typeof callback !== 'function') {
throw new TypeError('ShallowWrapper::setProps() expects a function as its second argument');
if (arguments.length > 1 && typeof callback !== 'function') {
throw new TypeError('ReactWrapper::setProps() expects a function as its second argument');
}
this.rerender(props);
callback();
if (callback) {
callback();
}
return this;
}

Expand Down

0 comments on commit ff11d22

Please sign in to comment.