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 call a setState callback after finishing the the render #1453

Merged
merged 6 commits into from
Jan 6, 2018

Conversation

koba04
Copy link
Contributor

@koba04 koba04 commented Dec 26, 2017

This PR is to fix #1451.
Currently, a setState callback is called before finishing the render because enzyme is passing the callback into instance.setState.
So I've fixed it to call the callback after this.update()

@@ -400,6 +402,10 @@ class ShallowWrapper {
}
}
this.update();
// call the setState callback
if (callback) {
callback.call(instance);
Copy link
Member

Choose a reason for hiding this comment

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

are we sure that React calls the callback with zero arguments, and the instance as the receiver?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

are we sure that React calls the callback with zero arguments, and the instance as the receiver?

I think so.

Copy link
Member

Choose a reason for hiding this comment

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

That implementation doesn't show where update.callback is assigned; but your demo is convincing (for the latest version of React).

However, in React ^15.4, it seems to get one argument set to undefined in your demo (whereas in <= 15.3, and >= 16, it seems to get zero arguments). We should maintain that behavior via adapters.

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! I'll work on to support < 15.3

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've tested for arguments of setState callback.

setState({foo: 'bar'}, function(...args) {
  console.log(args);
});
  • 0.13.3 ... []
  • 0.14.9 ... []
  • 15.4.2 ... [ undefined ]
  • 15.6.2 ... [ undefined ]
  • 16.2.0 ... []

The arguments seem to be [undefined] only in v15.

Copy link
Member

Choose a reason for hiding this comment

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

It's more than just painful; enzyme itself must not know about React at all, given that non-React adapters need to be able to work.

In other words, this functionality needs to be something that every adapter implements, and enzyme merely calls into.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

enzyme itself must not know about React at all, given that non-React adapters need to be able to work.

I agree with you. But calling setState callback without argument is depending on current React implementation.
Spying setState callback and using the result later is to reproduce this functionality so I think it can be applied any adapters which are including non-React adapters.

instance.setState(state, spy);
:
if (callback) {
  // replay setState callbacks
  spy.getCalls().forEach((spyCall) => {
    const context = spyCall.thisValue;
    const args = spyCall.args;
    callback.apply(context, args);
  });
}

Copy link
Member

Choose a reason for hiding this comment

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

Your spy suggestion is interesting, but I was suggesting something like adapter.invokeSetStateCallback(instance, callback) - I'm not sure which is better.

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, I got it.
Are you ok to implement a default behavior into EnzymeAdapter? If not, it will be a breaking change.

Copy link
Member

Choose a reason for hiding this comment

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

Yes, I think that would make sense.

@koba04 koba04 force-pushed the call-setstate-callback-after-update branch from 279cbf6 to d3c6cf1 Compare January 4, 2018 12:17
@@ -49,4 +49,4 @@
"eslint-plugin-jsx-a11y": "^6.0.3",
"eslint-plugin-react": "^7.5.1"
}
}
}
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 revert this file; every file should always have a trailing newline

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The diff is added again😉 I'm not digging into that but I guess it is added by a script to run tests.

Copy link
Member

Choose a reason for hiding this comment

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

ah, weird. maybe npm itself. not a big deal.

@@ -385,7 +385,9 @@ class ShallowWrapper {
return shouldRender;
};
}
instance.setState(state, callback);
// We don't pass the setState callback here
// to guarantee to call the callback after finishing the render
Copy link
Member

Choose a reason for hiding this comment

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

can we add a test case for this? or does expect(wrapper.find('div').prop('className')).to.eql('bar'); cover that

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, expect(wrapper.find('div').prop('className')).to.eql('bar'); is a test for this.

@koba04 koba04 force-pushed the call-setstate-callback-after-update branch from 40540e4 to 4afbef7 Compare January 5, 2018 15:10
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.

LGTM, just some questions


// The shallow renderer in react 16 does not yet support batched updates. When it does,
// we should be able to go un-skip all of the tests that are skipped with this flag.
const BATCHING = !REACT16;


// some React versions pass undefined as an argument of setState callback.
const CALLING_SETSTATE_CALLBACK_WITH_UNDEFINED = REACT154 || REACT155 || REACT156;
Copy link
Member

Choose a reason for hiding this comment

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

could this be made a bit more robust by changing it to !REACT15 && !REACT150 && !REACT151 && !REACT152 && !REACT153, since those are the only 4 versions of 15 that will ever not have this behavior?

if (MINOR_VERSION >= 4) {
callback.call(instance, undefined);
} else {
callback.call(instance);
Copy link
Member

Choose a reason for hiding this comment

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

maybe this could do super.invokeSetStateCallback(instance, callback)?

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.

Thanks!

@koba04
Copy link
Contributor Author

koba04 commented Jan 6, 2018

Thank you for your review!

@ljharb
Copy link
Member

ljharb commented Jan 6, 2018

Tests seem to be failing in React 15, both locally and in travis. I'm going to see what I can do to figure it out now.

@ljharb ljharb force-pushed the call-setstate-callback-after-update branch 4 times, most recently from 6ea171e to 751b1f7 Compare January 6, 2018 04:57
@ljharb ljharb merged commit bf14288 into enzymejs:master Jan 6, 2018
'^15.4',
() => { callback.call(instance, undefined); },
() => { super.invokeSetStateCallback(instance, callback); },
);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

ifReact returns a function, not calling it so you have to call a function which is returned from ifReact.

https://github.com/airbnb/enzyme/blob/master/packages/enzyme-adapter-react-helper/src/ifReact.js#L7

Copy link
Member

Choose a reason for hiding this comment

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

lol, oops. shouldn't tests have failed?

Copy link
Member

Choose a reason for hiding this comment

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

Will fix in a followup.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think so we should add a test to guarantee to call a setState callback.
Currently, we don't have a test for it.


// The shallow renderer in react 16 does not yet support batched updates. When it does,
// we should be able to go un-skip all of the tests that are skipped with this flag.
const BATCHING = !REACT16;

// some React versions pass undefined as an argument of setState callback.
const CALLING_SETSTATE_CALLBACK_WITH_UNDEFINED = REACT15 && !REACT150_4;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I guess this condition doesn't cover a case that React v15.4 calls setState callback with undefined.

But all tests are passing in my environment. The reason is ifReact is checking with React v16.2 because enzyme-adapter-react-helper has React v16.2 into its node_modules.
I'm not sure why enzyme-adapter-react-helper has React v16.2 into its node_modules.

Copy link
Member

Choose a reason for hiding this comment

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

it has a peer dep/dev dep on react >= 0.13, so that'd install the latest, but i'm not sure why that would do it. let me try something tho

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! I'll dig into it later.

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.

since it's a peer dep, it also needs to be a dev dep.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Other packages don't have react as a devDeps. Why does only this package have it?

Copy link
Member

Choose a reason for hiding this comment

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

That's a bug leftover from the v3 migration; they all should - every peer dep in every npm package anywhere must always have the identical peer dep string as a dep or a dev dep.

Copy link
Member

Choose a reason for hiding this comment

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

I've almost got a fix PR with a regression test; i'll put it up shortly.

Copy link
Member

Choose a reason for hiding this comment

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

@ljharb ljharb mentioned this pull request Jan 6, 2018
ljharb added a commit that referenced this pull request Jul 9, 2018
 - [fix] actually invoke the setState callback (#1465, #1453)
 - [fix] add missing support for animation events (#1569)
 - [fix] call ref for a root element (#1541)
 - [fix] Allow empty strings as key props (#1524)
 - [fix] correct the adapter class name
 - [fix] `mount`: make sure it works with native arrow functions (#1667)
 - [refactor]: add “lifecycles” adapter option (#1696)
 - [refactor] use `react-is` package
 - [deps] remove unneeded `lodash` dep; update `prop-types`, `enzyme-adapter-utils`, `object.assign`
 - [dev deps] update `babel-cli`, `babel-core`, `babel-plugin-transform-replace-object-assign`, `babel-preset-airbnb`, `chai`, `coveralls`, `eslint`, `eslint-config-airbnb`, `eslint-plugin-import`, `eslint-plugin-jsx-a11y`, `eslint-plugin-react`, `lerna`, `json-loader`, `karma-firefox-launcher`, `prop-types`, `lerna`, `rimraf`, `webpack`
ljharb added a commit that referenced this pull request Jul 9, 2018
 - [fix] actually invoke the setState callback (#1465, #1453)
 - [fix] add missing support for animation events (#1569)
 - [fix] call ref for a root element (#1541)
 - [fix] Allow empty strings as key props (#1524)
 - [fix] correct the adapter class name
 - [fix] `mount`: make sure it works with native arrow functions (#1667)
 - [refactor]: add “lifecycles” adapter option (#1696)
 - [refactor] use `react-is` package
 - [deps] remove unneeded `lodash` dep; update `prop-types`, `enzyme-adapter-utils`, `object.assign`
 - [dev deps] update `babel-cli`, `babel-core`, `babel-plugin-transform-replace-object-assign`, `babel-preset-airbnb`, `chai`, `coveralls`, `eslint`, `eslint-config-airbnb`, `eslint-plugin-import`, `eslint-plugin-jsx-a11y`, `eslint-plugin-react`, `lerna`, `json-loader`, `karma-firefox-launcher`, `prop-types`, `lerna`, `rimraf`, `webpack`
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.

setState on shallowWrapper needs explicit update
2 participants