-
-
Notifications
You must be signed in to change notification settings - Fork 2k
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
Add ShallowWrapper#invoke() to invoke event handlers and return the handlers value #945
Conversation
src/ShallowWrapper.js
Outdated
}); | ||
} | ||
return this; | ||
return null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this should return undefined
rather than null
.
src/ShallowWrapper.js
Outdated
performBatchedUpdates(this, () => { | ||
handler(...args); | ||
response = handler(...args); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if it would be better to return the equivalent of Promise.try(() => handler(...args))
, to cleanly encapsulate any exceptions - the only downside being that you couldn't get at the result synchronously. Are there use cases for a synchronous non-Promise return value?
src/Utils.js
Outdated
@@ -218,13 +218,14 @@ export function withSetStateAllowed(fn) { | |||
cleanup = true; | |||
global.document = {}; | |||
} | |||
fn(); | |||
const response = fn(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return try { fn(); } finally {
if (cleanup) {
// This works around a bug in node/jest in that developers aren't able to
// delete things from global when running in a node vm.
global.document = undefined;
delete global.document;
}
};
(this is arguably what it should have been doing all along, in case fn
throws)
Linking to two previous PRs doing similar things #867 #898 As per the discussion on the other two PRs, events don't have a single result, because they fire zero, one or more event listeners. (so if you start exposing the return value of a handler, which of the several handlers do you choose?) The fact that our current implementation of As such, this gets a 👎 from me, what do the other maintainers think? cc @ljharb |
In other words, if we get this in we're accepting that the |
@nfcampos In the context of a React component, there is only a single event with a single return value, so that shouldn't be an issue. But yes, this would deviate mount and shallow. Perhaps we can add a new method and keep existing functionality, like |
@milesj events in browsers bubble through the tree of rendered elements, ie. in
if you click on the inner div both onClick handlers fire. |
Correct, and that's a single event. That's what I'm getting at with shallow. |
yes, it's a single event, invoking two event handlers, so how do you choose which one to return the return value for? |
Shallow doesn't invoke 2 event handlers -- you literally just said this above. It executes a prop of the same name, for the element that is currently found. That is 1 event with 1 return value. We're talking in the context of shallow, not mount. |
yes, and as I said above, that's something we've been thinking about changing for quite some time (see open PR at https://github.com/airbnb/enzyme/pull/368/files#diff-cc80d1de64f4765743cbe5179e51ff36, or in fact just look at @lelandrichardson's comment right above one of the lines you changed https://github.com/airbnb/enzyme/pull/945/files#diff-00a0c52d2a0eac5ec0025c1d32767a95R624). So, re-iterating, if we do bring your change in, we'll have to abandon our previous effort at introducing event propagation for I'm not strictly against either option, but I do believe we should have that trade-off in mind while considering either option. |
Which is why I suggested an additional method that doesn't use propagation. I'd wager that triggering a prop function on a React component has much higher usage than triggering an actual event. |
Yup. Would just need to return the value, or be wrapped in a promise, or else we'd still have the same next tick problems. |
do you want to cherry-pick the relevant commits out of that PR and change the return value? |
I can look into it since that PR is currently blocked. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we add invoke
to ReactWrapper
as well?
@@ -0,0 +1,18 @@ | |||
# `.isEmptyRender() => Boolean` | |||
|
|||
Returns whether or not the current component returns a falsey value: `false`, `null`, `undefined`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/falsey/falsy/g
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(altho actually i'll just cherry-pick this into master now and fix it inline; you can rebase after that and omit this commit)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
@ljharb It looks like simulate functionality from React test utils does not return the handlers value, so we're unable to do that at this time. |
(you may want to retry the rebase, and this time excise the isEmptyRender commit entirely) |
@ljharb Rebased the last few commits and squashed them. Looks good now. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great!
It would be worth looking into what it would take (including rewriting simulate
on ReactWrapper) to add invoke
to mount.
test/ShallowWrapper-spec.jsx
Outdated
<a | ||
className={`clicks-${this.state.count}`} | ||
onClick={this.incrementCount} | ||
>foo</a> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nbd, especially in a test, but i'd expect the linter to error out here and require:
<a
className={`clicks-${this.state.count}`}
onClick={this.incrementCount}
>
foo
</a>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps the eslint config is out of date?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe, i also might be wrong and the linter doesn't catch this yet :-) no big deal tho, it'll get fixed eventually
@ljharb This is where the simulate happens from test-utils: https://github.com/facebook/react/blob/master/src/renderers/dom/test/ReactTestUtils.js#L442 Events are queued and executed as a batch. Doesn't look easy to pull the handler value out of it. https://github.com/facebook/react/blob/master/src/renderers/shared/shared/event/EventPluginHub.js#L224 |
I wonder if we did something like, invoke react's simulate method, but not on the prop value - instead, on something like For example, this could be done by temporarily mutating the prop value on the component, and then after the simulation was done, restoring it? The batched updates may make it harder tho. |
@ljharb When using React's simulate, we pass a DOM node, not the props. I'm not sure that will work. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM other than two minor comments
|
||
#### Returns | ||
|
||
`Any`: Returns the value from the event handler.. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
two dots at the end
@@ -172,6 +172,9 @@ Returns the key of the current node. | |||
#### [`.simulate(event[, data]) => ShallowWrapper`](ShallowWrapper/simulate.md) | |||
Simulates an event on the current node. | |||
|
|||
#### [`.invoke(event[, ...args]) => Any`](ShallowWrapper/invoke.md) | |||
Invokes an event handler on the current node and returns the handlers value. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
“and returns the handler’s return value.”
is anything stopping us from implementing this in |
Currently, |
@@ -0,0 +1,39 @@ | |||
# `.invoke(event[, ...args]) => Any` | |||
|
|||
Invokes an event handler (a prop that matches the event name). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i kind of wonder if maybe this should just be a prop name rather than an event name, ie, onClick
, not click
? Then it can be used to invoke any function-valued prop.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes I think I agree
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 for onClick
rather than click
.
This kind of got lost in the version 3 release. I'll dig into it again at some point. |
@milesj would you be open to rebasing this with invoke(propName, ...args) {
// throw if `this.prop(propName)` is not a function
// call it with `args` list
// returns the result
} That way, we can conveniently get access to the return value, with a nicer syntax than Thoughts? |
Yeah most definitely. Can jump on this again. |
cc: @kesne @alecklandgraf
This updates
ShallowWrapper#simulate()
to return the value of the executed handler if available. This allows us to easily test changes that occur in the next tick, like promise chains.