testing-library / user-event

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

userEvent.type() does not enter the desired text into a number input if that input has a defaultValue attribute but does if that attribute is removed. #1222

Closed edickins closed 3 weeks ago

edickins commented 1 month ago

Reproduction example

https://codesandbox.io/p/devbox/2x7q82?file=%2Fsrc%2Fpages%2Fentry%2FScoopOption.tsx%3A25%2C25

Prerequisites

you can run all tests using 'npm test' in the console and you will see an error from the totalUpdate.test.tsx test file where the subTotal value does not match what is expected.

Error: expect(element).toHaveTextContent()

Expected element to have text content:
  2.00
Received:
  Scoops total: $NaN

Expected behavior

when you run the tests you will see that it fails in totalsTest.test.tsx where the subTotal text content should be $2.00 after the userEvent.clear() and userEvent.type() functions have run.

The subtotal should include the text "$2.00" because the chocolate HTMLInput has had the text value updated by userEvent.type() and this change is handled, the value is read and passed to a Context, and the subtotal which is updated is tested.

Actual behavior

When the ScoopOption.tsx component (the one that the test is setting values in) has a defaultValue={0} attribute on the HTMLInput, the subTotal text content is "$NaN" because when theScoopOption.tsx reads the e.target.value in the onChangehandler for the input after userEvent.type() has changed the value to "1".

The HTMLInput has an empty string as a value when it should have a value of "1".

This value from the input is read in an onChangehandler and is sent to the ContextProvider, which updates the state in the Context, and so in the test when the subtotal is asserted to be "$2,00" it is in fact "$NaN".

in other words, userEvent.type() is not putting a value of "1" into the HTMLInput it is leaving it as an empty string, and that value is sent to the Context and displayed as $NaN. This only happens when the HTMLInput has a defaultValue={0} attribute.

You can make the test pass by commenting this line out in ScoopTopping.tsx

User-event version

14.5.1

Environment

Testing Library framework:

JS framework:

I have provided a link to a codesandbox replication of this code including the tests

Test environment:

DOM implementation:

Additional context

I tried a number of things to change the code so that for example the ScoopOption input was a controlled component and instead of having a defaultValue attribute it had a value attribute that was held in state and set that way, it did not fix it.

All I can say is that it looks like it is something to do with what does/doesn't trigger a re-render in the ScoopOption component - the userEvent.clear() is definitely working - the input changes from a defaultValue of 0 to an empty string value. The userEvent.type() method seems not to enter the text and get the component re-rendering to accept that text change if the input has a defaultValue attribute.

edickins commented 3 weeks ago

It might be interesting to add that I got the test working using fireEvent instead.

so instead of:

await user.clear(chocolateInput);
await user.type(chocolateInput, '1');

I used: fireEvent.change(chocolateInput, {target:{value:1}})

the difference being that fireEvent fires a single event whereas the purpose of UserEvent as I understand it is that it fires all of the events on the component that would be triggered if the element was in a webpage and a user clicked on it to enter text (onFocus, onChange, onBlue etc)

I also didn't make it clear that this example I have provided (which is from a course on using testing-library for TDD) was originally in JavaScript where UserEvent worked fine. I decided to re-do the course using TypeScript so I could get TDD experience for TypeScript and for whatever reason TypeScript is the main thing that has changed here. Everything else, the ContextProvider, the page and the test are all doing the same thing in the same way.