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
Make clear
method fire onChange
handlers
#286
Conversation
src/index.js
Outdated
@@ -1,7 +1,7 @@ | |||
import { fireEvent } from "@testing-library/dom"; | |||
|
|||
function wait(time) { | |||
return new Promise(function (resolve) { | |||
return new Promise(function(resolve) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With all of these irrelevant formatting changes, it's really hard to grok the real diff.
Is this caused by the pre-commit
hook, or is this a wonky editor plugin?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh how embarrassing. Let me go through and figure out why I have whitespace changes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Circling back this is totally the pre-commit hook. Let me separate out the actual changes I proposed from the prettier changes.
@evocateur I separated out a commit that has the prettier updates applied by the pre-commit hook to the files I was about to change. Hopefully this makes reviewing commit-by-commit useful now. Sorry for not doing this to begin with. |
src/index.js
Outdated
|
||
if (!element.readOnly) { | ||
fireEvent.input(element, { | ||
inputType: "deleteContentBackward" | ||
}); | ||
element.value = ""; // when we add special keys to API, will need to respect selected range | ||
|
||
setElementValue(element, ""); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmmm... Is there a reason we can't just use fireEvent.change(element, {target: {value}})
? The setElementValue
function is basically what fireEvent.change
does: https://github.com/testing-library/dom-testing-library/blob/8846eaf20972f8e41ed11f278948ac38a692c3f1/src/events.js#L81-L96
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great question. I tried to explain this in the PR description but I obviously struggled and didn't get the point across. Also, I hadn't yet seen the setNativeValue
implementation. Thanks for the patience of letting me try again.
My first attempt tried to use fireEvent.change(element, { target: { value: '' }})
without realizing that they have a very similar function to the function that you linked to (https://github.com/testing-library/dom-testing-library/blob/8846eaf20972f8e41ed11f278948ac38a692c3f1/src/events.js#L81-L96) above #286 (comment).
If I replace this setElementValue( ... )
with the fireEvent.change
, the problem is that React's internal representation of state will fall out of sync with the actual input.value
because React internally caches the input
value https://github.com/facebook/react/blob/8b3b5c35245b3aa0b2802b51856fe9dad5bcb671/packages/react-dom/src/client/inputValueTracking.js#L51-L104. When we set the native .value
, the React value
setter isn't called, causing React to have a different internal value than the input
itself. Fortunately the test additions I added will catch this. Changing the value will show that the value is not maintained.
This is the part of my abstraction that differs from the above linked: https://github.com/testing-library/user-event/pull/286/files#diff-1fdf421c05c1140f6d71444ea2b27638R144-R148, which calls the prototype overridden value setter after the event is dispatched.
However, there's a way simpler solution that I didn't see and is now obvious after looking at the fireEvent.change
and these new tests. If I just call input.value = ""
after calling fireEvent.change
then everything works as we'd expect! I'll make this change.
Codecov Report
@@ Coverage Diff @@
## master #286 +/- ##
=======================================
Coverage 98.75% 98.76%
=======================================
Files 1 1
Lines 161 162 +1
Branches 46 46
=======================================
+ Hits 159 160 +1
Misses 2 2
Continue to review full report at Codecov.
|
It looks like your |
I wish I had a good explanation of what I was doing wrong @vadimshvetsov. I checked out master and ran prettier after an |
__tests__/react/clear.js
Outdated
@@ -72,14 +78,18 @@ describe("userEvent.clear", () => { | |||
it.each(["input", "textarea"])( | |||
`should clear when <%s> is of type="${type}"`, | |||
(inputType) => { | |||
const onChange = jest.fn().mockImplementation((event) => { | |||
expect(input).toHaveProperty("value", ""); | |||
expect(event.target).toHaveProperty("value", ""); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm wondering is there a specific case why assertions placed inside a mocked implementation instead of test flow? Isn't input
strict equal event.target
here and already checked in the end of the test (on line 100)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, input
is strict equal to event.target
.
I ran into an edge case with the implementation that I've since scrapped. The sequence was events was as follows:
- call
input
's prototypevalue
setter - fired change event (wasn't using
fireEvent.change
) - use
input.value
setter, which is overridden by React
In this case, when the event handler was fired, input.value
would still be the previous value because input.value
's getter is also overridden by React and uses an internal copy of the state. If we didn't use input.value =
to assign the value, then the internal state would not be updated. This is super subtle and I'm fine getting rid of it, but this does add a little bit of confidence. I'll add a comment in the meantime and remove if it that's what you want.
I super duper apologize here, but I just made a bunch of changes to the project to clean things up a bit and now there are some significant merge conflicts. Rather than fix the merge conflicts, it may be easier to create the PR anew. I hope you accept my apology. I know that's totally not fun. Let me know if you'd rather I just do this myself 😬 Sorry! |
Thanks for the offer @kentcdodds ; merge conflicts resolved, rebased, and pushed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Super, thank you very much!
🎉 This PR is included in version 10.3.4 🎉 The release is available on:
Your semantic-release bot 📦🚀 |
Resolves issue #268
Per the issue comments, I believed that the change would only require calling
fireEvent.change
in theclear
method; but that turned out not to be the case.React deduplicates events that it fires so that it doesn't emit multiple
onChange
events with the same value. Usingelement.value = ""
causes React to expect a change event to have been fired, so callingfireEvent.change
has will be considered a duplicate event and no event will be fired. I added tests to verify the expected behavior (they fail before the second commit, as one would expect).The solution I landed on, and I'm super open to feedback, was to follow the logic I found in stackoverflow. I abstracted a
setValue
method that will call the native value setter, fire the change event, and then call the prototype override value setter if one exists. I tried a few other solutions that had subtle issues.The easier solution was to call the
onChange
event with thetarget: { value: '' }
and then call.value = ""
. This called theonChange
handler, theonChange
handler'sevent.target.value
was correct, and afterclear
finishes, the.value
would also be correct. However, if you check the original element'svalue
inside of theonChange
handler, it will be wrong because it'll be using React's prototype override; that override has an internal cache of the inputvalue
, which will be wrong. This is super subtle, but can be damning. I added a check to the testsonChange
methods to make sure thatevent.target.value
andinput.value
are correct whenonChange
is called.Lastly, I added checks to the existing tests to make sure
onChange
is called when expected.I expected this to be a much simpler fix; but this is the best I could come up with. I'd love to get some feedback!