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

Conversation

alecrobins
Copy link
Contributor

Resolves #509

Due to the asynchronous nature of setState, we can't assume a wrapper.setState or wrapper.setProps (which uses setState in the setChildProps call) will act synchronously.

This PR adds callback functions to the stateState and setProps functions for full DOM rendering and setState for shallow rendering.

Example:

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' });
});

if (this.root !== this) {
throw new Error('ReactWrapper::setState() can only be called on the root');
}
this.instance().setState(state);
this.instance().setState(state, cb);
Copy link
Member

Choose a reason for hiding this comment

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

does react handle throwing if cb isn't a function?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

Awesome - it would still be good to add tests for that behavior, in case a future version of react changes it

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yea sounds good. I'll add a test.

* @returns {ReactWrapper}
*/
setState(state) {
setState(state, cb) {
Copy link
Member

Choose a reason for hiding this comment

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

since this param is optional, it should default to undefined so it doesn't count towards the function's length (same throughout)

expect(wrapper.find('.foo').length).to.equal(1);
wrapper.setProps({ id: 'bar', foo: 'bla' }, () => {
expect(wrapper.find('.bar').length).to.equal(1);
});
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.

@@ -32,9 +32,9 @@ export default function createWrapperComponent(node, options = {}) {
};
},

setChildProps(newProps) {
setChildProps(newProps, cb = undefined) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Wont cb be undefined by default if its not passed in?

Copy link
Member

Choose a reason for hiding this comment

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

Yes, but defaulting it explicitly makes it not count towards the function's length.

Copy link
Collaborator

Choose a reason for hiding this comment

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

What are the advantages of doing that here? Specifically for setState which is meant to shadow the React API, which does not make this explicit.

Copy link
Member

Choose a reason for hiding this comment

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

Function lengths are intended to only count required arguments - the language follows this for the most part. You're right that React doesn't make this explicit, but I'd prefer to correct their mistake for our API.

@alecrobins alecrobins force-pushed the fix-509 branch 2 times, most recently from 4ed9c95 to 7b94db8 Compare October 3, 2016 14:37
@aronwoost
Copy link

Awesome work! Well, should it be shipped?

Copy link
Member

@ljharb ljharb left a comment

Choose a reason for hiding this comment

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

This LGTM - it would still be preferred, imo, to have a test that guarantees that the setState callback is async (we don't have that right now) - but I don't think that needs to block.

@alecrobins
Copy link
Contributor Author

@ljharb I added a test where the callback is called asynchronously ( 7b94db8#diff-2f849fa739461853c70ddd193891074fR976 )

@ljharb
Copy link
Member

ljharb commented Oct 7, 2016

@alecrobins awesome, thanks! Would you mind rebasing so as to remove all merge commits, and then this is good to go.

@lencioni
Copy link
Contributor

@alecrobins can you rebase this again for us when you have a moment? Thanks!

@@ -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[, cb])](/docs/api/ShallowWrapper/setState.md)
Copy link
Contributor

@lencioni lencioni Oct 11, 2016

Choose a reason for hiding this comment

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

It would be nice to use callback for this variable (here and elsewhere), for maximum clarity.

@alecrobins
Copy link
Contributor Author

@lencioni Just rebased and changed the variable name to callback.

@lencioni
Copy link
Contributor

LGTM

@lencioni lencioni merged commit c1e03cc into enzymejs:master Oct 11, 2016
@kentor
Copy link

kentor commented Nov 16, 2016

hi! when will this get released?

Looking forward to do this in tests:

flushSetState = (wrapper) => new Promise(resolve => wrapper.setState({}, resolve));
...
await flushSetState(wrapper);
expect(wrapper.state(...))

@koba04
Copy link
Contributor

koba04 commented Nov 17, 2016

@kentor This has been released in 2.5.0.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants