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

Expressing user actions at a higher level #107

Closed
gnapse opened this issue Sep 20, 2018 · 34 comments
Closed

Expressing user actions at a higher level #107

gnapse opened this issue Sep 20, 2018 · 34 comments

Comments

@gnapse
Copy link
Member

gnapse commented Sep 20, 2018

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.

@matchai
Copy link
Contributor

matchai commented Sep 20, 2018

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

@sompylasar
Copy link
Collaborator

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.

@kentcdodds
Copy link
Member

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.

@sompylasar
Copy link
Collaborator

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.

@lgandecki
Copy link
Collaborator

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

@alexkrolick
Copy link
Collaborator

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.

@Gpx
Copy link
Member

Gpx commented Sep 20, 2018

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();
});

@kentcdodds
Copy link
Member

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

@Gpx
Copy link
Member

Gpx commented Sep 20, 2018

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?

@kentcdodds
Copy link
Member

Go for it!

@sompylasar
Copy link
Collaborator

sompylasar commented Sep 20, 2018

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

@Gpx
Copy link
Member

Gpx commented Sep 21, 2018

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

@Gpx
Copy link
Member

Gpx commented Sep 30, 2018

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!

@kentcdodds
Copy link
Member

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.

@weyert
Copy link
Contributor

weyert commented Oct 18, 2018

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

@kentcdodds
Copy link
Member

Hey @weyert, your test is firing change events at a button, not the input field. I believe that's the problem.

@weyert
Copy link
Contributor

weyert commented Oct 18, 2018

Amazing, thank you @kentcdodds. Clearly you are having a pair of fresh eyes!!

@weyert
Copy link
Contributor

weyert commented Oct 19, 2018

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

@alexkrolick
Copy link
Collaborator

@kentcdodds do you think user-event is mature enough to include in the next release?

@kentcdodds
Copy link
Member

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?

@sompylasar
Copy link
Collaborator

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

@matchai
Copy link
Contributor

matchai commented Nov 17, 2018

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.

@artooras
Copy link

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.

@kentcdodds
Copy link
Member

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.

@Gpx
Copy link
Member

Gpx commented Nov 19, 2018

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.

@weyert
Copy link
Contributor

weyert commented Nov 20, 2018

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 :)

@kentcdodds
Copy link
Member

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.

@alexkrolick
Copy link
Collaborator

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.

@kentcdodds
Copy link
Member

Closing this as we now have user-event

@nickmccurdy
Copy link
Member

nickmccurdy commented Nov 8, 2019

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.

@kentcdodds
Copy link
Member

I'm happy to discuss this further, but let's open a new issue on user-event.

@JimmyLv
Copy link

JimmyLv commented Nov 15, 2019

Where is the new issue? :D we are looking forward to it.

@nickmccurdy
Copy link
Member

@nickmccurdy

This comment has been minimized.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests