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' ;
27import { act } from 'react' ;
38import { Keyboard , View } from 'react-native' ;
49
@@ -13,13 +18,35 @@ import {
1318import { GestureDetector } from '../v3/detectors' ;
1419import { useSimultaneousGestures } from '../v3/hooks' ;
1520import { usePanGesture , useTapGesture } from '../v3/hooks/gestures' ;
16- import type { SingleGesture } from '../v3/types' ;
1721
1822const 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+
2350describe ( '[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} ) ;
0 commit comments