Expressing user actions at a higher level
gnapse opened this issue Β· 34 comments
Describe the feature you'd like:
Following the discussion in testing-library/jest-dom#53 and testing-library/jest-dom#59, it is becoming apparent the need to express user actions on a web page using a higher-level abstraction than fireEvent
.
This issue is to bring the discussion to this repo, where the potential solution actually lies.
Suggested implementation
See this comment from @kentcdodds for details on how the api could look like.
I'd also like to add that we could take a look at similar cypress APIs. I wonder even if having something like what the above linked comment suggests, would be duplicating what cypress already provides. Of course this project is independent from cypress and it could well provide similar functionality.
Skimming over the Cypress API, these seem to be some of the most useful APIs provided and an idea of how they would act:
check
Focus, toggle-on
clear
Focus, select all, backspace
click
Click, focus
dblclick
Click, focus, 10ms delayed click
hover
As hover is a trusted event, it wouldn't be possible to activate the pseudo-class state of the element programmatically.
To address testing-library/jest-dom#53, you would need to programmatically apply the CSS of the hover pseudo-class to the element. Sounds challenging to do correctly.
select
Opening a select is a trusted event, so it wouldn't be possible programmatically.
Focus select, set option to selected
uncheck
Focus, toggle-off
type
Focus, add one character every 10ms
Also have a look at https://github.com/getgauge/taiko whose goal is to provide such a high-level API on top of a browser and a REPL to test it and record scripts.
I wish the very similar efforts dom-testing-library and cousins, Cypress, and now Taiko all put into making human-emulating APIs for webapp testing and automation could be united, but unfortunately Cypress is a not-so-modular-and-importable pile of CoffeeScript.
Yeah, I think that someone should develop a package separate from dom-testing-library that's intended to do this. Then dom-testing-library can just re-export that library (like we do with wait-for-element
).
Anyone want to do that? It'd probably be a lot of fun and I bet would get a lot of use. I just don't have time for it at the moment.
The very part that I wish could be reuse from Cypress is all the funky and often-simply-impossible-without-devtools-protocol but still useful emulation of the browser default behavior via just the DOM APIs (what dom-testing-library does as an infant compared to Cypress).
The part I wish could be reused from Taiko (although I read the code and it's not very robust yet) is the visually relative selectors by text (dom-testing-library kinda does by text, but not visually relative). I mean near
, above
, below
, I would also add alongVector(minDistance, maxDistance)
.
I agree with Kent that it should be a separate library.
@sompylasar Taiko looks amazing, but if I understand correctly since jsdom doesn't support "getBoundingClientRects", or "the ability to calculate where elements will be visually laid out as a result of CSS", we won't be able to do things like near/above/below. :-( Is there something that I'm missing?
It would be fantastic to have it in terms of testing the app like the user would. My team could possibly spend some time building a library like that, but if implementing layout in jsdom is prerequisite, that seems like way beyond our availability. :-(
a package separate from dom-testing-library
Are you sure you don't want to pursue the monorepo idea in #98? The cross-dependency between these packages is a bit hairy and some of the wrappers are undoubtedly already falling behind, depending on implementation.
Just for me to understand, would this "user" library work somehow like this?
import React from "react";
import { render, fireEvent } from "react-testing-library";
import "jest-dom/extend-expect";
function App() {
return (
<React.Fragment>
<input data-testid="A" />
<input data-testid="B" />
</React.Fragment>
);
}
const userEvent = {
click(element) {
/* This is what really happens in my browser
* when I click on an element.
*
* Chrome 69.0.3497.92 (Official Build) (64-bit)
* OS X 10.11.6 (15G22010)
*
* mouseenter
* mouseover
* mousemove (a bunch of them)
* mousedown
* focus
* mouseup
* click
*
* If we fire the same events with fireEvent
* in the same exact order it doesn't work.
*
* This is because:
*
* 1. fireEvent.mouseOver also fires mouseenter
* although in the wrong order
* 2. fireEvent.focus does not set
* document.activeElement. I had to use
* element.focus()
*
* This is the best I could come up with
* to simulate the browser as closely as possible.
*/
const focusedElement = document.activeElement;
const wasAnotherElementFocused =
focusedElement !== document.body && focusedElement !== element;
if (wasAnotherElementFocused) {
fireEvent.mouseMove(focusedElement);
fireEvent.mouseLeave(focusedElement);
}
fireEvent.mouseOver(element);
fireEvent.mouseMove(element);
fireEvent.mouseDown(element);
element.focus();
fireEvent.mouseUp(element);
fireEvent.click(element);
wasAnotherElementFocused && focusedElement.blur();
}
};
test("events", () => {
const { getByTestId } = render(<App />);
userEvent.click(getByTestId("A"));
expect(getByTestId("A")).toHaveFocus();
userEvent.click(getByTestId("B"));
expect(getByTestId("B")).toHaveFocus();
expect(getByTestId("A")).not.toHaveFocus();
});
@alexkrolick, I'm going to be building a monorepo project at work (because I can't use semantic-release there) so we'll see how I enjoy it. My biggest hesitance for using monorepos is the semantic-release solution for monorepos seems slightly unmaintained and I'm worried about things that could come up. Dropping semantic-release from my workflow is kinda out of the question. I wouldn't have time to maintain the packages I do without semantic-release.
That said, I'll let you know what I think of my experience using a monorepo. If it's positive, then I'll see about trying to get semantic-release to work for a monorepo.
@Gpx, yes, I think that's exactly the kind of thing we're looking for π
I think that someone should develop a package separate from dom-testing-library that's intended to do this
Can I give it a shot?
Go for it!
@Gpx Not to discourage you from doing what you're going to do, but there's a lot of corner cases, so please reuse the experience Cypress has collected over time in how this should work. Also puppeteer has an extensive user-simulating API to look at.
From my perspective the most accurate representation of user interactions with the UI is described in terms of the tools available to the user: 1) analyze the output: eyes or screenreaders to observe elements and move pointers on the screen, 2) produce the input: pointers, fingers, keyboards to interact with the screen. The input tools have state: the current coordinates and pressed/released for every button. The "click" action is actually a pointer click so it will be just optional focus of the element under the current coordinates and then click of the element under the current coordinates (also something may rerender on focus so click may hit a different element). Cypress has had a different approach last time I looked: to make as many shortcuts as possible (sacrificing the accuracy of representation), but still they try to emulate all events they can via the DOM.
@sompylasar yep you're right there are going to be many edge cases. As you suggested I intend to start by reviewing what other projects do and what browsers do.
Next, I'll take the new user events I create and put them in my test suite to see how it goes.
We'll probably need to sacrifice some events (I'm thinking of mousemove
for example) but I'll try to be as accurate as possible.
I'll keep you posted and I'm happy if somebody wants to contribute.
Hi everyone, a little update. I've started working on a library called user-event
. At the moment it supports only click
, dblClick
and type
but I've been testing it in my day to day job, and it seems quite useful.
I would much appreciate any feedback and especially bug reports. I believe many edge cases are not covered yet and at the moment I'm focusing only on the issues I find while working on my other projects.
Let me know what you think, and if you want to contribute you're more than welcome!
Fantastic! That's a great start π once we get more user actions in there, then we can pull that in and re-export it π―
Circular dependencies are fun π I'm pretty sure it works though.
I have had a look at the library and even when using that I am struggling to make onChange event handlers be called or the keydown events. I am not sure if something is broken or not. I have been able to make a reproducible sandbox which can be found at the link below. I have added some logs in my onChange
and onKeyDown
event handlers in my component MyField
when using it in the preview I am correctly receiving things like MyField.onFieldChange()
etc but not when running the tests.
I am a bit worried that my testing approach is wrong. I am trying to test things which I should test?
Sandbox URL: https://codesandbox.io/s/ol4qnvy669
Hey @weyert, your test is firing change events at a button, not the input field. I believe that's the problem.
Amazing, thank you @kentcdodds. Clearly you are having a pair of fresh eyes!!
I have created a PR for @Gpx library so that it will dispatch onKeyDown/onKeyUp and change correctly when you are using preventDefault()
in your code to block the entry of a character, you can find it here: testing-library/user-event#27
If this change is against the intended use of react-testing-library
let me know. I think when you a user types 1.5a
and your component does event.preventDefault()
on the a
then the type() should accordingly trigger 1.5
and not 1.5a
@kentcdodds do you think user-event is mature enough to include in the next release?
I'm not sure. I'm actually a bit concerned about this. One of the things I didn't like about enzyme was that there were a million ways to do things. If we include this in dom-testing-library then we have two ways to do things and I feel like it could get confusing. What do you all think?
I think there are inherently at least two ways to simulate user-originated events in the browser: 1) imitate them with variable precision via browser DOM API from within JS runtime (not all possible to implement, many shortcuts and hacks); 2) inject into browser user input queue as if an input device generated them, let the browser translate to DOM events (precise but requires browser automation API which either not available or is async).
I was under the impression that this would be the new default behavior, and that if someone would want to fire events the old way, they would just do it manually. That seems consistent with how dom-testing-library handles selection - if you want to select a class, use DOM methods directly.
I would back using user-events as the new default. The philosophy of dom-testing-library is to test "the way a user would". A user certainly doesn't trigger separate DOM events, of which the user is usually even unaware (think a simple "hover" vs. all the mouseIn, mouseOver and their chains). Instead, if a user clicks on an input field, he would expect the input field to become active, along with any side effects that normally take place in the browser. So, I would replace the "old" way with this.
I would like to see it support more typical user events (drag and drop, hover, etc), then I'd like to see it used in real codebases for a while to see how it works in practice and work out any issues/common questions. I'm a little hesitant, particularly when someone changes from fireEvent.change
to user.type
. Their change handler will get called once for every character typed rather than once for the event which is correct, but I think could lead to confusion.
I agree with @kentcdodds user-event is not mature enough. It still lacks some basic actions and more real-world testing.
As for including it in dom-testing-library, I'm not sure either. I think it shoudβat least in the beginningβbe an external library that offers an alternative to fireEvent
.
Yes, I think it would be a good idea to mention the availability in the React and Dom testing library README that this helper library exists for some common user events :)
Yes, I agree @weyert. Will help with adoption. I would like to suggest that we refer to it as experimental until it gets more features and is more proven.
https://github.com/bigtestjs/interactor - an interesting library for simulating interactions, plus some Cypress like "interactability" detectors like isHidden, etc. Might be possible to get it to work with dom-testing-library somehow.
Closing this as we now have user-event
I think it would be great to expand on user-event
enough for it to be worth replacing fireEvent
. Are there any specific events we'd want to add (maybe all the ones from Cypress)? Since this initial discussion, selectOptions
has been added.
I'm happy to discuss this further, but let's open a new issue on user-event.
Where is the new issue? :D we are looking forward to it.