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

userEvent.type gets only last letter on v12.0.9 #387

Closed
nasdan opened this issue Jun 25, 2020 · 33 comments · Fixed by #388 or #389
Closed

userEvent.type gets only last letter on v12.0.9 #387

nasdan opened this issue Jun 25, 2020 · 33 comments · Fixed by #388 or #389
Labels

Comments

@nasdan
Copy link

nasdan commented Jun 25, 2020

  • @testing-library/user-event version: 12.0.9
  • Testing Framework and version: jest@26.0.1
  • DOM Environment: jsdom@16.2.2, jest-environment-jsdom@26.1.0
  • node@12.14.0

Relevant code or config

name-edit.js

import React from 'react';

export const NameEdit = () => {
  const [userName, setUserName] = React.useState('');

  return (
    <input value={userName} onChange={(e) => setUserName(e.target.value)} />
  );
};

name-edit.spec.js

import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { NameEdit } from './name-edit';

describe('NameEdit component specs', () => {
  it('should update value when input changes', () => {
    // Arrange

    // Act
    render(<NameEdit />);

    const inputElement = screen.getByRole('textbox', {
      name: '',
    });

    userEvent.type(inputElement, 'John');

    // Assert
    expect(inputElement.value).toEqual('John');
  });
});

What happened:

Since I update to v12.0.9 this spec fails because it gets only last letter.

image

It's working on v12.0.8

Reproduction repository:

Sandbox with v12.0.9: https://codesandbox.io/s/quizzical-surf-wpkq1
Sandbox with v12.0.8: https://codesandbox.io/s/eager-lamport-2hwhb

@ybentz
Copy link
Collaborator

ybentz commented Jun 25, 2020

@kentcdodds looking at the changes introduced in 12.0.9 and the code and comments in type.js, looks like type wasn't wrapped by default on purpose and should only be wrapped if a delay was passed otherwise it's considered a sync op? Should that specific change be reverted?
In addition, passing a { delay: 1 } to .type() makes the test pass but throws an "overlapping act() calls" warning so that's probably not the intended behavior.

I did some minimal debugging and looks like if the <input/> uses defaultValue instead of value then the test passes as well. Not sure what that means for how the library handles typing as I'm still very new to the codebase but maybe it'll mean something to you.

I hope this helps.

@kentcdodds
Copy link
Member

I think we need to wrap it with the event wrapper only if it's not using delay. Otherwise it should be wrapped in only the async wrapper

kentcdodds added a commit that referenced this issue Jun 25, 2020
kentcdodds added a commit that referenced this issue Jun 25, 2020
@kentcdodds
Copy link
Member

🎉 This issue has been resolved in version 12.0.10 🎉

The release is available on:

Your semantic-release bot 📦🚀

@marcosvega91
Copy link
Member

It seams that the issue is not solved yet. I have tried here, but the problem is the same

@kentcdodds kentcdodds reopened this Jun 25, 2020
@kentcdodds
Copy link
Member

Could you add a test to reproduce this issue?

https://github.com/testing-library/user-event/blob/master/src/__tests__/type.js#L717

@marcosvega91
Copy link
Member

I think that the problem is that react is not rerendering after input change.

It seams to happen at the beginning and at the end

@wKovacs64
Copy link

I agree it seems specific to React. (Or at least, I can't reproduce in vanilla but I can in React.)

@marcosvega91
Copy link
Member

marcosvega91 commented Jun 25, 2020

I think I understand the problem.

We have type function that is wrapped by act. At the same time all fire event functions are wrapped by act.

act will flush microtask only when there are no more pending act. So because we have nested act this will be done only after typing and not during it.

I don't know if I'm wrong in something

@kentcdodds
Copy link
Member

Yup, verified that intuition is correct @marcosvega91 👍

import React from 'react'
import ReactDOM from 'react-dom'
import {act} from 'react-dom/test-utils'

test('nested act does not flush until the top parent act finishes', () => {
  let setName
  const NameEdit = () => {
    const [userName, setUserName] = React.useState('')
    setName = setUserName
    console.log({userName})

    return (
      <input
        value={userName}
        onChange={(e) => {
          console.log(e.target.value)
          setUserName(e.target.value)
        }}
      />
    )
  }

  const div = document.createElement('div')
  ReactDOM.render(<NameEdit />, div)
  act(() => {
    act(() => {
      setName('John')
    })

    // this fails
    expect(div.firstChild.value).toBe('John')
  })
  // this passes
  // expect(div.firstChild.value).toBe('John')
})

So what I'm going to do is change from option 2 to option 1 (ref: #384)

@marcosvega91
Copy link
Member

Yes this will solve the problem :)

@kentcdodds
Copy link
Member

🎉 This issue has been resolved in version 12.0.11 🎉

The release is available on:

Your semantic-release bot 📦🚀

@grochadc
Copy link

grochadc commented Sep 18, 2020

I am on version 12.1.5 and I still get this error

import React, { Fragment } from "react";
import { render, act } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import App from "./App";

test("Gets a code and renders it in a new route", () => {
  const { getByLabelText } = render(<App />);
  act(() => {
    userEvent.type(getByLabelText("Code:"), "12345");
  });
  expect(getByLabelText("Code:")).toHaveValue("12345");
});
  ● Gets a code and renders it in a new route

    expect(element).toHaveValue(12345)

    Expected the element to have value:
      12345
    Received:
      5

@cevr
Copy link

cevr commented Sep 19, 2020

I get this issue as well, it seems like it sends 1 character at a time instead of all

eg:
userEvent.type(input, "group") is entered as g r o u p instead of g gr gro grou group

@grochadc
Copy link

As I found out by Kent's blog post here
userEvent calls shouldn't be wrapped in act (because they already come pre-wrapped) and it says that the warning might come from some other problem in the code.

@v1adko
Copy link

v1adko commented Sep 22, 2020

Can confirm this issue for me. Code below is working at 12.0.8, however current latest 12.1.6 is logging an error saying that userEvent.type should be wrapped in act. If I attempt it, the issue with having only the last letter typed arises.

    const input = screen.queryByTestId('inline-edit-input');
    expect(input.value).toBe('Text');
    userEvent.type(input, '{backspace}{backspace}tris');
    expect(input.value).toBe('Tetris');

@tlehwalder
Copy link

As I found out by Kent's blog post here
userEvent calls shouldn't be wrapped in act (because they already come pre-wrapped) and it says that the warning might come from some other problem in the code.

Correct me, if I'am wrong, but it says that render and fireEvent are already wrapped in act.

I have the same issue. Without wrapping userEvent.type(..) in act, I get a warning.
Wrapped in act only the last character appears as input value.

@ph-fritsche
Copy link
Member

Warnings about type being not wrapped in act might be caused by a delayed state/hook change.
Your code might defer a call per setTimeout or not await a promise.

    act(() => {
        jest.useFakeTimers()
        // Fire event
        jest.runAllTimers()
    })

@mavv-coder
Copy link

mavv-coder commented Nov 21, 2020

I am still suffering this error while testing next component:

import React from 'react';
import { useField } from 'formik';
import MuiTextArea, { TextFieldProps } from '@material-ui/core/TextField';

export const TextAreaComponent: React.FunctionComponent<TextFieldProps> = props => {
  const [field, meta] = useField(props.name);
  const textAreaProps = Boolean(field) ? field : props;
  const hasError = Boolean(meta && meta.touched && meta.error);

  return (
    <MuiTextArea
      {...props}
      name={textAreaProps.name}
      onChange={textAreaProps.onChange}
      onBlur={textAreaProps.onBlur}
      value={textAreaProps.value ?? ''}
      multiline={true}
      type={'TextareaAutosize'}
      variant={'standard'}
      error={hasError}
      helperText={hasError ? meta.error : ''}
    />
  );
};

Using this test:

import React from 'react';
import { Formik, Form } from 'formik';
import { render, screen, act } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { TextAreaComponent } from './textArea-field.component';

describe('textfield component specs', () => {
  const renderWithFormik = (component, initialValues) => ({
    ...render(
      <Formik initialValues={initialValues} onSubmit={console.log}>
        {() => <Form>{component}</Form>}
      </Formik>
    ),
  });

 it('"input" should change value when typing', () => {
    // Arrange

    // Act
    renderWithFormik(<TextAreaComponent name={name} />, { name: 'test name' });

    const textarea = screen.getByRole('textbox') as HTMLInputElement;

    userEvent.type(textarea, 'test input text');
    
    // Assert
    expect(textarea.value).toEqual('test input text');
  });
});

The test doesn't pass because expect receives 'est input textt' instead of 'test input text'.
If I use act function to wrap userEvent expect only receives the last letter.

What's going on here? Does anyone know the solution to this?


  • node: v12.18.2
  • @testing-library/jest-dom: "^4.2.4"
  • @testing-library/react: "^9.3.2"
  • @testing-library/user-event: "^12.1.10"
  • jest: "^24.9.0"
  • @types/jest: "^24.0.23",

@wangxx1412
Copy link

Hi guys, I have a similar issue with userEvent.type, is only typed the first letter of the string. Does anyone have idea about this weird behavior?

 const emailInput = getByTestId(container, "login-form-email");

 userEvent.type(emailInput,`EmailThatIsCorrect@Email.com`)

 expect(emailInput).toHaveValue('EmailThatIsCorrect@Email.com' );

Terminal:

Expected the element to have value:
  EmailThatIsCorrect@Email.com
Received:
  E

@nickmccurdy
Copy link
Member

nickmccurdy commented Nov 30, 2020

This works fine on CodeSandbox. Can you post a reproduction either on the site or a downloadable zip/gist/repository/etc.?

@kentcdodds
Copy link
Member

Codesandbox was updated to use jsdom a while ago. But it sometimes seems to have other issues which is why I typically avoid using it if I need to do much.

@Hyllesen
Copy link

Hyllesen commented Jan 3, 2021

I've just created a brand new project with create-react-app and I'm able to reproduce both the act warning and getting the error with userEvent only typing the last character.
https://github.com/Hyllesen/testing-library-formik-bug
I could not reproduce the error in codesandbox, https://codesandbox.io/s/youthful-river-0wxrz

@aeaston
Copy link

aeaston commented Jan 5, 2021

For what it's worth, I'm using the latest user-event (12.6.0) and just updated to React 17 (17.0.1 from 16.13.1) and started seeing this issue. Currently using react testing library 10.4.8, but still seemed to have the issue when upgrading to latest there as well.

Unfortunately I can't set up an example repo at the moment, but will try to do so later if it would be helpful. But wondering if there could be something in React 17 that might contribute to this?

EDIT: Turns out my issue is actually that the input element is never getting focused, and that's messing with some of our custom logic. In version 17, React uses focusin/focusout events instead of focus/blur, and older versions of jsdom (<16.3.0) didn't fire those events on focus/blur.

Also I've edited this a few times, so feel free to check out the edit history to see the stages I went through to get here =)

@Hyllesen
Copy link

Hyllesen commented Jan 5, 2021

For what it's worth, I'm using the latest user-event (12.6.0) and just updated to React 17 (17.0.1 from 16.13.1) and started seeing this issue. Currently using react testing library 10.4.8, but still seemed to have the issue when upgrading to latest there as well.

Unfortunately I can't set up an example repo at the moment, but will try to do so later if it would be helpful. But wondering if there could be something in React 17 that might contribute to this?

EDIT: Turns out my issue is actually that the input element is never getting focused, and that's messing with some of our custom logic. In version 17, React uses focusin/focusout events instead of focus/blur, and older versions of jsdom (<16.3.0) didn't fire those events on focus/blur.

Also I've edited this a few times, so feel free to check out the edit history to see the stages I went through to get here =)

Could you give an example of how you solved this issue?
Isn't the whole purpose of userEvent to automate things like focus and blur vs. fireEvent?

@aeaston
Copy link

aeaston commented Jan 5, 2021

Yeah so userEvent correctly calls element.focus() and element.blur(). It just took me a minute to realize that. The problem we were seeing was that jsdom 16.2.2 (what we were using at the time) actually didn't fire focusin or focusout events when those calls were made, as browsers do. But it turns out jsdom actually fixed that in 16.3.0, so just had to upgrade. Since we get jsdom from Jest, we needed to upgrade jest to 26.5.0.

I want to reiterate though that our issue was actually due to custom logic we had running when the user typed and the input wasn't focused (which shouldn't really ever happen). We only saw this because of the way React changed onFocus to bind to focusin event instead of focus event in version 17, and the way jsdom fired those events (or in our case, didn't fire them).

@j10sanders
Copy link

j10sanders commented Apr 9, 2021

I had this issue when migrating from fireEvent to userEvent. I found that the userEvents were unnecessarily wrapped in act

@maxscott
Copy link

Context

I got burned by a version of this today and want to document it for anyone else having a similar problem.
Also, this is my current understanding of the problem so I welcome any and all suggestions to make it more correct/complete.

Description

As I understand it, this is a classic case of mishandling closed-over data in the implementation of the state-altering function. It becomes apparent when the client (in my case, userEvent.type(element, text)) queues up several events (looping over the characters in your typed string).

Test Snippet

render(<App formAction={myAssertions} />);
userEvent.type(screen.getByPlaceholderText('Password'), 'p@ssw0rd');
userEvent.type(screen.getByPlaceholderText('Email'), 'some@email.com');
userEvent.click(screen.getByText('Submit'));

App Code

  function handleChange(ev: FormEvent, key = "") {
    ev.preventDefault();

    // ✅ Passes
    const value = (ev.target as HTMLInputElement).value;

    setState(s => {
      // ❌ Fails (individual characters instead of progressively built up strings)
      // const value = (ev.target as HTMLInputElement).value;

      return { ...s, [key]: value };
    });
  }

My understanding: if we call handleChange in a loop (which I believe the libraries are) and don't use let/const to capture the event first, it's our handleChange function's responsibility to capture the event (or use a closure) before referring to it inside setState. If we don't, the setState functions can be executed after the last event is passed in, and javascript will not find the correct event, since the events were never scoped to the individual loop.

I don't have any suggestions here because I'm not super familiar with the libraries, but I would love to hear other's opinions on how to remove this pain outright, because it seems like an easy mistake that's hard to explain.

@ph-fritsche
Copy link
Member

@maxscott You're nearly correct.

Properties are resolved when the expression is executed. So when you access a property in a function and pass this function to be executed at some point later it will yield the value the property will have then.

const a = { foo: 'bar' }
const b = () => a.foo
b() // is 'bar'
a.foo = 'baz'
b() // is 'baz'

React optimizes state changes and the resulting rendering by batching these changes.
It queues them and they will be executed at the end of the event handling.

function Foo() {
  const [, setStateA] = useState()
  const [, setStateB] = useState()
  return <button onClick={() => { // a click on this button will only cause one rerender
    setStateA({});
    setStateB({});
  }/>
}

React also queues up state changes that happen inside act as described above

  render(<Foo/>)
  act(() => { // this will only rerender once at the end of act
    screen.getByRole('button').click()
    screen.getByRole('button').click()
  })

This also means that the event handler for the second click in that act is executed before the rerender and if it accesses the DOM, it will get properties before the rerender.
userEvent.type accesses element.value to determine the new value, so when the event handlers triggered by userEvent.type are executed in act they will all access the DOM before any rerender so any controlled input will only receive the last change.

That was the issue - everything else in this thread is unrelated to the initial issue and only adds confusion.

Should you experience any problems that look like the issues described above please file a new one with a reproduction at codesandbox.

@maxscott
Copy link

@ph-fritsche Thank you for the insight here, especially into act and type, and thank you for your work on this great library! This also confirms my suspicion about type's implementation and controlled inputs. I hope this isn't too common a pitfall, and if you don't think the above use case warrants it's own issue, I'll defer for now.

@cs11wcq
Copy link

cs11wcq commented Sep 8, 2021

Hey @kentcdodds so is there a solution for this? I try to use userEvent instead of fireEvent. The input here has type "number". I am trying to test that entering an alphabetic character is ignored. I want only numeric characters to be allowed. It seems like its only getting the last character

userEvent.type(input, '20a1');
expect(input.value).toBe('201');

I am getting

  Expected: "201"
  Received: "1"

Edit: I notice when I change the input type to text then it works as expected... I guess thats the fix then. dont use input type number

@wsanchez
Copy link

FWIW I've still got this problem (specifically: without act() I get an error and with act() I get only the last letter) with 13.5.0.

Workaround that seems to work is: keep act() and add {delay: 0.00001} to userEvent.type() options.

@RobinLeeb
Copy link

RobinLeeb commented Jan 25, 2022

@wsanchez Unfortunately, does not work for me.

In my case the received value just doesn't change at all. Also when i search for workarounds and try that out.

Even if i wrapped it with act() or try to use waitFor() or not - also async and not async.
I use version "@testing-library/user-event": "^13.3.0".

Has this maybe also to do with formik? Because i wrote a formik wrapper/mock for that test? Or has this to do, because i use three input fields which gets at the end wrapped in one <Field/>

@ph-fritsche
Copy link
Member

The initial issue is explained above.
This was resolved in v12.0.11.
Since then, it is not required and strongly advised against wrapping calls to userEvent in act because this conflicts with batched state changes as explained above.

That was the issue - everything else in this thread is unrelated to the initial issue and only adds confusion.

I strongly recommend you update to v14-beta.
If you still have problems, please consider to open a discussion or join us at Discord.

@testing-library testing-library locked as resolved and limited conversation to collaborators Jan 25, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet