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

Add ShallowWrapper#invoke() to invoke event handlers and return the handlers value #945

Merged
merged 1 commit into from Apr 6, 2019
Merged

Conversation

milesj
Copy link
Contributor

@milesj milesj commented May 16, 2017

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.

// handler
onClick() {
  return Promise.resolve()
    .then(() => {
      this.setState({ foo: true });
    });
}

// test
wrapper.simulate('click').then(() => {
  expect(wrapper.state('foo')).toBe(true);
});

@milesj milesj changed the title Updated ShallowWrapper#simulate() to return the handlers value Update ShallowWrapper#simulate() to return the handlers value May 16, 2017
});
}
return this;
return null;
Copy link
Member

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.

performBatchedUpdates(this, () => {
handler(...args);
response = handler(...args);
Copy link
Member

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();
Copy link
Member

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)

@nfcampos
Copy link
Collaborator

nfcampos commented May 17, 2017

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 ShallowWrapper#simulate does not properly propagate bubble events, thus firing at most a single event handler, is a shortcoming that we probably want to address at some point (see #368 for an implementation of just that).

As such, this gets a 👎 from me, what do the other maintainers think?

cc @ljharb

@nfcampos
Copy link
Collaborator

In other words, if we get this in we're accepting that the simulate method on shallow will be semantically different from the simulate method on mount going forward, and we should close #368

@milesj
Copy link
Contributor Author

milesj commented May 17, 2017

@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 simulateAndReturn?

@nfcampos
Copy link
Collaborator

@milesj events in browsers bubble through the tree of rendered elements, ie. in

<div onClick={doSomethingElse}>
  <div onClick={doSomething} />
</div>

if you click on the inner div both onClick handlers fire.
Our current implementation of shallow happens to not respect that (mount does) so currently if you do .simulate('click') on that inner div using the shallow renderer only doSomething will be invoked.

@milesj
Copy link
Contributor Author

milesj commented May 17, 2017

Correct, and that's a single event. That's what I'm getting at with shallow.

@nfcampos
Copy link
Collaborator

yes, it's a single event, invoking two event handlers, so how do you choose which one to return the return value for?

@milesj
Copy link
Contributor Author

milesj commented May 17, 2017

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.

@nfcampos
Copy link
Collaborator

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 shallow.simulate because the two concepts are not compatible.

I'm not strictly against either option, but I do believe we should have that trade-off in mind while considering either option.

@milesj
Copy link
Contributor Author

milesj commented May 17, 2017

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.

@nfcampos
Copy link
Collaborator

@milesj
Copy link
Contributor Author

milesj commented May 17, 2017

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.

@nfcampos
Copy link
Collaborator

do you want to cherry-pick the relevant commits out of that PR and change the return value?

@milesj
Copy link
Contributor Author

milesj commented May 17, 2017

I can look into it since that PR is currently blocked.

@milesj milesj changed the title Update ShallowWrapper#simulate() to return the handlers value Add ShallowWrapper#invoke() to invoke event handlers and return the handlers value Jun 1, 2017
@milesj
Copy link
Contributor Author

milesj commented Jun 1, 2017

@nfcampos @ljharb @kesne

Updated the PR to pull invoke from #368

Also noticed there were no docs for isEmptyRender, so added them here.

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.

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`.
Copy link
Member

Choose a reason for hiding this comment

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

s/falsey/falsy/g

Copy link
Member

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)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

👍

@milesj
Copy link
Contributor Author

milesj commented Jun 1, 2017

@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.

@ljharb
Copy link
Member

ljharb commented Jun 1, 2017

(you may want to retry the rebase, and this time excise the isEmptyRender commit entirely)

@milesj
Copy link
Contributor Author

milesj commented Jun 1, 2017

@ljharb Rebased the last few commits and squashed them. Looks good now.

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.

Looks great!

It would be worth looking into what it would take (including rewriting simulate on ReactWrapper) to add invoke to mount.

<a
className={`clicks-${this.state.count}`}
onClick={this.incrementCount}
>foo</a>
Copy link
Member

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>

Copy link
Contributor Author

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?

Copy link
Member

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 ljharb added semver: minor New stuff. and removed semver: major Breaking changes labels Jun 1, 2017
@milesj
Copy link
Contributor Author

milesj commented Jun 2, 2017

@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

@ljharb
Copy link
Member

ljharb commented Jun 3, 2017

I wonder if we did something like, invoke react's simulate method, but not on the prop value - instead, on something like (...args) => { const value = originalPropValue(...args); return value; }?

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.

@milesj
Copy link
Contributor Author

milesj commented Jun 6, 2017

@ljharb When using React's simulate, we pass a DOM node, not the props. I'm not sure that will work.

Copy link
Collaborator

@nfcampos nfcampos left a 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..
Copy link
Collaborator

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.
Copy link
Collaborator

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.”

@nfcampos
Copy link
Collaborator

nfcampos commented Jun 11, 2017

is anything stopping us from implementing this in mount in the exact same way, ie. select the prop out of the component and invoke it?

@koba04
Copy link
Contributor

koba04 commented Dec 30, 2017

Currently, .simulate() causes many problems so I think enzyme should have an alternative API to replace simulate().
I think this invoke() feels to be close it.
What status is this PR?

@@ -0,0 +1,39 @@
# `.invoke(event[, ...args]) => Any`

Invokes an event handler (a prop that matches the event name).
Copy link
Member

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.

Copy link
Collaborator

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

Copy link
Contributor

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.

@milesj
Copy link
Contributor Author

milesj commented Dec 31, 2017

This kind of got lost in the version 3 release. I'll dig into it again at some point.

@ljharb
Copy link
Member

ljharb commented Aug 29, 2018

@milesj would you be open to rebasing this with invoke on both shallow and mount wrappers, with this signature/behavior?

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 .prop('onClick')().

Thoughts?

@milesj
Copy link
Contributor Author

milesj commented Aug 29, 2018

Yeah most definitely. Can jump on this again.

@koba04 koba04 mentioned this pull request Oct 8, 2018
@milesj milesj closed this Mar 7, 2019
@ljharb ljharb reopened this Mar 7, 2019
@ljharb ljharb merged commit 258823f into enzymejs:master Apr 6, 2019
@milesj milesj deleted the simulate-return branch April 6, 2019 23:25
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.

None yet

4 participants