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

feat(userEvent): build-in @testing-library/user-event #616

Closed
wants to merge 31 commits into from

Conversation

kentcdodds
Copy link
Member

@kentcdodds kentcdodds commented Jun 11, 2020

What: This merges @testing-library/user-event into @testing-library/dom.

Why: Most of the time people should be using user event.

How:

The first commit was: copy/paste + slightly modify.

This is NOT yet ready to be merged! I have several breaking changes, bug fixes, and new features to add before we'll be able to release this.

I've opened the PR up just to get discussion going. Would love to hear people's thoughts.

Checklist:

  • Documentation added to the docs site
  • Types
  • Tests
  • Ready to be merged

I'll mention that I know and accept that there will be limitations to user-event as we bring this in. But there will always be limitations. Despite that I think people are better off using userEvent rather than fireEvent for situations it works. Also, sometimes it will make more sense for people to give up on userEvent and instead use Cypress to test whatever it is they're trying to test.

Current status (help wanted):

@codesandbox-ci
Copy link

codesandbox-ci bot commented Jun 11, 2020

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 480fe24:

Sandbox Source
beautiful-proskuriakova-ognmh Configuration

@@ -24,6 +24,7 @@ test('{esc} triggers typing the escape character', async () => {
Events fired on: input[value=""]

focus
select
Copy link
Member Author

Choose a reason for hiding this comment

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

All of the select events are coming from calling setSelectionRange on the input...

Not sure how to avoid this but I think we should try... Will circle back on this when we do the audit.

@kentcdodds
Copy link
Member Author

Alright. Things are starting to look pretty good I think.

What we need now is to do the correctness audit and to add a few more tests to get to 100% coverage.

Copy link
Member

@eps1lon eps1lon left a comment

Choose a reason for hiding this comment

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

There are too many testing utils for my taste. These tend to get bloated over time and in a couple of months nobody understands what a test is doing. I'd really appreciate the extra time to inline these. It costs more writing time but will pay off long term for readability.

@@ -0,0 +1,6 @@
{
"rules": {
// the wait in the loop is intentional and we don't want to parallelize
Copy link
Member

Choose a reason for hiding this comment

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

the loop

Sounds like this should rather be an eslint-disable-next-line

Copy link
Member Author

Choose a reason for hiding this comment

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

I copy/pasted that comment from one situation, but there are several. I couldn't think of a single situation in this directory where we'd want the parallization so it made sense to me to just disable it for this directory.

Copy link
Member

Choose a reason for hiding this comment

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

But this will also apply to any future file/addition. It's intentional for the code right now. But nobody will remember this rule exists if they add new code or it isn't intentional anymore.

Copy link
Member Author

@kentcdodds kentcdodds Jun 11, 2020

Choose a reason for hiding this comment

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

I honestly can't imagine a situation where we'd want parallelization. The async nature of this directory is to simulate the user's behavior (user's can't do two things at once). I think it's more sensible to disable this rule in this directory rather than add comments everywhere. They're just noise and make people feel like they may be doing something wrong.

@@ -58,7 +58,8 @@
"import/prefer-default-export": "off",
"import/no-unassigned-import": "off",
"import/no-useless-path-segments": "off",
"no-console": "off"
"no-console": "off",
"no-func-assign": "off"
Copy link
Member

Choose a reason for hiding this comment

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

The rule seems fairly reasonable to me. Any reasons you can't use function expressions over function declarations?

Copy link
Member Author

Choose a reason for hiding this comment

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

This rule bugs me a bit. I prefer this:

function foo() {
}
foo = wrap(foo)

Over this:

const foo = wrap(function foo() { // don't want this to be anonymous because I want the name
})

Applies to all HOCs I use in React (like React.memo). In here we have this wrapAsync function which wraps all of our utilities in the config's asyncWrapper and I just like the way it looks better when we reassign the function.

Copy link
Member

Choose a reason for hiding this comment

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

Why do you prefer the first one? Because I definitely didn't know you can re-assign function declarations. I wouldn't think this is valid which is exactly the rationale of this rule. The second pattern looks fine to me.

Copy link
Member

Choose a reason for hiding this comment

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

In any case: If you like it why not disable it in kcd-scripts and explain the rationale? If you only have a personal, stylistic preference I'd always favor the default ruleset which makes onboarding easier.

Copy link
Member Author

Choose a reason for hiding this comment

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

I've been meaning to disable it in eslint-config-kentcdodds for a while now 😅 Just haven't gotten around to it.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I didn't know that was possible either

src/user-event/__tests__/select-options.js Outdated Show resolved Hide resolved
src/user-event/__tests__/tab.js Outdated Show resolved Hide resolved
@kentcdodds
Copy link
Member Author

I have some ideas on how we could simplify the test utils. Will work on that.

@benmonro
Copy link
Member

@kentcdodds might be nice to create a codeshift to help folks migrate their code.

@kentcdodds
Copy link
Member Author

A codemod would be super cool. I will not have the time to work on it unfortunately. But anyone's welcome to make one. So far, here are the differences:

  1. All userEvent calls will be async
  2. The allAtOnce option in type will be replaced by userEvent.paste
  3. The imports will need to be updated (React Testing Library will re-export userEvent)

@benmonro
Copy link
Member

@kentcdodds Is the plan still that fireEvent will now be just using userEvent?

@kentcdodds
Copy link
Member Author

fireEvent will be unchanged through all of this. userEvent will use fireEvent. fireEvent will still be exposed as a lower-level primitive, but all examples and things that can be updated to userEvent should be.

@benmonro
Copy link
Member

oh... ok. curious what the rationale was for that? seems like it'd be a bit confusing to have 2 options there, no?

@kentcdodds
Copy link
Member Author

Because there are some things that we can't really do within userEvent that someone could only do with fireEvent.

@MichaelDeBoey
Copy link
Member

@benmonro Something I suggested was adding a drag-event, which isn't a separate event at this moment, but could be implemented with fireEvent.

@kentcdodds
Copy link
Member Author

Perhaps a lint rule against things that can be done with userEvent (so if someone does fireEvent.click tell them to use userEvent.click for example). But there are plenty of events that you can fire that don't have a corollary userEvent yet.

@timdeschryver
Copy link
Member

A codemod would be super cool. I will not have the time to work on it unfortunately. But anyone's welcome to make one.

I'll look into that

@MichaelDeBoey
Copy link
Member

@timdeschryver Don't know if it's possible, but I'm interested in learning how to create codemods and help you out on creating one

@benmonro
Copy link
Member

benmonro commented Jun 12, 2020

@MichaelDeBoey it's not too hard, astexplorer.net is pretty great for building out codemods, lint rules, and really anything AST based.

if you go there and choose 'jscodeshift' (which is what codemods are based on)
from the transforms it will give you an example.
image

From there it's up to you to write all the jscodeshift code.

https://github.com/facebook/jscodeshift

@kentcdodds
Copy link
Member Author

FWIW, I've tried jscodeshift multiple times and never could figure it out, so I use a babel plugin with babel-codemod instead (still recommend astexplorer though).

@codecov
Copy link

codecov bot commented Jun 12, 2020

Codecov Report

Merging #616 into master will not change coverage.
The diff coverage is 100.00%.

Impacted file tree graph

@@            Coverage Diff             @@
##            master      #616    +/-   ##
==========================================
  Coverage   100.00%   100.00%            
==========================================
  Files           24        35    +11     
  Lines          565       956   +391     
  Branches       141       250   +109     
==========================================
+ Hits           565       956   +391     
Impacted Files Coverage Δ
src/event-map.js 100.00% <100.00%> (ø)
src/events.js 100.00% <100.00%> (ø)
src/user-event/blur.js 100.00% <100.00%> (ø)
src/user-event/clear.js 100.00% <100.00%> (ø)
src/user-event/click.js 100.00% <100.00%> (ø)
src/user-event/focus.js 100.00% <100.00%> (ø)
src/user-event/hover.js 100.00% <100.00%> (ø)
src/user-event/select-options.js 100.00% <100.00%> (ø)
src/user-event/tab.js 100.00% <100.00%> (ø)
src/user-event/type.js 100.00% <100.00%> (ø)
... and 15 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update a76dfad...480fe24. Read the comment docs.

@kentcdodds
Copy link
Member Author

Alrighty folks!

This is officially ready for review (thank you to everyone who's already been involved so far).

What remains is in the original comment (right now that's type definition updates and docs, both of which I'd really love help with!!).

Regarding the types, I'm thinking I'd prefer to get this merged in before #614. I think that would be quickest and easiest. This is a bit of a shake-up (though luckily most of the changes are in the brand new user-event folder).

If you'd like to help with types/docs/bugs, you can make a PR to this branch and I'll merge it in! I won't be working on anything this weekend (family time) but I may be able to slip in a review/merge here or there.

I'd love to get this released early next week. Thanks!

@kentcdodds
Copy link
Member Author

Tracking docs contribution: testing-library/testing-library-docs#490

@@ -18,72 +18,78 @@ function fireEvent(element, event) {
})
}

const createEvent = {}
function createEvent(
Copy link
Member Author

Choose a reason for hiding this comment

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

No substantial changes here. This was necessary to make it easier to create regular window.Event events that still had the niceties that createEvent had to offer.

@@ -1,350 +1,360 @@
export const eventMap = {
Copy link
Member Author

Choose a reason for hiding this comment

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

Formatting change only.

@kentcdodds
Copy link
Member Author

Sorry for the massive ping (feel free to unsubscribe from this issue), but I just want to give a huge shout-out to all the contributors of @testing-library/user-event over the year and a half (especially @Gpx). I was skeptical that userEvent could reasonably deliver on what it promises, and while it's not perfect and there are limitations, it's really quite good and I'm excited for it to take it's rightful place as the officially recommended and built-in way to interact with components under test in the Testing Library family.

Thank you contributors!

applause

👏👏👏👏👏👏👏👏👏👏👏👏👏👏👏👏👏👏👏👏

@Gpx @weyert @twhitbeck @MichaelDeBoey @michaellasky @shomalgan @calebeby @afontcu @skywickenden @bogdanbodnar @zperrault @FLGMwt @benmonro @GentlemanHal @YuanchengWu @maheshjag @jmcriffey @kandros @jordyvandomselaar @ilyamkin @klujanrosas @jsmapr1 @wachunga @bdh1011 @Meemaw @pomber @Raynos @skovy @vadimshvetsov @9still @rbusquet @dougbacelar @kayleighridd @malcolm-kee @kelvinlzhang @krzysztof-hellostudio @hontas @hudochenkov @nanivijay @tpict @nvh95 @nickmccurdy @timdeschryver (hope I didn't miss anyone 😅)

nickserv
nickserv previously approved these changes Jun 13, 2020
Copy link
Member

@nickserv nickserv left a comment

Choose a reason for hiding this comment

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

Looks solid so far, not noticing any glaring issues

@Raynos
Copy link

Raynos commented Jun 13, 2020

👍 I use both packages in my tests and it makes sense for it to be a single package to install and import.

@eps1lon
Copy link
Member

eps1lon commented Jun 15, 2020

In my opinion, it does not make sense that userEvent.xxx are async

I think the underlying problem is that all dispatches are awaited sequentially instead of once. The dispatched events shouldn't be awaited since that wouldn't be the case in a browser anyway. In react we should wrap all dispatches in a single act instead of wrapping every single dispatch i.e.

act(() => {
  fireEvent.mouseDown();
  element.focus();
  fireEvent.mouseUp();
  element.click();
});

instead of

act(() => void fireEvent.mouseDown())
act(() => void element.focus())
act(() => void fireEvent.mouseUp())
act(() => void element.click())

or for Promises:

await Promise.all([fireEvent.mouseDown(), element.focus(), fireEvent.mouseUp(), element.click()]);

instead of

await fireEvent.mouseDown();
element.focus();
await fireEvent.mouseUp();
element.click();

@Lukas-Kullmann
Copy link
Contributor

This problem is not related to the react testing library. It is an inherit problem of the dom testing library. You can reproduce the bug with just the dom testing library.

Here is a small code sandbox with @testing-library/dom only: https://codesandbox.io/s/lingering-firefly-rl9fn

In any case

I think the underlying problem is that all dispatches are awaited sequentially instead of once

In that case, the dispatches being async is the underlying problem to this problem. Why do they need to be async?
Maybe the bug can be fixed if all dispatchers are awaited in parallel. But I think the easier solution would be to make this thing synchronous again.

@marcosvega91
Copy link
Member

maybe is not the right solution, but if we save a DOM diff for every event between start and end of the event? Then in waitForElementToBeRemoved we could check what happened before.
😕

* Exit early from selectOption if select is disabled

* fix(userEvent): Port test to dom-testing-library

* fix(userEvent): Exit early if options are disabled

Co-authored-by: Ben Dyer <[email protected]>
@kentcdodds
Copy link
Member Author

The rational for making it all async (and making every fireEvent async) is that a user cannot fire a mousedown and then a mouseup synchronously after it. Those two events will happen on different ticks of the event loop. It's possible that the browser fires some events synchronously (maybe after the mouseup, the click is dispatched synchronously).

In any case, I don't think that awaiting between fireEvents is the problem here because the click event we're listening for is the last one that's fired. We don't await anything after that.

The only solution I can see as "working" is to make it all sync again, but then it's not really resembling the way the software is used and I'd like to do that...

@eps1lon
Copy link
Member

eps1lon commented Jun 15, 2020

The rational for making it all async (and making every fireEvent async) is that a user cannot fire a mousedown and then a mouseup synchronously after it

We cannot fire them sync as well. They are always consecutive. The issue right now is that we put each event in a separate tick of the (micro?)-task loop. We need an explainer for that. Does the ui-events spec define it that way?

@kentcdodds
Copy link
Member Author

kentcdodds commented Jun 15, 2020

I don't think the spec needs to. How could a user possibly click their mouse fast enough to fire mousedown and mouseup on the same tick of the event loop? They couldn't.

@kentcdodds
Copy link
Member Author

This may be one of the trade-offs we have to make on correctness to practicality though...

@eps1lon
Copy link
Member

eps1lon commented Jun 15, 2020

They couldn't.

Maybe for mousedown and mouseup. But right now you're putting every fired event in a separate tick without any justification. Just saying "you couldn't do it" is not an argument.

@kentcdodds
Copy link
Member Author

kentcdodds commented Jun 15, 2020

We'd have the same problem for anyone testing a similar situation for typing. So if a loading indicator pops up while the user is typing, they would have this same problem with async type.

@kentcdodds
Copy link
Member Author

kentcdodds commented Jun 15, 2020

I realize that it's possible the browser fires some events in the same event loop tick. I'm not sure how to determine that. But it really doesn't matter. Once we have to make anything async, then we'll have this problem. Remember that the click event is the one that we're listening for in my example and that's the last event that fires in the sequence.

@eps1lon
Copy link
Member

eps1lon commented Jun 15, 2020

I'm just now realizing the extent of all this async wrapping. Why is focus async? Why do we fire focusIn and await it? (btw the whole implementation is only required for JSDOM. It makes more sense to upstream focusin/focusout).

I don't feel comfortable merging user-event in its current state. There are just too many assumptions that may very well be correct but aren't documented in any way. This works for a personal library but not one where we want to share maintenance.

@Lukas-Kullmann
Copy link
Contributor

but then it's not really resembling the way the software is used and I'd like to do that...

This is of course a valid design decision and a killing argument for every counter argument argument that goes into the direction of making it synchronous.

The thing that concerns me however is that tests never fully mimic the real world. The reason why we run into tgis bug here is because we have an unrealistically fast UI update from state A -> loading -> state B. In the real world, that would never happen in just one async tick. And of course, the bug is solved if you introduce a delay before generating the new random number of, say, 200ms.

But that's also how network mocking usually works. You resolve the network request after just one async frame (or at least a very shorter time than it would usually take in the production app). So as long as we resolve network requests so fast, we cannot really affort to have event phases that take so long (compared to all the other async tasks that you have in your app).

@kentcdodds
Copy link
Member Author

I'm beginning to feel that this needs more research before we can commit to bringing this into Dom testing library core. we can take the improvements that I've made in this PR and port them back to the original user event module and we'll wait on for the research before we bring it into core.

@kentcdodds
Copy link
Member Author

I love feedback on that plan. Just give a thumbs up to that comment if you agree or comment with a argument against it if you disagree.

@Lukas-Kullmann
Copy link
Contributor

I think this is not a good idea. Porting this to user-event does not make this type of bug go away. It will just lead to a very painful migration for the users of the library to the new version.

And I think this would also be a thing that quite some users would run into. And there is nothing that the library can do to nudge the user into the right direction for finding the cause of this bug. Let alone how to solve it.
I already find it quite painful find the cause of a failing test that says "Cannot find element with text/role/whatever".
At the moment you can most likely resolve this issue by replacing a "getBy" with a "findBy". With this change, the search space for the error extends "the element is not there yet" to also "the element is already gone". In my experience, the latter does not occur often at the moment. With bug still present, this would become quite usual.

@kentcdodds
Copy link
Member Author

Sorry, to be more clear, I'd move all my improvements I've made here back to user-event, without the "everything is async now" bit :)

@kentcdodds
Copy link
Member Author

Ok, I'm going to port back my improvements (sans the async-ify everything) and we'll close this here.

@Lukas-Kullmann
Copy link
Contributor

Sorry, to be more clear, I'd move all my improvements I've made here back to user-event, without the "everything is async now" bit :)

Then the question becomes, why not removing the async stuff and leaving it here? 🙂

@kentcdodds
Copy link
Member Author

Fair point. I guess it's because I don't think that it's right if we're not doing async and I don't want it in core. It's just not ready yet I think.

@kentcdodds
Copy link
Member Author

Ok, it's official. We're not merging this right now. I've made all the same improvements that I worked on for the last two weeks to user-event itself. Except everything is sync (even type unless you pass a delay, in which case it's async, but you're opting into that behavior).

I'm disappointed about this (especially since I know several frameworks need everything to be async, so this means we're fragmented), but this is the best we can do.

@kentcdodds
Copy link
Member Author

Oh, and here's the PR: testing-library/user-event#348

@kentcdodds kentcdodds closed this Jun 15, 2020
@kentcdodds kentcdodds deleted the pr/user-event branch June 15, 2020 22:17
@nickserv
Copy link
Member

Could we continue merging in user-event but without the async API changes? I'm not sure they both have to be done together, especially if we still plan on releasing user-event.

@kentcdodds
Copy link
Member Author

I really don't think we should. I'd prefer to make it async somehow. That's more correct.

But I'm out of bandwidth on this so I'm no longer going to be working on it 😫

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
enhancement New feature or request help wanted Extra attention is needed
Projects
None yet
Development

Successfully merging this pull request may close these issues.