testing-library / user-event

🐕 Simulate user events
https://testing-library.com/user-event
MIT License
2.2k stars 249 forks source link

userEvent.type not working in v14 #1150

Open yasin459 opened 1 year ago

yasin459 commented 1 year ago

Reproduction example

zip file listed bellow

Prerequisites

please download this project demo and run it using npm install react-testing-library-bug.zip

it has 2 tests: 1- testThatFails 2- testThatWorks

in thre first example, test fails because and can not type into coponent using userEvent.type:

image code for failing test:


const ExampleThatFails = () => {
  const [state, dispatch] = useReducer(reducer, [
    {
      name: "Age",
      show: true,
      filterValueType: "integer",
      value: ["", ""],
      optionType: "exact",
      key: "age",
    }
  ]);

  return (
    <div>
      <FilterCombo filters={state} dispatch={dispatch} />
      <FilterItem dispatch={dispatch} idx={0} filter={state[0]} />
    </div>
  );
};

it.each(Array(100).fill(null))("ExampleThatFails", async () => {
  render(<ExampleThatFails />);

  const AgeFilterOption = screen.getAllByTestId("filter-item")[0];
  const AgeSelectBox = within(AgeFilterOption).getByRole("button", {
    name: "exact",
  });
  await userEvent.click(AgeSelectBox);
  const AllAgeOptions = within(AgeFilterOption).getAllByRole("option");

  await userEvent.click(AllAgeOptions[1]);

  const numberInputs = screen.getAllByRole("spinbutton");

  await userEvent.type(numberInputs[0], "12333");
  expect(numberInputs[0]).toHaveDisplayValue("12333");
},10000);

code for working test:


const ExampleThatWorks = () => {
  const [state, dispatch] = useReducer(reducer, [
    {
      name: "Age",
      show: true,
      filterValueType: "integer",
      value: ["", ""],
      optionType: "between",
      key: "age",
    }
  ]);

  return (
    <div>
      <FilterItem dispatch={dispatch} filter={state[0]} idx={0} />
    </div>
  );
};

it.each(Array(100).fill(null))("ExampleThatWorks", async () => {

  render(<ExampleThatWorks />);

  const numberInputs = screen.getAllByRole("spinbutton");
  await userEvent.type(numberInputs[0], "12333");
  expect(numberInputs[0]).toHaveDisplayValue("12333");
},10000);

Expected behavior

expected to pass all tests on v14

Actual behavior

testThatFails fails in v14, but when using v13.5 it works properly testTahtWorks passes on both versions

User-event version

14.1.1(actually all v14 dists act the same)

Environment

{
  "name": "userevent-react",
  "keywords": [],
  "description": "",
  "private": true,
  "dependencies": {
    "@headlessui/react": "1.7.16",
    "@testing-library/jest-dom": "5.16.3",
    "@testing-library/react": "14.0.0",
    "@testing-library/user-event": "^14.1.1",
    "jest": "29.5.0",
    "react": "18.0.0",
    "react-dom": "18.0.0",
    "react-scripts": "^5.0.1"
  },
  "devDependencies": {
    "husky": "^4.2.3",
    "jest-environment-jsdom": "^29.6.2",
    "lint-staged": "^10.0.8",
    "prettier": "^1.19.1",
    "ts-jest": "^29.1.1"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "format": "prettier --write \"**/*.+(js|json|css|md|mdx|html)\""
  },
  "eslintConfig": {
    "extends": "react-app"
  },
  "browserslist": [
    ">0.2%",
    "not dead",
    "not ie <= 11",
    "not op_mini all"
  ]
}

Additional context

No response

endymion1818 commented 1 year ago

Getting this too with v14.4.3:

My test:

    const inputText = wrapper.getByLabelText('my-label');

    await event.type(inputText, textString);

    expect(inputText.value).toBe(textString)

And the result:

AssertionError: expected 't1t5e rlse tltoenrgs long' to be '15 letters long' // Object.is equality

- Expected
+ Received

- 15 letters long
+ t1t5e rlse tltoenrgs long

Downgrading to v13.5.0 was the only resolution I could find.

Hatlen commented 1 year ago

I'm getting the same behavior as well (in a project with lots of dependencies). I worked around the issue by refactoring to one call to userEvent.type per character that I want to type.

finken2 commented 1 year ago

Getting similar behavior, where it seems to be random how many of the characters in the type() make it into a text box. Also tried keyboard() directly with the same result.

kristiannilsson commented 1 year ago

You are likely not awaiting your changes with waitFor() or findBy. From v14 methods are not synchronous. The test works as intended when awaiting the changes.

Hatlen commented 1 year ago

I'm getting the same behavior as well (in a project with lots of dependencies). I worked around the issue by refactoring to one call to userEvent.type per character that I want to type.

I was able to get await userEvent.type() to work correctly after upgrading @testing-library/dom to version 9.3.3 or 8.19.0 (previously my lock file also had version 9.0.1 and 8.13 (don't know which version is actually used, should be 9.33 since version 8 is a dependency of a dependency).

You are likely not awaiting your changes with waitFor() or findBy. From v14 methods are not synchronous. The test works as intended when awaiting the changes.

This was part of the issue, when I awaited the user.type() calls I got an not wrapped in act error, then I removed the await and I didn't get an error but the calls to type just entered one character. Even when I tried to wait 1s after the call to type there was only one character in the inputs.

finken2 commented 1 year ago

You are likely not awaiting your changes with waitFor() or findBy. From v14 methods are not synchronous. The test works as intended when awaiting the changes.

I am using waitFor, and an await on the user.type:

    const filenameField = getByLabelText('Filename');
    await user.clear(filenameField);
    await user.type(filenameField, '.startswithperiod');

    await waitFor(() => expect(filenameField).toBeInvalid());
    await waitFor(() => expect(filenameField.value).toEqual('.startswithperiod'));

Pretty straightforward, but if you see something wrong there, let me know. In this test, it fails with only part of the string being the filenameField.value, e.g.

  Expected: ".startswithperiod"
  Received: ".starts"
LMatass commented 1 year ago

Same issue here, type is sometimes not being awaited and it randomly fails...

I'm using the following versions:

    "@testing-library/angular": "14.3.0",
    "@testing-library/dom": "9.3.3",
    "@testing-library/jasmine-dom": "1.3.3",
    "@testing-library/user-event": "14.4.3",
    "karma": "6.4.2",

Example of my code:

    await user.type(
          document.querySelector('textarea'),
          'Test example'
        );

    await user.click(
      getByText(
        transloco.instant('action.save_example')
      )
    );

    const [submission] = submitData.calls.mostRecent().args;
    expect(submission.text).toBe('Test example');

Error: Expected 'Test' to be 'Test example'.

tortilaman commented 1 year ago

In case this helps anyone else, I was running into this issue when using onKeyDown as an event handler for a textInput. Switching to using onChange resolved the issue.

it('Renders a text input', async () => {
  const Template = () => {
    const [value, setValue] = useState('')
    // switching from `onKeyDown` to `onChange` here fixed the issue for me
    return <TextInput value={value} onChange={(e) => setValue(e.currentTarget.value)} />
  }
  render(<Template />)

  expect(screen.getByRole('textbox')).toBeInTheDocument()
  await userEvent.type(screen.getByRole('textbox'), 'text content')
  expect(screen.getByRole('textbox')).toHaveValue('text content')
})
LMatass commented 1 year ago

You are likely not awaiting your changes with waitFor() or findBy. From v14 methods are not synchronous. The test works as intended when awaiting the changes.

I am using waitFor, and an await on the user.type:

    const filenameField = getByLabelText('Filename');
    await user.clear(filenameField);
    await user.type(filenameField, '.startswithperiod');

    await waitFor(() => expect(filenameField).toBeInvalid());
    await waitFor(() => expect(filenameField.value).toEqual('.startswithperiod'));

Pretty straightforward, but if you see something wrong there, let me know. In this test, it fails with only part of the string being the filenameField.value, e.g.

  Expected: ".startswithperiod"
  Received: ".starts"

I'm still with the same issue, the days that the machine runs slower, the tests fails more. Did you find any workaround? I tried with setTimeouts, waitFor.... and none of them worked.

haagOS commented 12 months ago

I have tried your example with

it.each(Array(100).fill(null))("ExampleThatFails",
  async () => {
    const user = userEvent.setup({ delay: null }); // <-- added
    render(<ExampleThatFails />);

    // replaced every userEvent with user

and that seems to work. Why? I don't know. But maybe it's helpful.

Sti2nd commented 11 months ago

By now I think it is clear this is a real bug. It is not just people forgetting await. Here is my test that is failing (using vitest):

const { container } = render( <MyComponent /> );
const input = container.querySelector<HTMLInputElement>(
  '#inputfield',
);
const user = userEvent.setup();
await user.type(input, '61857600265');
expect(input.value).toBe('61857600265')
iammerrick commented 11 months ago

Wrapping user.type in my own act call removes the warnings and makes my tests deterministic.

Before:

await user.type(textarea, '@');

After:

 await act(async () => {
          await user.type(textarea, '@');
});

I thought that user.type would take care of act for me, however, in this case it doesn't seem to.

Sti2nd commented 11 months ago

Wrapping in act doesn't fix it for me 🤔. My test fails consistently, though.

"react": "18.2.0", "@testing-library/jest-dom": "6.1.5", "@testing-library/react": "14.1.2", "@testing-library/user-event": "14.5.1", "vitest": "0.34.4"
EmmaB commented 11 months ago

+1. None of the suggestions work for me, upgrading to react 18 and v14.5.1

Meags27 commented 11 months ago

None of the suggestions work for me too. It just says "AssertionError: expected '' to be 't' // Object.is equality"

Trying this

test("Should allow user to enter passwords into the input", async () => {

  const user = userEvent.setup();
  const { getByRole, getByTestId } = render(
    <div>
      <label
        htmlFor="currentPassword"
        className="text-white block mb-2 text-sm font-medium dark:text-white"
      >
        Current Password
      </label>
      <input
        type="password"
        role="textbox"
        name="currentPassword"
        id="currentPassword"
        data-testid="currentPassword"
        required
        autoFocus
        value={""}
        onChange={(event) => vi.fn()}
      />
    </div>,
  );
//also tried getByRole
  const currentPasswordInputElement = getByTestId("currentPassword");

  await user.type(currentPasswordInputElement, "t");

  await waitFor(() =>
    expect((currentPasswordInputElement as HTMLInputElement).value).toBe("t"),
  );

});

package.json (parts that matter)

 "dependencies": { 
    "next": "^14.0.4",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
  },
  "devDependencies": {
    "@testing-library/jest-dom": "^6.1.5",
    "@testing-library/react": "^14.1.2",
    "@testing-library/user-event": "^14.5.1",
    "@types/node": "^20.10.5",
    "@types/react": "^18.2.45",
    "@types/react-dom": "^18.2.18",
    "@vitejs/plugin-react": "^4.2.1",
    "jsdom": "^23.0.1",
    "typescript": "^5.3.3",
    "vitest": "^1.1.0"
  }
}
applesaucesome commented 10 months ago

Still broken :(

daacrx commented 10 months ago

I await it and it doesnt work for me, neither the other suggestions

nguyenduy commented 10 months ago

setting up userEvent first and using await works for me:

it("should be able to type username", async () => {
  const user = userEvent.setup()
  render(<App />);
  const element = screen.getByLabelText("Username") as HTMLInputElement;

  await user.type(element, "Admin");
  expect(element.value).toBe("Admin");
}); 
asfg84 commented 9 months ago

Same issue here, none of the above hints works for me

Solar5503 commented 9 months ago

I have the same issue when i test component that accepts state from the parent component. When I call user.type() just entered one character and so several times,character by character according to the number of character in the test word.

itmarck commented 8 months ago

Same issue, it seems the onChange events are fired properly simulating the typing but when I check the input.value it doesn't change.

UPDATE: I found a solution for my scenario. I was replacing the value property of the input from props on every change, that works on browser, but it seems it causes conflict when testing. I just changed the value for defaultValue in the input element and the error was solved.

MiroslavPetrik commented 8 months ago

I've noticed that it types the text letter by letter, and with number input, it does not adds the letters, but each time overrides it (I have a controlled component, could be that the issue?)

image

So for my test purpose, I will just set single digit number...

my 2 cents

obryckim commented 7 months ago

I am experiencing the same issue as @MiroslavPetrik; When typing, my onChange handler is only receiving the most recent character typed, not the additive string.

omidmogasemi commented 6 months ago

@MiroslavPetrik @obryckim I've found you'll need to await the userEvent.type statement inside of your act block to see the entire string.

AllanTAP commented 1 month ago

Like @omidmogasemi said, adding this await for user.type did the trick. No need of act():

const user = userEvent.setup();

const input = screen.getByTestId('input-search');

user.click(input);
await user.type(input, 'abc');

expect(input).toHaveValue('abc');
tibbe commented 1 month ago

Removing

jest.useFakeTimers();

or adding{delay: null} to userEvent.setup.

Makes more, but not all, tests pass. Perhaps that's a hint to what's going on.

Adding await or act or any combination of the two doesn't work.