Skip to content

Commit 6afb378

Browse files
m-bertj-piasecki
andauthored
Handle testID prop (#3838)
## Description This PR adds handling of `testID` property. ## Test plan `yarn test api_v3` --------- Co-authored-by: Jakub Piasecki <jakub.piasecki@swmansion.com>
1 parent 0885d75 commit 6afb378

File tree

7 files changed

+163
-25
lines changed

7 files changed

+163
-25
lines changed

.github/workflows/check-relations-traversal-algorithm.yml renamed to .github/workflows/rngh-api-v3.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
name: Test relations traversal algorithm
1+
name: Test Gesture Handler 3 API
22

33
on:
44
pull_request:
55
paths:
66
- packages/react-native-gesture-handler/src/v3/**
77
- packages/react-native-gesture-handler/src/__tests__/RelationsTraversal.test.tsx
8+
- packages/react-native-gesture-handler/src/__tests__/API_V3.test.tsx
89
push:
910
branches:
1011
- main
@@ -34,4 +35,4 @@ jobs:
3435

3536
- name: Run tests
3637
working-directory: packages/react-native-gesture-handler
37-
run: yarn test RelationsTraversal
38+
run: yarn test RelationsTraversal API_V3
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { usePanGesture } from '../v3/hooks/gestures';
2+
import { render, renderHook } from '@testing-library/react-native';
3+
import { fireGestureHandler, getByGestureTestId } from '../jestUtils';
4+
import { State } from '../State';
5+
import GestureHandlerRootView from '../components/GestureHandlerRootView';
6+
import { RectButton } from '../v3/components';
7+
import { act } from 'react';
8+
9+
describe('[API v3] Hooks', () => {
10+
test('Pan gesture', () => {
11+
const onBegin = jest.fn();
12+
const onStart = jest.fn();
13+
14+
const panGesture = renderHook(() =>
15+
usePanGesture({
16+
disableReanimated: true,
17+
onBegin: (e) => onBegin(e),
18+
onActivate: (e) => onStart(e),
19+
})
20+
).result.current;
21+
22+
fireGestureHandler(panGesture, [
23+
{ oldState: State.UNDETERMINED, state: State.BEGAN },
24+
{ oldState: State.BEGAN, state: State.ACTIVE },
25+
{ oldState: State.ACTIVE, state: State.ACTIVE },
26+
{ oldState: State.ACTIVE, state: State.END },
27+
]);
28+
29+
expect(onBegin).toHaveBeenCalledTimes(1);
30+
expect(onStart).toHaveBeenCalledTimes(1);
31+
});
32+
});
33+
34+
describe('[API v3] Components', () => {
35+
test('Rect Button', () => {
36+
const pressFn = jest.fn();
37+
38+
const RectButtonExample = () => {
39+
return (
40+
<GestureHandlerRootView>
41+
<RectButton testID="btn" onPress={pressFn} />
42+
</GestureHandlerRootView>
43+
);
44+
};
45+
46+
render(<RectButtonExample />);
47+
48+
const nativeGesture = getByGestureTestId('btn');
49+
50+
act(() => {
51+
fireGestureHandler(nativeGesture, [
52+
{ oldState: State.UNDETERMINED, state: State.BEGAN },
53+
{ oldState: State.BEGAN, state: State.ACTIVE },
54+
{ oldState: State.ACTIVE, state: State.END },
55+
]);
56+
});
57+
58+
expect(pressFn).toHaveBeenCalledTimes(1);
59+
});
60+
});

packages/react-native-gesture-handler/src/handlers/handlersRegistry.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,37 @@
11
import { isTestEnv } from '../utils';
22
import { GestureType } from './gestures/gesture';
33
import { GestureEvent, HandlerStateChangeEvent } from './gestureHandlerCommon';
4+
import { SingleGesture } from '../v3/types';
45

56
export const handlerIDToTag: Record<string, number> = {};
7+
8+
// There were attempts to create types that merge possible HandlerData and Config,
9+
// but ts was not able to infer them properly in many cases, so we use any here.
10+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
11+
const hookGestures = new Map<number, SingleGesture<any, any>>();
612
const gestures = new Map<number, GestureType>();
713
const oldHandlers = new Map<number, GestureHandlerCallbacks>();
814
const testIDs = new Map<string, number>();
915

16+
export function registerGesture<THandlerData, TConfig>(
17+
handlerTag: number,
18+
gesture: SingleGesture<THandlerData, TConfig>
19+
) {
20+
if (isTestEnv() && gesture.config.testID) {
21+
hookGestures.set(handlerTag, gesture);
22+
testIDs.set(gesture.config.testID, handlerTag);
23+
}
24+
}
25+
26+
export function unregisterGesture(handlerTag: number) {
27+
const gesture = hookGestures.get(handlerTag);
28+
29+
if (gesture && isTestEnv() && gesture.config.testID) {
30+
testIDs.delete(gesture.config.testID);
31+
hookGestures.delete(handlerTag);
32+
}
33+
}
34+
1035
export function registerHandler(
1136
handlerTag: number,
1237
handler: GestureType,
@@ -40,14 +65,18 @@ export function findHandler(handlerTag: number) {
4065
return gestures.get(handlerTag);
4166
}
4267

68+
export function findGesture(handlerTag: number) {
69+
return hookGestures.get(handlerTag);
70+
}
71+
4372
export function findOldGestureHandler(handlerTag: number) {
4473
return oldHandlers.get(handlerTag);
4574
}
4675

4776
export function findHandlerByTestID(testID: string) {
4877
const handlerTag = testIDs.get(testID);
4978
if (handlerTag !== undefined) {
50-
return findHandler(handlerTag) ?? null;
79+
return findHandler(handlerTag) ?? findGesture(handlerTag) ?? null;
5180
}
5281
return null;
5382
}

packages/react-native-gesture-handler/src/jestUtils/jestUtils.ts

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ import {
6060
} from '../handlers/TapGestureHandler';
6161
import { State } from '../State';
6262
import { hasProperty, withPrevAndCurrent } from '../utils';
63+
import type { SingleGesture } from '../v3/types';
64+
import { maybeUnpackValue } from '../v3/hooks/utils';
6365

6466
// Load fireEvent conditionally, so RNGH may be used in setups without testing-library
6567
let fireEvent = (
@@ -164,11 +166,17 @@ const handlersDefaultEvents: DefaultEventsMapping = {
164166
};
165167

166168
function isGesture(
167-
componentOrGesture: ReactTestInstance | GestureType
169+
componentOrGesture: ReactTestInstance | GestureType | SingleGesture<any, any>
168170
): componentOrGesture is GestureType {
169171
return componentOrGesture instanceof BaseGesture;
170172
}
171173

174+
function isHookGesture(
175+
componentOrGesture: ReactTestInstance | SingleGesture<any, any>
176+
): componentOrGesture is SingleGesture<any, any> {
177+
return 'detectorCallbacks' in componentOrGesture;
178+
}
179+
172180
interface WrappedGestureHandlerTestEvent {
173181
nativeEvent: GestureHandlerTestEvent;
174182
}
@@ -408,7 +416,7 @@ interface HandlerData {
408416
enabled: boolean | undefined;
409417
}
410418
function getHandlerData(
411-
componentOrGesture: ReactTestInstance | GestureType
419+
componentOrGesture: ReactTestInstance | GestureType | SingleGesture<any, any>
412420
): HandlerData {
413421
if (isGesture(componentOrGesture)) {
414422
const gesture = componentOrGesture;
@@ -421,6 +429,33 @@ function getHandlerData(
421429
enabled: gesture.config.enabled,
422430
};
423431
}
432+
433+
if (isHookGesture(componentOrGesture)) {
434+
return {
435+
handlerType: componentOrGesture.type as HandlerNames,
436+
handlerTag: componentOrGesture.tag,
437+
enabled: maybeUnpackValue(componentOrGesture.config.enabled),
438+
emitEvent: (eventName, args) => {
439+
const { state, oldState, handlerTag, ...rest } = args.nativeEvent;
440+
441+
const event = {
442+
state,
443+
handlerTag,
444+
handlerData: { ...rest },
445+
};
446+
447+
if (eventName === 'onGestureHandlerStateChange') {
448+
componentOrGesture.detectorCallbacks.onGestureHandlerStateChange({
449+
oldState: oldState as State,
450+
...event,
451+
});
452+
} else if (eventName === 'onGestureHandlerEvent') {
453+
componentOrGesture.detectorCallbacks.onGestureHandlerEvent?.(event);
454+
}
455+
},
456+
};
457+
}
458+
424459
const gestureHandlerComponent = componentOrGesture;
425460
return {
426461
emitEvent: (eventName, args) => {
@@ -465,7 +500,7 @@ type ExtractConfig<T> =
465500
: Record<string, unknown>;
466501

467502
export function fireGestureHandler<THandler extends AllGestures | AllHandlers>(
468-
componentOrGesture: ReactTestInstance | GestureType,
503+
componentOrGesture: ReactTestInstance | GestureType | SingleGesture<any, any>,
469504
eventList: Partial<GestureHandlerTestEvent<ExtractConfig<THandler>>>[] = []
470505
): void {
471506
const { emitEvent, handlerType, handlerTag, enabled } =

packages/react-native-gesture-handler/src/v3/hooks/useGesture.ts

Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ import {
1010
} from './utils';
1111
import { tagMessage } from '../../utils';
1212
import { BaseGestureConfig, SingleGesture, SingleGestureName } from '../types';
13+
import { scheduleFlushOperations } from '../../handlers/utils';
14+
import {
15+
registerGesture,
16+
unregisterGesture,
17+
} from '../../handlers/handlersRegistry';
1318
import { Platform } from 'react-native';
1419
import { NativeProxy } from '../NativeProxy';
1520

@@ -64,25 +69,8 @@ export function useGesture<THandlerData, TConfig>(
6469
NativeProxy.createGestureHandler(type, tag, {});
6570
}
6671

67-
useEffect(() => {
68-
return () => {
69-
NativeProxy.dropGestureHandler(tag);
70-
};
71-
}, [type, tag]);
72-
73-
useEffect(() => {
74-
const preparedConfig = prepareConfigForNativeSide(type, config);
75-
NativeProxy.setGestureHandlerConfig(tag, preparedConfig);
76-
77-
bindSharedValues(config, tag);
78-
79-
return () => {
80-
unbindSharedValues(config, tag);
81-
};
82-
}, [tag, config, type]);
83-
84-
return useMemo(
85-
(): SingleGesture<THandlerData, TConfig> => ({
72+
const gesture = useMemo(
73+
() => ({
8674
tag,
8775
type,
8876
config,
@@ -121,4 +109,27 @@ export function useGesture<THandlerData, TConfig>(
121109
gestureRelations,
122110
]
123111
);
112+
113+
useEffect(() => {
114+
return () => {
115+
NativeProxy.dropGestureHandler(tag);
116+
scheduleFlushOperations();
117+
};
118+
}, [type, tag]);
119+
120+
useEffect(() => {
121+
const preparedConfig = prepareConfigForNativeSide(type, config);
122+
NativeProxy.setGestureHandlerConfig(tag, preparedConfig);
123+
scheduleFlushOperations();
124+
125+
bindSharedValues(config, tag);
126+
registerGesture(tag, gesture);
127+
128+
return () => {
129+
unbindSharedValues(config, tag);
130+
unregisterGesture(tag);
131+
};
132+
}, [tag, config, type, gesture]);
133+
134+
return gesture;
124135
}

packages/react-native-gesture-handler/src/v3/hooks/utils/propsWhiteList.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const CommonConfig = new Set<keyof CommonGestureConfig>([
2424
'mouseButton',
2525
'enableContextMenu',
2626
'touchAction',
27+
'testID',
2728
]);
2829

2930
const ExternalRelationsConfig = new Set<keyof ExternalRelations>([

packages/react-native-gesture-handler/src/v3/types/ConfigTypes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export type InternalConfigProps<THandlerData> = {
4545
export type CommonGestureConfig = {
4646
disableReanimated?: boolean;
4747
useAnimated?: boolean;
48+
testID?: string;
4849
} & WithSharedValue<
4950
{
5051
runOnJS?: boolean;

0 commit comments

Comments
 (0)