-
Notifications
You must be signed in to change notification settings - Fork 273
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
feat: userEvent.press
#1386
feat: userEvent.press
#1386
Conversation
@pierrezimmermannbam, @MattAgn I'm really happy that you are working on this. From reading the tests it seems that the current version of |
src/userEvent/touchEvents.ts
Outdated
}; | ||
|
||
export type PressOptions = { | ||
pressDuration: number; |
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.
Q: What are the use cases for pressDuration
prop?
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.
The main use case is to simulate a long press. If press duration is greater or equal than the long press duration offset then it will call onLongPress and not onPress. You can look at the test cases for pressDuration, it should make things clearer
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, exposing pressDuration
is looking like quite a low-level implementation detail. To use it properly it would require user to know the default press duration of Pressable
. I think a simpler API would be to expose two events:
press: () => void,
longPress: ({ duration = 500 }) => void
As for regular presses, which are far more popular than long press, the duration does not matter that much, and for longPresses
most of the users would probably stick with the default pressDuration
value. If they tweaked it, then they would also have option to apply the theaked value in test.
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 don't agree with press duration being an implementation detail, it's directly related to the user action. In my opinion longPress will be interpreted like it triggers my onLongPress prop so I think it's more linked to implementation details. So I think it's a good addition to be able to change the duration, having an additional longPress api with a different default may be a good idea I'm not too sure on this
This is based on the Pressability implementation in React Native so it should work with every components that is pressable, I'll make sure and add test cases for those |
re Pressability: I am really curious if this will work with other components out of the box. That would be the lucky option, as we would handle all types of components with single flow. In the unlucky case that they need a separate implementation, I think we should still support them, e.g. by checking the element type |
The initial implementation works with every touchable/pressable but it doesn't work this Text or TextInput. The reason for that is that the implementation of Text is mocked in the React Native preset so it's just a component with name Text that has the same props than the composite one. It's not the same for pressables because the View is mocked but not pressable directly. It would work if only the native part of the Text component was mocked and I don't know why it's done this way, I'm gonna ask on the React Native repo if they know and maybe open a PR but that would be a huge breaking change and I don't know yet what impacts it could have so I'd need to try it on my projects to see how tests behave. In the meantime we can have a special treatment for Text and TextInput as implemented in this pr but it's a shame |
For the real timers :
There are some issues right now with the tests:
I think the solution would be to have a jest config with two projects, one with fake timers enabled and another with real timers. This way every test would be run with both setups without having to duplicate them. Also we could have a command that would run only tests with fake or real timers so that while developing we're still able to have fast tests and run tests with real timers less frequently. If you think For longPress, I have reworked the API as @mdjastrzebski suggested:
For Text and TextInput I have done a workaround for now, I still need to ask on the React Native repo if they want to change or not the way those are mocked but it will take time and we'll still need support for earlier version of RN either way so the workaround will stay for a while |
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.
Awesome job @pierrezimmermannbam I really like the direction where this is going. Supporting both advanced Pressables
/etc, as well as simpler Text
/etc is very useful.
The current version of the code, seems to use older User Event API (userEvent.press()
), the latest, v14, User Event API is slightly different:
const user = userEvent.setup();
user.press()
While there are some pros of using the older version of the API, e.g. it's simpler, I think the way forward is to use the latest version of the API, in order to follow RTL's best practices and provide high degree of compatibility. It also has some useful features as configuring the User Event default params, etc.
I've been recently working on implementing User Event type()
API, and I've got the userEvent.setup()
, etc already implemented in #1396. Perhaps, I should extract that + some reusable utils like wait
to a separate PR that could get reviewed and merged quickly so that we would be working on the same base API.
I am really happy how fast is the progress for User Event moving. Having press()
and type()
will cover the majority of the basic use cases.
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've extracted the common User Event code in #1405. @pierrezimmermannbam pls take a look so that we have a common base API to add proper functions.
I've merged #1405 to main, @pierrezimmermannbam pls rebase 🚀 |
src/user-event/press/press.ts
Outdated
} | ||
await wait(config, options.duration); | ||
if (onPressOut) { | ||
if (DEFAULT_MIN_PRESS_DURATION - options.duration > 0) { |
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.
Is that 130 ms happening experimentally in an actual RN app for Text
/TextInput
? We should consider adding it artificially if this is the case.
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 it happens also for Text
and TextInput
because they use the same implementation of Pressability
. Not sure I understand your comment though, we're already adding it artificially here
96a1d10
to
c49ae70
Compare
Co-authored-by: Maciej Jastrzebski <mdjastrzebski@gmail.com>
c49ae70
to
b78b0c3
Compare
@mdjastrzebski I think I addressed all your comments, could you please review when you find the time? |
src/user-event/press/press.ts
Outdated
await wait(config, DEFAULT_MIN_PRESS_DURATION - options.duration); | ||
} | ||
onPressOut(EventBuilder.Common.touch()); | ||
} |
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.
[high] Looks like we need an early return here, otherwise it will continue propagation to the host parent.
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 you're right, good catch!
dispatchConfig: { registrationName: 'onResponderRelease' }, | ||
}); | ||
|
||
if (DEFAULT_MIN_PRESS_DURATION - options.duration > 0) { |
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.
[mid] q: why do we need to do additional waiting to reach at least min press duration limit here and in the Text/TextInput block above?
I've found this line in RN source. This seems to be an un-documented internal implementation detail of RN. I wonder if:
- If it's already being applied so that we would not need to add manual wait
- If not is it worth relying on such implementation details.
@pierrezimmermannbam wdyt?
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.
What happens in RN code when the press is finished is:
- the delay for the pressOut is computed: it's equal to
minimum press duration
-press duration
(see source code - if the delay is > 0 then pressOut is programmed through a setTimeout with the delay computed at the above step (see source code)
In the implementation for Pressable
, we wait because if the press duration is shorter than the minimum press duration, then onPressOut
has not been called yet. This is problematic for two reasons:
- users would expect
onPressOut
to have been called - it results in an act warning
This is why we wait in this case and in the second scenario, we could just call onPressOut
without waiting, it would just be less realistic. The upside of not waiting is that tests that don't use fake timers would be faster
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.
Could you confirm that I get that right: the Pressable
is actually delaying the action, so that we wait at least 130 ms, event when running under tests. If we do not wait to that 130 ms, then the event handler will be called outside of async act
causing async act
warning? So we are actually waiting to prevent async act
warning from happening.
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 that's correct. We're waiting both to prevent act warning but also to have onPressOut
called synchronously so user doesn't have to wait for it
|
||
const mockConsoleWarn = jest.spyOn(console, 'warn').mockImplementation(); | ||
|
||
test('logs a warning about usign real timers with user event', () => { |
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.
Testing this method seems redundant. In order to achieve code coverage it would be better to check if by calling userEvent.press
with real times. That way would would at least check that it's being invoked from press()
function. Here we are just confirming the logged message.
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 I agree it's far from ideal, the reason I tested this separately is because I'm mocking here console warn to prevent warnings to appear in console when running tests and I didn't want to mock console.warn for every test in the userEvent.press tests. I did it a bit differently by putting the other tests in separate describe to have different mock just for them within the test file
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.
Awesome work @pierrezimmermannbam! Also thank you for patience with that, as due to large amount of work and holiday time I was not very responsive lately.
I wonder if we should simulate the min 130 ms press delay by ourselves. First of all it looks like it might already be applied for us by Pressability code in RN (could you check that?). Second of all, it might create additional hurdle for adoption of userEvent.press
as users would be nagged to enable fake timers with warnings even for default zero duration presses. Personally, I would rather have easier adoption for the less advanced users rather than increased reality due to 130 ms delay.
@thymikee @AugustinLF @MattAgn perhaps you can give your outsider perspective on how you see that trade off.
092c51a
to
d1548c5
Compare
Codecov ReportPatch coverage:
Additional details and impacted files@@ Coverage Diff @@
## main #1386 +/- ##
==========================================
+ Coverage 96.88% 97.02% +0.13%
==========================================
Files 65 68 +3
Lines 3693 3862 +169
Branches 538 568 +30
==========================================
+ Hits 3578 3747 +169
Misses 115 115
☔ View full report in Codecov by Sentry. |
Regarding the 130ms delay, it's not quite the same when pressing Text/TextInput and other Pressable like: For Pressables, we don't directly call the For Text/TextInput it's a bit different. We call directly However I think that the main use cases for |
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.
Time to merge it! 🚀
This PR has been released in v12.1.3 🚢 |
Summary
With @MattAgn we've been working on the userEvent feature and this is a first proposition for an implementation of userEvent.press. It's not perfect yet but I'm pretty excited about it and it can be a support for discussion.
Unlike fireEvent, this uses only the host components. This has several benefits:
Overall I feel this works really well and provides tons of advantages. It has also some drawbacks though:
Regarding the option pressDuration, I was thinking initially of making a userEvent.longPress API but since the delayLongPress can be modified by the user, it would have required to look at a composite components props, so I figured it would make more sense to think in terms of press duration rather than press / longPress.
For now this doesn't handle the touch events (for instance onTouchStart prop would not be called), this handles only press events because pressability and gestures are handled by different classes in React Native. It can very well be added later though.
If you think it's promising then in a second time we'll need to start discussing the strategy for release and user adoption.
Test plan
I added a bunch of test cases showcasing what props are called and how the pressDuration option works.
Curious to have your opinion on this @mdjastrzebski @MattAgn @thymikee @AugustinLF