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

Fixes #509: pass callback on setState and setProps #617

Merged
merged 7 commits into from
Oct 11, 2016
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
6 changes: 3 additions & 3 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
* [render()](/docs/api/ShallowWrapper/render.md)
* [setContext(context)](/docs/api/ShallowWrapper/setContext.md)
* [setProps(nextProps)](/docs/api/ShallowWrapper/setProps.md)
* [setState(nextState)](/docs/api/ShallowWrapper/setState.md)
* [setState(nextState[, callback])](/docs/api/ShallowWrapper/setState.md)
* [shallow([options])](/docs/api/ShallowWrapper/shallow.md)
* [simulate(event[, data])](/docs/api/ShallowWrapper/simulate.md)
* [some(selector)](/docs/api/ShallowWrapper/some.md)
Expand Down Expand Up @@ -110,8 +110,8 @@
* [ref(refName)](/docs/api/ReactWrapper/ref.md)
* [render()](/docs/api/ReactWrapper/render.md)
* [setContext(context)](/docs/api/ReactWrapper/setContext.md)
* [setProps(nextProps)](/docs/api/ReactWrapper/setProps.md)
* [setState(nextState)](/docs/api/ReactWrapper/setState.md)
* [setProps(nextProps[, callback])](/docs/api/ReactWrapper/setProps.md)
* [setState(nextState[, callback])](/docs/api/ReactWrapper/setState.md)
* [simulate(event[, data])](/docs/api/ReactWrapper/simulate.md)
* [some(selector)](/docs/api/ReactWrapper/some.md)
* [someWhere(predicate)](/docs/api/ReactWrapper/someWhere.md)
Expand Down
4 changes: 2 additions & 2 deletions docs/api/ReactWrapper/setProps.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# `.setProps(props) => Self`
# `.setProps(props[, callback]) => Self`

A method that sets the props of the root component, and re-renders. Useful for when you are
wanting to test how the component behaves over time with changing props. Calling this, for
Expand All @@ -13,7 +13,7 @@ NOTE: can only be called on a wrapper instance that is also the root instance.
#### Arguments

1. `props` (`Object`): An object containing new props to merge in with the current state

2. `callback` (`Function` [optional]): If provided, the callback function will be executed once setProps has completed


#### Returns
Expand Down
4 changes: 2 additions & 2 deletions docs/api/ReactWrapper/setState.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# `.setState(state) => Self`
# `.setState(state[, callback]) => Self`

A method to invoke `setState()` on the root component instance similar to how you might in the
definition of the component, and re-renders. This method is useful for testing your component
Expand All @@ -12,7 +12,7 @@ NOTE: can only be called on a wrapper instance that is also the root instance.
#### Arguments

1. `state` (`Object`): An object containing new state to merge in with the current state

2. `callback` (`Function` [optional]): If provided, the callback function will be executed once setState has completed


#### Returns
Expand Down
4 changes: 2 additions & 2 deletions docs/api/ShallowWrapper/setState.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# `.setState(state) => Self`
# `.setState(state[, callback]) => Self`

A method to invoke `setState()` on the root component instance similar to how you might in the
definition of the component, and re-renders. This method is useful for testing your component
Expand All @@ -12,7 +12,7 @@ NOTE: can only be called on a wrapper instance that is also the root instance.
#### Arguments

1. `state` (`Object`): An object containing new state to merge in with the current state

2. `callback` (`Function` [optional]): If provided, the callback function will be executed once setState has completed


#### Returns
Expand Down
10 changes: 6 additions & 4 deletions src/ReactWrapper.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -205,13 +205,14 @@ class ReactWrapper {
* NOTE: can only be called on a wrapper instance that is also the root instance.
*
* @param {Object} props object
* @param {Function} cb - callback function
* @returns {ReactWrapper}
*/
setProps(props) {
setProps(props, callback = undefined) {
if (this.root !== this) {
throw new Error('ReactWrapper::setProps() can only be called on the root');
}
this.component.setChildProps(props);
this.component.setChildProps(props, callback);
return this;
}

Expand All @@ -225,13 +226,14 @@ class ReactWrapper {
* NOTE: can only be called on a wrapper instance that is also the root instance.
*
* @param {Object} state to merge
* @param {Function} cb - callback function
* @returns {ReactWrapper}
*/
setState(state) {
setState(state, callback = undefined) {
if (this.root !== this) {
throw new Error('ReactWrapper::setState() can only be called on the root');
}
this.instance().setState(state);
this.instance().setState(state, callback);
return this;
}

Expand Down
4 changes: 2 additions & 2 deletions src/ReactWrapperComponent.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ export default function createWrapperComponent(node, options = {}) {
};
},

setChildProps(newProps) {
setChildProps(newProps, callback = undefined) {
const props = objectAssign({}, this.state.props, newProps);
this.setState({ props });
this.setState({ props }, callback);
},

setChildContext(context) {
Expand Down
5 changes: 3 additions & 2 deletions src/ShallowWrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -245,9 +245,10 @@ class ShallowWrapper {
* NOTE: can only be called on a wrapper instance that is also the root instance.
*
* @param {Object} state to merge
* @param {Function} cb - callback function
* @returns {ShallowWrapper}
*/
setState(state) {
setState(state, callback = undefined) {
if (this.root !== this) {
throw new Error('ShallowWrapper::setState() can only be called on the root');
}
Expand All @@ -256,7 +257,7 @@ class ShallowWrapper {
}
this.single('setState', () => {
withSetStateAllowed(() => {
this.instance().setState(state);
this.instance().setState(state, callback);
this.update();
});
});
Expand Down
83 changes: 81 additions & 2 deletions test/ReactWrapper-spec.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import React from 'react';
import { expect } from 'chai';
import sinon from 'sinon';
import { batchedUpdates } from '../src/react-compat';

import {
describeWithDOM,
Expand Down Expand Up @@ -857,7 +858,7 @@ describeWithDOM('mount', () => {

});

describe('.setProps(newProps)', () => {
describe('.setProps(newProps[, callback])', () => {
it('should set props for a component multiple times', () => {
class Foo extends React.Component {
render() {
Expand Down Expand Up @@ -960,6 +961,27 @@ describeWithDOM('mount', () => {
expect(setInvalidProps).to.throw(TypeError, similarException.message);
});

it('should call the callback when setProps has completed', () => {
class Foo extends React.Component {
render() {
return (
<div className={this.props.id}>
{this.props.id}
</div>
);
}
}
const wrapper = mount(<Foo id="foo" />);
expect(wrapper.find('.foo').length).to.equal(1);

batchedUpdates(() => {
wrapper.setProps({ id: 'bar', foo: 'bla' }, () => {
expect(wrapper.find('.bar').length).to.equal(1);
});
expect(wrapper.find('.bar').length).to.equal(0);
});
Copy link
Member

Choose a reason for hiding this comment

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

is there a way we could add a sync assertion that the length is NOT equal to 1, to prove that the callback is doing its job?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is difficult to force setState to act asynchronously. In most cases it does act synchronously it's just not a guarantee.

Copy link
Member

Choose a reason for hiding this comment

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

hm - the motivation for adding this callback is those cases where it does act async. there must be some likely usage pattern that forces it?

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure usage patterns that setState does act async in testing. but I think you can use batchedUpdates like this.

import { batchedUpdates } from '../src/react-compat';

batchedUpdates(() => {
  wrapper.setProps({ id: 'bar', foo: 'bla' }, () => {
    expect(wrapper.find('.bar').length).to.equal(1);
  });
  expect(wrapper.find('.bar').length).to.equal(0);
});

Copy link
Contributor Author

Choose a reason for hiding this comment

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

thanks @koba04. Using batchedUpdates worked.

});

describeIf(!REACT013, 'stateless function components', () => {
it('should set props for a component multiple times', () => {
const Foo = props => (
Expand Down Expand Up @@ -1268,7 +1290,7 @@ describeWithDOM('mount', () => {
});
});

describe('.setState(newState)', () => {
describe('.setState(newState[, callback])', () => {
it('should set the state of the root node', () => {
class Foo extends React.Component {
constructor(props) {
Expand Down Expand Up @@ -1305,6 +1327,63 @@ describeWithDOM('mount', () => {
const wrapper = mount(<MySharona />);
expect(wrapper.find('div').text()).to.equal('a');
});

it('should call the callback when setState has completed', () => {
class Foo extends React.Component {
constructor(props) {
super(props);
this.state = { id: 'foo' };
}
render() {
return (
<div className={this.state.id} />
);
}
}
const wrapper = mount(<Foo />);
expect(wrapper.state()).to.eql({ id: 'foo' });
wrapper.setState({ id: 'bar' }, () => {
expect(wrapper.state()).to.eql({ id: 'bar' });
});
});

it('should throw error when cb is not a function', () => {
class Foo extends React.Component {
constructor(props) {
super(props);
this.state = { id: 'foo' };
}
render() {
return (
<div className={this.state.id} />
);
}
}
const wrapper = mount(<Foo />);
expect(wrapper.state()).to.eql({ id: 'foo' });
expect(() => wrapper.setState({ id: 'bar' }, 1)).to.throw(Error);
});

itIf(REACT15, 'should throw error when cb is not a function', () => {
class Foo extends React.Component {
constructor(props) {
super(props);
this.state = { id: 'foo' };
}
render() {
return (
<div className={this.state.id} />
);
}
}
const wrapper = mount(<Foo />);
expect(wrapper.state()).to.eql({ id: 'foo' });
expect(() => wrapper.setState({ id: 'bar' }, 1)).to.throw(
Error,
'setState(...): Expected the last optional `callback` argument ' +
'to be a function. Instead received: number.'
);
});
});

describe('.is(selector)', () => {
Expand Down
21 changes: 20 additions & 1 deletion test/ShallowWrapper-spec.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -1078,7 +1078,7 @@ describe('shallow', () => {
});
});

describe('.setState(newState)', () => {
describe('.setState(newState[, callback])', () => {
it('should set the state of the root node', () => {
class Foo extends React.Component {
constructor(props) {
Expand All @@ -1097,6 +1097,25 @@ describe('shallow', () => {
expect(wrapper.find('.bar').length).to.equal(1);
});

it('should call the callback when setState has completed', () => {
class Foo extends React.Component {
constructor(props) {
super(props);
this.state = { id: 'foo' };
}
render() {
return (
<div className={this.state.id} />
);
}
}
const wrapper = shallow(<Foo />);
expect(wrapper.state()).to.eql({ id: 'foo' });
wrapper.setState({ id: 'bar' }, () => {
expect(wrapper.state()).to.eql({ id: 'bar' });
});
});

describeIf(!REACT013, 'stateless function components', () => {
it('should throw when trying to access state', () => {
const Foo = () => (
Expand Down