Skip to content

Commit b9a3fb2

Browse files
fix: UserEvent longPress on Text component (#1436)
* chore: add tests * fix: support Text longPress, improve pressable Text checks * chore: refactor * refactor: tweaks
1 parent 574205a commit b9a3fb2

File tree

2 files changed

+96
-24
lines changed

2 files changed

+96
-24
lines changed

src/user-event/press/__tests__/press.test.tsx

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,7 @@ describe('userEvent.press with fake timers', () => {
304304
expect(mockOnPress).toHaveBeenCalled();
305305
});
306306

307-
test('works on Text', async () => {
307+
test('press works on Text', async () => {
308308
const { events, logEvent } = createEventLogger();
309309

310310
render(
@@ -317,11 +317,29 @@ describe('userEvent.press with fake timers', () => {
317317
press me
318318
</Text>
319319
);
320-
await userEvent.press(screen.getByText('press me'));
321320

321+
await userEvent.press(screen.getByText('press me'));
322322
expect(getEventsName(events)).toEqual(['pressIn', 'press', 'pressOut']);
323323
});
324324

325+
test('longPress works Text', async () => {
326+
const { events, logEvent } = createEventLogger();
327+
328+
render(
329+
<Text
330+
onPress={logEvent('press')}
331+
onPressIn={logEvent('pressIn')}
332+
onPressOut={logEvent('pressOut')}
333+
onLongPress={logEvent('longPress')}
334+
>
335+
press me
336+
</Text>
337+
);
338+
339+
await userEvent.longPress(screen.getByText('press me'));
340+
expect(getEventsName(events)).toEqual(['pressIn', 'longPress', 'pressOut']);
341+
});
342+
325343
test('doesnt trigger on disabled Text', async () => {
326344
const { events, logEvent } = createEventLogger();
327345

@@ -361,7 +379,7 @@ describe('userEvent.press with fake timers', () => {
361379
expect(events).toEqual([]);
362380
});
363381

364-
test('works on TetInput', async () => {
382+
test('press works on TextInput', async () => {
365383
const { events, logEvent } = createEventLogger();
366384

367385
render(
@@ -371,12 +389,27 @@ describe('userEvent.press with fake timers', () => {
371389
onPressOut={logEvent('pressOut')}
372390
/>
373391
);
392+
374393
await userEvent.press(screen.getByPlaceholderText('email'));
394+
expect(getEventsName(events)).toEqual(['pressIn', 'pressOut']);
395+
});
396+
397+
test('longPress works on TextInput', async () => {
398+
const { events, logEvent } = createEventLogger();
399+
400+
render(
401+
<TextInput
402+
placeholder="email"
403+
onPressIn={logEvent('pressIn')}
404+
onPressOut={logEvent('pressOut')}
405+
/>
406+
);
375407

408+
await userEvent.longPress(screen.getByPlaceholderText('email'));
376409
expect(getEventsName(events)).toEqual(['pressIn', 'pressOut']);
377410
});
378411

379-
test('does not call onPressIn and onPressOut on non editable TetInput', async () => {
412+
test('does not call onPressIn and onPressOut on non editable TextInput', async () => {
380413
const { events, logEvent } = createEventLogger();
381414

382415
render(
@@ -387,11 +420,12 @@ describe('userEvent.press with fake timers', () => {
387420
onPressOut={logEvent('pressOut')}
388421
/>
389422
);
423+
390424
await userEvent.press(screen.getByPlaceholderText('email'));
391425
expect(events).toEqual([]);
392426
});
393427

394-
test('does not call onPressIn and onPressOut on TetInput with pointer events disabled', async () => {
428+
test('does not call onPressIn and onPressOut on TextInput with pointer events disabled', async () => {
395429
const { events, logEvent } = createEventLogger();
396430

397431
render(
@@ -402,6 +436,7 @@ describe('userEvent.press with fake timers', () => {
402436
onPressOut={logEvent('pressOut')}
403437
/>
404438
);
439+
405440
await userEvent.press(screen.getByPlaceholderText('email'));
406441
expect(events).toEqual([]);
407442
});

src/user-event/press/press.ts

Lines changed: 56 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,35 +11,48 @@ import { UserEventConfig, UserEventInstance } from '../setup';
1111
import { dispatchEvent, wait, warnAboutRealTimersIfNeeded } from '../utils';
1212
import { DEFAULT_MIN_PRESS_DURATION } from './constants';
1313

14-
export type PressOptions = {
15-
duration: number;
16-
};
14+
export interface PressOptions {
15+
duration?: number;
16+
}
1717

1818
export async function press(
1919
this: UserEventInstance,
2020
element: ReactTestInstance
2121
): Promise<void> {
22-
await basePress(this.config, element);
22+
await basePress(this.config, element, {
23+
type: 'press',
24+
duration: 0,
25+
});
2326
}
2427

2528
export async function longPress(
2629
this: UserEventInstance,
2730
element: ReactTestInstance,
28-
options: PressOptions = { duration: 500 }
31+
options?: PressOptions
2932
): Promise<void> {
30-
await basePress(this.config, element, options);
33+
await basePress(this.config, element, {
34+
type: 'longPress',
35+
duration: options?.duration ?? 500,
36+
});
37+
}
38+
39+
interface BasePressOptions {
40+
type: 'press' | 'longPress';
41+
duration: number;
3142
}
3243

3344
const basePress = async (
3445
config: UserEventConfig,
3546
element: ReactTestInstance,
36-
options: PressOptions = { duration: 0 }
47+
options: BasePressOptions
3748
): Promise<void> => {
38-
// Text and TextInput components are mocked in React Native preset so the mock
39-
// doesn't implement the pressability class
40-
// Thus we need to call the props directly on the host component
41-
if (isPressableText(element) || isEnabledTextInput(element)) {
42-
await emitBasicPressEvents(config, element, options);
49+
if (isPressableText(element)) {
50+
await emitTextPressEvents(config, element, options);
51+
return;
52+
}
53+
54+
if (isEnabledTextInput(element)) {
55+
await emitTextInputPressEvents(config, element, options);
4356
return;
4457
}
4558

@@ -59,7 +72,7 @@ const basePress = async (
5972
const emitPressablePressEvents = async (
6073
config: UserEventConfig,
6174
element: ReactTestInstance,
62-
options: PressOptions = { duration: 0 }
75+
options: BasePressOptions
6376
) => {
6477
warnAboutRealTimersIfNeeded();
6578

@@ -97,11 +110,18 @@ const isEnabledTouchResponder = (element: ReactTestInstance) => {
97110
};
98111

99112
const isPressableText = (element: ReactTestInstance) => {
113+
const hasPressEventHandler = Boolean(
114+
element.props.onPress ||
115+
element.props.onLongPress ||
116+
element.props.onPressIn ||
117+
element.props.onPressOut
118+
);
119+
100120
return (
101121
isHostText(element) &&
102122
isPointerEventEnabled(element) &&
103123
!element.props.disabled &&
104-
element.props.onPress
124+
hasPressEventHandler
105125
);
106126
};
107127

@@ -114,18 +134,35 @@ const isEnabledTextInput = (element: ReactTestInstance) => {
114134
};
115135

116136
/**
117-
* Dispatches a basic press event sequence on non-Pressable component,
118-
* e.g. Text or TextInput.
137+
* Dispatches a press event sequence for Text.
138+
*/
139+
async function emitTextPressEvents(
140+
config: UserEventConfig,
141+
element: ReactTestInstance,
142+
options: BasePressOptions
143+
) {
144+
await wait(config);
145+
dispatchEvent(element, 'pressIn', EventBuilder.Common.touch());
146+
147+
// Emit either `press` or `longPress`.
148+
dispatchEvent(element, options.type, EventBuilder.Common.touch());
149+
150+
await wait(config, options.duration);
151+
dispatchEvent(element, 'pressOut', EventBuilder.Common.touch());
152+
}
153+
154+
/**
155+
* Dispatches a press event sequence for TextInput.
119156
*/
120-
async function emitBasicPressEvents(
157+
async function emitTextInputPressEvents(
121158
config: UserEventConfig,
122159
element: ReactTestInstance,
123-
options: PressOptions = { duration: 0 }
160+
options: BasePressOptions
124161
) {
125162
await wait(config);
126163
dispatchEvent(element, 'pressIn', EventBuilder.Common.touch());
127164

128-
dispatchEvent(element, 'press', EventBuilder.Common.touch());
165+
// Note: TextInput does not have `onPress`/`onLongPress` props.
129166

130167
await wait(config, options.duration);
131168
dispatchEvent(element, 'pressOut', EventBuilder.Common.touch());

0 commit comments

Comments
 (0)