Skip to content

Commit 6efe98c

Browse files
committed
Use the native Touchable implementation on all native platforms
1 parent fe7e402 commit 6efe98c

5 files changed

Lines changed: 422 additions & 446 deletions

File tree

packages/react-native-gesture-handler/src/__tests__/api_v3.test.tsx

Lines changed: 58 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { render, renderHook } from '@testing-library/react-native';
1+
import {
2+
fireEvent,
3+
render,
4+
renderHook,
5+
screen,
6+
} from '@testing-library/react-native';
27
import { act } from 'react';
38
import { Keyboard, View } from 'react-native';
49

@@ -13,13 +18,35 @@ import {
1318
import { GestureDetector } from '../v3/detectors';
1419
import { useSimultaneousGestures } from '../v3/hooks';
1520
import { usePanGesture, useTapGesture } from '../v3/hooks/gestures';
16-
import type { SingleGesture } from '../v3/types';
1721

1822
const flushImmediate = () =>
1923
new Promise((resolve) => {
2024
setImmediate(() => resolve(undefined));
2125
});
2226

27+
// The Touchable press state machine runs on the native side — the JS layer
28+
// receives the resulting press events directly from the button.
29+
const buttonEvent = (pointerInside = true) => ({
30+
nativeEvent: {
31+
pointerInside,
32+
x: 0,
33+
y: 0,
34+
absoluteX: 0,
35+
absoluteY: 0,
36+
numberOfPointers: 1,
37+
pointerType: 0,
38+
},
39+
});
40+
41+
// Mirrors the event sequence the native side dispatches for a successful tap.
42+
const fireNativeTap = (testID: string) => {
43+
const button = screen.getByTestId(testID);
44+
fireEvent(button, 'pressIn', buttonEvent());
45+
fireEvent(button, 'pressOut', buttonEvent());
46+
fireEvent(button, 'press', buttonEvent());
47+
fireEvent(button, 'interactionFinished', buttonEvent());
48+
};
49+
2350
describe('[API v3] Hooks', () => {
2451
test('Pan gesture', () => {
2552
const onBegin = jest.fn();
@@ -384,15 +411,6 @@ describe('[API v3] Components', () => {
384411
keyboardShouldPersistTaps,
385412
});
386413

387-
const fireTap = (testID: string) =>
388-
act(() => {
389-
fireGestureHandler(getByGestureTestId(testID), [
390-
{ state: State.BEGAN },
391-
{ state: State.ACTIVE },
392-
{ state: State.END },
393-
]);
394-
});
395-
396414
test('isKeyboardDismissingTap is true only in never mode while the keyboard is visible', async () => {
397415
const addListenerSpy = jest.spyOn(Keyboard, 'addListener');
398416

@@ -458,18 +476,18 @@ describe('[API v3] Components', () => {
458476
await act(flushImmediate);
459477
showKeyboard(addListenerSpy);
460478

461-
// Includes a move (second ACTIVE) so the onUpdate path is exercised too.
462-
act(() => {
463-
fireGestureHandler(getByGestureTestId('touchable'), [
464-
{ state: State.BEGAN },
465-
{ state: State.ACTIVE },
466-
{ state: State.ACTIVE },
467-
{ state: State.END },
468-
]);
469-
});
479+
// Includes a re-entry PressIn (finger dragged out and back in) so the
480+
// capture-once verdict path is exercised too.
481+
const button = screen.getByTestId('touchable');
482+
fireEvent(button, 'pressIn', buttonEvent());
483+
fireEvent(button, 'pressOut', buttonEvent(false));
484+
fireEvent(button, 'pressIn', buttonEvent());
485+
fireEvent(button, 'pressOut', buttonEvent());
486+
fireEvent(button, 'press', buttonEvent());
487+
fireEvent(button, 'interactionFinished', buttonEvent());
470488

471489
// The whole interaction is swallowed - not just onPress, but the press-in/
472-
// out side effects too (incl. via onUpdate as the finger moves).
490+
// out side effects too (incl. the re-entry as the finger moves).
473491
expect(onPress).not.toHaveBeenCalled();
474492
expect(onPressIn).not.toHaveBeenCalled();
475493
expect(onPressOut).not.toHaveBeenCalled();
@@ -488,7 +506,7 @@ describe('[API v3] Components', () => {
488506
);
489507
await act(flushImmediate);
490508

491-
fireTap('touchable');
509+
fireNativeTap('touchable');
492510

493511
expect(onPress).toHaveBeenCalledTimes(1);
494512
});
@@ -509,7 +527,7 @@ describe('[API v3] Components', () => {
509527
await act(flushImmediate);
510528
showKeyboard(addListenerSpy);
511529

512-
fireTap('touchable');
530+
fireNativeTap('touchable');
513531

514532
expect(onPress).toHaveBeenCalledTimes(1);
515533
addListenerSpy.mockRestore();
@@ -528,15 +546,7 @@ describe('[API v3] Components', () => {
528546
);
529547

530548
render(<Example />);
531-
const gesture = getByGestureTestId('touchable');
532-
533-
act(() => {
534-
fireGestureHandler(gesture, [
535-
{ oldState: State.UNDETERMINED, state: State.BEGAN },
536-
{ oldState: State.BEGAN, state: State.ACTIVE },
537-
{ oldState: State.ACTIVE, state: State.END },
538-
]);
539-
});
549+
fireNativeTap('touchable');
540550

541551
expect(pressFn).toHaveBeenCalledTimes(1);
542552
});
@@ -551,22 +561,17 @@ describe('[API v3] Components', () => {
551561
);
552562

553563
render(<Example />);
554-
const gesture = getByGestureTestId('touchable');
555564

556-
act(() => {
557-
fireGestureHandler(gesture, [
558-
{ oldState: State.UNDETERMINED, state: State.BEGAN },
559-
{ oldState: State.BEGAN, state: State.ACTIVE },
560-
{ oldState: State.ACTIVE, state: State.FAILED },
561-
]);
562-
});
565+
// A cancelled interaction ends without a Press event on the native side.
566+
const button = screen.getByTestId('touchable');
567+
fireEvent(button, 'pressIn', buttonEvent());
568+
fireEvent(button, 'pressOut', buttonEvent(false));
569+
fireEvent(button, 'interactionFinished', buttonEvent(false));
563570

564571
expect(pressFn).not.toHaveBeenCalled();
565572
});
566573

567-
test('calls onLongPress after delayLongPress and suppresses onPress', () => {
568-
jest.useFakeTimers();
569-
574+
test('forwards onLongPress and requests the native long-press timer', () => {
570575
const pressFn = jest.fn();
571576
const longPressFn = jest.fn();
572577
const DELAY = 800;
@@ -584,59 +589,20 @@ describe('[API v3] Components', () => {
584589

585590
render(<Example />);
586591

587-
const gesture = getByGestureTestId('touchable') as SingleGesture<
588-
any,
589-
any,
590-
any
591-
>;
592-
const { jsEventHandler } = gesture.detectorCallbacks;
592+
// The long-press timer runs on the native side; passing a callback flips
593+
// the props that arm it.
594+
const button = screen.getByTestId('touchable');
595+
expect(button.props.hasLongPressHandler).toBe(true);
596+
expect(button.props.longPressDuration).toBe(DELAY);
593597

594-
// Fire BEGAN — long press timer starts here
595-
act(() => {
596-
jsEventHandler?.({
597-
oldState: State.UNDETERMINED,
598-
state: State.BEGAN,
599-
handlerTag: gesture.handlerTag,
600-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
601-
handlerData: { pointerInside: true, numberOfPointers: 1 } as any,
602-
});
603-
});
604-
605-
// Fire ACTIVE
606-
act(() => {
607-
jsEventHandler?.({
608-
oldState: State.BEGAN,
609-
state: State.ACTIVE,
610-
handlerTag: gesture.handlerTag,
611-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
612-
handlerData: { pointerInside: true, numberOfPointers: 1 } as any,
613-
});
614-
});
615-
616-
expect(longPressFn).not.toHaveBeenCalled();
617-
618-
// Advance fake timers past delayLongPress
619-
act(() => {
620-
jest.advanceTimersByTime(DELAY);
621-
});
598+
// A long press ends without a Press event on the native side.
599+
fireEvent(button, 'pressIn', buttonEvent());
600+
fireEvent(button, 'longPress', buttonEvent());
601+
fireEvent(button, 'pressOut', buttonEvent());
602+
fireEvent(button, 'interactionFinished', buttonEvent());
622603

623604
expect(longPressFn).toHaveBeenCalledTimes(1);
624605
expect(pressFn).not.toHaveBeenCalled();
625-
626-
// Fire END — onPress should be suppressed because long press was detected
627-
act(() => {
628-
jsEventHandler?.({
629-
oldState: State.ACTIVE,
630-
state: State.END,
631-
handlerTag: gesture.handlerTag,
632-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
633-
handlerData: { pointerInside: true, numberOfPointers: 1 } as any,
634-
});
635-
});
636-
637-
expect(pressFn).not.toHaveBeenCalled();
638-
639-
jest.useRealTimers();
640606
});
641607
});
642608
});

packages/react-native-gesture-handler/src/__tests__/mocks.test.tsx

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
import { render } from '@testing-library/react-native';
2-
import React, { act } from 'react';
1+
import { fireEvent, render, screen } from '@testing-library/react-native';
2+
import React from 'react';
33
import { Text } from 'react-native';
44

55
import GestureHandlerRootView from '../components/GestureHandlerRootView';
6-
import { fireGestureHandler, getByGestureTestId } from '../jestUtils';
76
import {
87
LegacyBaseButton,
98
LegacyBorderlessButton,
@@ -25,7 +24,6 @@ import {
2524
TouchableOpacity,
2625
TouchableWithoutFeedback,
2726
} from '../mocks/Touchables';
28-
import { State } from '../State';
2927
import { Touchable } from '../v3/components';
3028

3129
describe('Jest mocks – legacy components render without crashing', () => {
@@ -128,14 +126,18 @@ test('Trigger Touchable press', () => {
128126
</GestureHandlerRootView>
129127
);
130128

131-
const gesture = getByGestureTestId('touchable');
132-
133-
act(() => {
134-
fireGestureHandler(gesture, [
135-
{ oldState: State.UNDETERMINED, state: State.BEGAN },
136-
{ oldState: State.BEGAN, state: State.ACTIVE },
137-
{ oldState: State.ACTIVE, state: State.END },
138-
]);
129+
// The press state machine runs on the native side — the JS layer receives
130+
// the resulting press events directly from the button.
131+
fireEvent(screen.getByTestId('touchable'), 'press', {
132+
nativeEvent: {
133+
pointerInside: true,
134+
x: 0,
135+
y: 0,
136+
absoluteX: 0,
137+
absoluteY: 0,
138+
numberOfPointers: 1,
139+
pointerType: 0,
140+
},
139141
});
140142

141143
expect(onPress).toHaveBeenCalled();

0 commit comments

Comments
 (0)