diff --git a/.github/workflows/check-relations-traversal-algorithm.yml b/.github/workflows/rngh-api-v3.yml similarity index 83% rename from .github/workflows/check-relations-traversal-algorithm.yml rename to .github/workflows/rngh-api-v3.yml index e15de660ac..12b208aeaf 100644 --- a/.github/workflows/check-relations-traversal-algorithm.yml +++ b/.github/workflows/rngh-api-v3.yml @@ -1,10 +1,11 @@ -name: Test relations traversal algorithm +name: Test Gesture Handler 3 API on: pull_request: paths: - packages/react-native-gesture-handler/src/v3/** - packages/react-native-gesture-handler/src/__tests__/RelationsTraversal.test.tsx + - packages/react-native-gesture-handler/src/__tests__/API_V3.test.tsx push: branches: - main @@ -34,4 +35,4 @@ jobs: - name: Run tests working-directory: packages/react-native-gesture-handler - run: yarn test RelationsTraversal + run: yarn test RelationsTraversal API_V3 diff --git a/packages/react-native-gesture-handler/src/__tests__/api_v3.test.tsx b/packages/react-native-gesture-handler/src/__tests__/api_v3.test.tsx new file mode 100644 index 0000000000..f3f8758f0c --- /dev/null +++ b/packages/react-native-gesture-handler/src/__tests__/api_v3.test.tsx @@ -0,0 +1,60 @@ +import { usePanGesture } from '../v3/hooks/gestures'; +import { render, renderHook } from '@testing-library/react-native'; +import { fireGestureHandler, getByGestureTestId } from '../jestUtils'; +import { State } from '../State'; +import GestureHandlerRootView from '../components/GestureHandlerRootView'; +import { RectButton } from '../v3/components'; +import { act } from 'react'; + +describe('[API v3] Hooks', () => { + test('Pan gesture', () => { + const onBegin = jest.fn(); + const onStart = jest.fn(); + + const panGesture = renderHook(() => + usePanGesture({ + disableReanimated: true, + onBegin: (e) => onBegin(e), + onActivate: (e) => onStart(e), + }) + ).result.current; + + fireGestureHandler(panGesture, [ + { oldState: State.UNDETERMINED, state: State.BEGAN }, + { oldState: State.BEGAN, state: State.ACTIVE }, + { oldState: State.ACTIVE, state: State.ACTIVE }, + { oldState: State.ACTIVE, state: State.END }, + ]); + + expect(onBegin).toHaveBeenCalledTimes(1); + expect(onStart).toHaveBeenCalledTimes(1); + }); +}); + +describe('[API v3] Components', () => { + test('Rect Button', () => { + const pressFn = jest.fn(); + + const RectButtonExample = () => { + return ( + + + + ); + }; + + render(); + + const nativeGesture = getByGestureTestId('btn'); + + act(() => { + fireGestureHandler(nativeGesture, [ + { oldState: State.UNDETERMINED, state: State.BEGAN }, + { oldState: State.BEGAN, state: State.ACTIVE }, + { oldState: State.ACTIVE, state: State.END }, + ]); + }); + + expect(pressFn).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/react-native-gesture-handler/src/handlers/handlersRegistry.ts b/packages/react-native-gesture-handler/src/handlers/handlersRegistry.ts index b60fc2d79d..0996d75075 100644 --- a/packages/react-native-gesture-handler/src/handlers/handlersRegistry.ts +++ b/packages/react-native-gesture-handler/src/handlers/handlersRegistry.ts @@ -1,12 +1,37 @@ import { isTestEnv } from '../utils'; import { GestureType } from './gestures/gesture'; import { GestureEvent, HandlerStateChangeEvent } from './gestureHandlerCommon'; +import { SingleGesture } from '../v3/types'; export const handlerIDToTag: Record = {}; + +// There were attempts to create types that merge possible HandlerData and Config, +// but ts was not able to infer them properly in many cases, so we use any here. +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const hookGestures = new Map>(); const gestures = new Map(); const oldHandlers = new Map(); const testIDs = new Map(); +export function registerGesture( + handlerTag: number, + gesture: SingleGesture +) { + if (isTestEnv() && gesture.config.testID) { + hookGestures.set(handlerTag, gesture); + testIDs.set(gesture.config.testID, handlerTag); + } +} + +export function unregisterGesture(handlerTag: number) { + const gesture = hookGestures.get(handlerTag); + + if (gesture && isTestEnv() && gesture.config.testID) { + testIDs.delete(gesture.config.testID); + hookGestures.delete(handlerTag); + } +} + export function registerHandler( handlerTag: number, handler: GestureType, @@ -40,6 +65,10 @@ export function findHandler(handlerTag: number) { return gestures.get(handlerTag); } +export function findGesture(handlerTag: number) { + return hookGestures.get(handlerTag); +} + export function findOldGestureHandler(handlerTag: number) { return oldHandlers.get(handlerTag); } @@ -47,7 +76,7 @@ export function findOldGestureHandler(handlerTag: number) { export function findHandlerByTestID(testID: string) { const handlerTag = testIDs.get(testID); if (handlerTag !== undefined) { - return findHandler(handlerTag) ?? null; + return findHandler(handlerTag) ?? findGesture(handlerTag) ?? null; } return null; } diff --git a/packages/react-native-gesture-handler/src/jestUtils/jestUtils.ts b/packages/react-native-gesture-handler/src/jestUtils/jestUtils.ts index 6f30923caf..ee7a951c57 100644 --- a/packages/react-native-gesture-handler/src/jestUtils/jestUtils.ts +++ b/packages/react-native-gesture-handler/src/jestUtils/jestUtils.ts @@ -60,6 +60,8 @@ import { } from '../handlers/TapGestureHandler'; import { State } from '../State'; import { hasProperty, withPrevAndCurrent } from '../utils'; +import type { SingleGesture } from '../v3/types'; +import { maybeUnpackValue } from '../v3/hooks/utils'; // Load fireEvent conditionally, so RNGH may be used in setups without testing-library let fireEvent = ( @@ -164,11 +166,17 @@ const handlersDefaultEvents: DefaultEventsMapping = { }; function isGesture( - componentOrGesture: ReactTestInstance | GestureType + componentOrGesture: ReactTestInstance | GestureType | SingleGesture ): componentOrGesture is GestureType { return componentOrGesture instanceof BaseGesture; } +function isHookGesture( + componentOrGesture: ReactTestInstance | SingleGesture +): componentOrGesture is SingleGesture { + return 'detectorCallbacks' in componentOrGesture; +} + interface WrappedGestureHandlerTestEvent { nativeEvent: GestureHandlerTestEvent; } @@ -408,7 +416,7 @@ interface HandlerData { enabled: boolean | undefined; } function getHandlerData( - componentOrGesture: ReactTestInstance | GestureType + componentOrGesture: ReactTestInstance | GestureType | SingleGesture ): HandlerData { if (isGesture(componentOrGesture)) { const gesture = componentOrGesture; @@ -421,6 +429,33 @@ function getHandlerData( enabled: gesture.config.enabled, }; } + + if (isHookGesture(componentOrGesture)) { + return { + handlerType: componentOrGesture.type as HandlerNames, + handlerTag: componentOrGesture.tag, + enabled: maybeUnpackValue(componentOrGesture.config.enabled), + emitEvent: (eventName, args) => { + const { state, oldState, handlerTag, ...rest } = args.nativeEvent; + + const event = { + state, + handlerTag, + handlerData: { ...rest }, + }; + + if (eventName === 'onGestureHandlerStateChange') { + componentOrGesture.detectorCallbacks.onGestureHandlerStateChange({ + oldState: oldState as State, + ...event, + }); + } else if (eventName === 'onGestureHandlerEvent') { + componentOrGesture.detectorCallbacks.onGestureHandlerEvent?.(event); + } + }, + }; + } + const gestureHandlerComponent = componentOrGesture; return { emitEvent: (eventName, args) => { @@ -465,7 +500,7 @@ type ExtractConfig = : Record; export function fireGestureHandler( - componentOrGesture: ReactTestInstance | GestureType, + componentOrGesture: ReactTestInstance | GestureType | SingleGesture, eventList: Partial>>[] = [] ): void { const { emitEvent, handlerType, handlerTag, enabled } = diff --git a/packages/react-native-gesture-handler/src/v3/hooks/useGesture.ts b/packages/react-native-gesture-handler/src/v3/hooks/useGesture.ts index 3f0bdbee05..98b49879a8 100644 --- a/packages/react-native-gesture-handler/src/v3/hooks/useGesture.ts +++ b/packages/react-native-gesture-handler/src/v3/hooks/useGesture.ts @@ -10,6 +10,11 @@ import { } from './utils'; import { tagMessage } from '../../utils'; import { BaseGestureConfig, SingleGesture, SingleGestureName } from '../types'; +import { scheduleFlushOperations } from '../../handlers/utils'; +import { + registerGesture, + unregisterGesture, +} from '../../handlers/handlersRegistry'; import { Platform } from 'react-native'; import { NativeProxy } from '../NativeProxy'; @@ -64,25 +69,8 @@ export function useGesture( NativeProxy.createGestureHandler(type, tag, {}); } - useEffect(() => { - return () => { - NativeProxy.dropGestureHandler(tag); - }; - }, [type, tag]); - - useEffect(() => { - const preparedConfig = prepareConfigForNativeSide(type, config); - NativeProxy.setGestureHandlerConfig(tag, preparedConfig); - - bindSharedValues(config, tag); - - return () => { - unbindSharedValues(config, tag); - }; - }, [tag, config, type]); - - return useMemo( - (): SingleGesture => ({ + const gesture = useMemo( + () => ({ tag, type, config, @@ -121,4 +109,27 @@ export function useGesture( gestureRelations, ] ); + + useEffect(() => { + return () => { + NativeProxy.dropGestureHandler(tag); + scheduleFlushOperations(); + }; + }, [type, tag]); + + useEffect(() => { + const preparedConfig = prepareConfigForNativeSide(type, config); + NativeProxy.setGestureHandlerConfig(tag, preparedConfig); + scheduleFlushOperations(); + + bindSharedValues(config, tag); + registerGesture(tag, gesture); + + return () => { + unbindSharedValues(config, tag); + unregisterGesture(tag); + }; + }, [tag, config, type, gesture]); + + return gesture; } diff --git a/packages/react-native-gesture-handler/src/v3/hooks/utils/propsWhiteList.ts b/packages/react-native-gesture-handler/src/v3/hooks/utils/propsWhiteList.ts index 29a19e52e2..e59ae78643 100644 --- a/packages/react-native-gesture-handler/src/v3/hooks/utils/propsWhiteList.ts +++ b/packages/react-native-gesture-handler/src/v3/hooks/utils/propsWhiteList.ts @@ -24,6 +24,7 @@ const CommonConfig = new Set([ 'mouseButton', 'enableContextMenu', 'touchAction', + 'testID', ]); const ExternalRelationsConfig = new Set([ diff --git a/packages/react-native-gesture-handler/src/v3/types/ConfigTypes.ts b/packages/react-native-gesture-handler/src/v3/types/ConfigTypes.ts index aa043e8249..697589e213 100644 --- a/packages/react-native-gesture-handler/src/v3/types/ConfigTypes.ts +++ b/packages/react-native-gesture-handler/src/v3/types/ConfigTypes.ts @@ -45,6 +45,7 @@ export type InternalConfigProps = { export type CommonGestureConfig = { disableReanimated?: boolean; useAnimated?: boolean; + testID?: string; } & WithSharedValue< { runOnJS?: boolean;