Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for useReanimatedGestureHandler in Jest #1762

Closed
wants to merge 28 commits into from
Closed
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
219 changes: 219 additions & 0 deletions __tests__/Events.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
import React from 'react';
import { render } from '@testing-library/react-native';
import { Text, View } from 'react-native';
import {
GestureHandlerRootView,
TapGestureHandler,
PanGestureHandler,
LongPressGestureHandler,
FlingGestureHandler,
RotationGestureHandler,
PinchGestureHandler,
Gesture,
GestureDetector
} from 'react-native-gesture-handler';
import {
fireTapGestureHandler,
firePanGestureHandler,
fireLongPressGestureHandler,
fireRotationGestureHandler,
fireFlingGestureHandler,
firePinchGestureHandler,
ghTagEventMacro
} from '../src/jestUtils'
import { useAnimatedGestureHandler } from 'react-native-reanimated';

const mockEventFunctions = () => {
return {
begin: jest.fn(),
progress: jest.fn(),
end: jest.fn(),
fail: jest.fn(),
cancel: jest.fn(),
finish: jest.fn()
};
}

const assertEventCalls = (eventFunctions, counts) => {
expect(eventFunctions.begin).toHaveBeenCalledTimes(
counts?.begin ? counts.begin : 1
);
expect(eventFunctions.progress).toHaveBeenCalledTimes(
counts?.progress ? counts.progress : 1
);
expect(eventFunctions.end).toHaveBeenCalledTimes(
counts?.end ? counts.end : 1
);
}

const App = (props) => {
const eventHandler = useAnimatedGestureHandler({
onStart: () => props.eventFunctions.begin(),
onActive: () => props.eventFunctions.progress(),
onEnd: () => props.eventFunctions.end()
});

return (
<GestureHandlerRootView>
<TapGestureHandler onHandlerStateChange={eventHandler}>
<Text {...ghTagEventMacro()}>TapGestureHandlerTest</Text>
</TapGestureHandler>

<PanGestureHandler onHandlerStateChange={eventHandler} onGestureEvent={eventHandler}>
<Text {...ghTagEventMacro()}>PanGestureHandlerTest</Text>
</PanGestureHandler>

<LongPressGestureHandler onHandlerStateChange={eventHandler}>
<Text {...ghTagEventMacro()}>LongPressGestureHandlerTest</Text>
</LongPressGestureHandler>

<RotationGestureHandler onHandlerStateChange={eventHandler}>
<Text {...ghTagEventMacro()}>RotationGestureHandlerTest</Text>
</RotationGestureHandler>

<FlingGestureHandler onHandlerStateChange={eventHandler}>
<Text {...ghTagEventMacro()}>FlingGestureHandlerTest</Text>
</FlingGestureHandler>

<PinchGestureHandler onHandlerStateChange={eventHandler}>
<Text {...ghTagEventMacro()}>PinchGestureHandlerTest</Text>
</PinchGestureHandler>

<PanGestureHandler onHandlerStateChange={eventHandler}>
<View>
<Text {...ghTagEventMacro()}>NestedGestureHandlerTest1</Text>
<TapGestureHandler onHandlerStateChange={eventHandler}>
<Text {...ghTagEventMacro()}>NestedGestureHandlerTest2</Text>
</TapGestureHandler>
</View>
</PanGestureHandler>

</GestureHandlerRootView>
);
};

test('test fireTapGestureHandler', () => {
const eventFunctions = mockEventFunctions();
const { getByText } = render(<App eventFunctions={eventFunctions} />);
fireTapGestureHandler(getByText('TapGestureHandlerTest'));
assertEventCalls(eventFunctions);
});

test('test firePanGestureHandler', () => {
const eventFunctions = mockEventFunctions();
const { getByText } = render(<App eventFunctions={eventFunctions} />);
firePanGestureHandler(
getByText('PanGestureHandlerTest'),
{
configBegin: { x: 1, y: 1 },
configProgress: [{ x: 2, y: 2 }, { x: 3, y: 3 }],
configEnd: { x: 4, y: 4 }
},
);
assertEventCalls(eventFunctions, { progress: 2 });
});

test('test fireLongPressGestureHandler', () => {
const eventFunctions = mockEventFunctions();
const { getByText } = render(<App eventFunctions={eventFunctions} />);
fireLongPressGestureHandler(
getByText('LongPressGestureHandlerTest'),
{ configBegin: { x: 1, y: 1 } },
);
assertEventCalls(eventFunctions);
});

test('test fireRotationGestureHandler', () => {
const eventFunctions = mockEventFunctions();
const { getByText } = render(<App eventFunctions={eventFunctions} />);
fireRotationGestureHandler(
getByText('RotationGestureHandlerTest'),
{
configBegin: {
rotation: 0,
velocity: 0,
anchorX: 0,
anchorY: 0,
},
configProgress: {
rotation: 5,
velocity: 5,
anchorX: 5,
anchorY: 5,
},
configEnd: {
rotation: 0,
velocity: 0,
anchorX: 0,
anchorY: 0,
},
}
);
assertEventCalls(eventFunctions);
});

test('test fireFlingGestureHandler', () => {
const eventFunctions = mockEventFunctions();
const { getByText } = render(<App eventFunctions={eventFunctions} />);
fireFlingGestureHandler(
getByText('FlingGestureHandlerTest'),
{ configBegin: { x: 1, y: 1 } },
);
assertEventCalls(eventFunctions);
});

test('test firePinchGestureHandler', () => {
const eventFunctions = mockEventFunctions();
const { getByText } = render(<App eventFunctions={eventFunctions} />);
firePinchGestureHandler(
getByText('PinchGestureHandlerTest'),
{ configBegin: { x: 1, y: 1 } },
);
assertEventCalls(eventFunctions);
});

test('test nestedGestureHandler', () => {
const eventFunctions = mockEventFunctions();
const { getByText } = render(<App eventFunctions={eventFunctions} />);
firePanGestureHandler(getByText('NestedGestureHandlerTest1'));
firePanGestureHandler(getByText('NestedGestureHandlerTest2'));
fireTapGestureHandler(getByText('NestedGestureHandlerTest2'));
assertEventCalls(eventFunctions, { begin: 3, progress: 3, end: 3 });
});

const AppAPIv2 = props => {
const tap = Gesture.Tap()
.onBegin(() => {
props.eventFunctions.begin()
})
.onEnd(() => {
props.eventFunctions.progress()
});

const pan = Gesture.Pan()
.onBegin(() => {
props.eventFunctions.begin()
})
.onEnd(() => {
props.eventFunctions.progress()
});

return (
<GestureHandlerRootView>
<GestureDetector gesture={Gesture.Race(tap, pan)}>
<View>
<Text {...ghTagEventMacro()}>Text</Text>
</View>
</GestureDetector>
</GestureHandlerRootView>
);
};

test('test API v2', () => {
const eventFunctions = mockEventFunctions();
const {getByText} = render(<AppAPIv2 eventFunctions={eventFunctions} />);
fireTapGestureHandler(getByText('Text'));
firePanGestureHandler(getByText('Text'));
expect(eventFunctions.begin).toHaveBeenCalledTimes(2);
expect(eventFunctions.progress).toHaveBeenCalledTimes(2);
});
7 changes: 7 additions & 0 deletions babel.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module.exports = {
presets: [
'@babel/preset-env',
'@babel/preset-typescript',
'module:metro-react-native-babel-preset',
],
};
9 changes: 9 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module.exports = {
preset: 'react-native',
setupFiles: ['./jestSetup.js'],
setupFilesAfterEnv: ['@testing-library/jest-native/extend-expect'],
transformIgnorePatterns: [
"node_modules/?!(react-native-reanimated)",
"node_modules/?!(react-native)"
]
};
10 changes: 6 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,11 @@
"@babel/preset-env": "^7.12.11",
"@babel/preset-typescript": "^7.12.7",
"@babel/runtime": "^7.12.5",
"@testing-library/jest-native": "^4.0.4",
"@testing-library/react-native": "^9.0.0",
"@types/hammerjs": "^2.0.38",
"@types/hoist-non-react-statics": "^3.3.1",
"@types/jest": "^26.0.19",
"@types/jest": "^27.0.3",
"@types/react": "^17.0.0",
"@types/react-native": "^0.64.2",
"@types/react-test-renderer": "^17.0.0",
Expand All @@ -84,7 +86,7 @@
"expo": "^35.0.1",
"flow-bin": "^0.98.0",
"husky": "^0.14.3",
"jest": "^24.7.1",
"jest": "^26.6.3",
"jest-react-native": "16.0.0",
"lint-staged": "^10.2.11",
"metro-react-native-babel-preset": "^0.64.0",
Expand All @@ -93,9 +95,9 @@
"react-dom": "^16.12.0",
"react-native": "^0.64.0",
"react-native-builder-bob": "^0.17.1",
"react-native-reanimated": "^2.0.0",
"react-native-reanimated": "^2.3.1",
"react-native-web": "^0.11.7",
"react-test-renderer": "16.8.6",
"react-test-renderer": "17.0.2",
"release-it": "^13.6.5",
"typescript": "^4.1.2"
},
Expand Down
12 changes: 12 additions & 0 deletions src/handlers/createHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
findNodeHandle,
} from './gestureHandlerCommon';
import { ValueOf } from '../typeUtils';
import { decorateChildrenWithTag } from '../jestUtils';

const UIManagerAny = UIManager as any;

Expand Down Expand Up @@ -162,6 +163,17 @@ export default function createHandler<
}
handlerIDToTag[props.id] = this.handlerTag;
}
// @ts-ignore @typescript-eslint/ban-ts-comment
if (process.env.JEST_WORKER_ID) {
const handlerProperties = {
handlerType: name,
handlerTag: this.handlerTag,
onGestureEvent: props.onGestureEvent,
onHandlerStateChange: props.onHandlerStateChange,
};
// @ts-ignore @typescript-eslint/ban-ts-comment
decorateChildrenWithTag({ props }, handlerProperties);
}
}

componentDidMount() {
Expand Down
23 changes: 23 additions & 0 deletions src/handlers/gestures/GestureDetector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { tapGestureHandlerProps } from '../TapGestureHandler';
import { State } from '../../State';
import { EventType } from '../../EventType';
import { ComposedGesture } from './gestureComposition';
import { decorateChildrenWithTag } from '../../jestUtils';

const ALLOWED_PROPS = [
...baseGestureHandlerWithMonitorProps,
Expand Down Expand Up @@ -416,6 +417,28 @@ export const GestureDetector: React.FunctionComponent<GestureDetectorProps> = (
);
}

// @ts-ignore @typescript-eslint/ban-ts-comment
if (process.env.JEST_WORKER_ID) {
for (const handler of gesture) {
const handlers = handler.handlers;
// eslint-disable-next-line react-hooks/rules-of-hooks
const reaGestureHandler = Reanimated.useAnimatedGestureHandler({
onStart: handlers.onBegin,
onActive: handlers.onUpdate,
onEnd: handlers.onEnd,
onFinish: handlers.onFinalize,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
onStart: handlers.onBegin,
onActive: handlers.onUpdate,
onEnd: handlers.onEnd,
onFinish: handlers.onFinalize,
// this one passes the context to the onBegin callback, but I think it's fine in tests
onStart: handlers.onBegin,
onActive: (event, context) => {
// useAnimatedGestureHandler passes all events with state === 4 (ACTIVE) to the onActive
// callback, event the StateChangeEvent from 2 (BEGAN) to 4. Since the StateChangeEvent
// will always be the first in the new state we can just check a flag here.
if (context.active) {
handlers.onUpdate?.(event);
} else {
handlers.onStart?.(event);
context.active = true;
}
},
onEnd: (event, context) => {
// in the new API onEnd, onFail and onCancel callbacks are merged into one, because
// - from the user's perspective there is no meaningfull difference between gesture failing
// and being cancelled, so having separate callbacks for it makes no sense
// - most of the time you want to clean up after the gesture in all of those callbacks,
// so there is no point in repeating the same logic three times
// new onEnd callback receives a bool as the second argument which tells whether the gesture
// has finished gracefully or not, and if you really need to know if it failed or was cancelled
// you can chech the state property of event
handlers.onEnd?.(event, true);
context.success = true;
},
onCancel: (event, context) => {
handlers.onEnd?.(event, false);
context.success = false;
},
onFail: (event, context) => {
handlers.onEnd?.(event, false);
context.success = false;
},
onFinish: (event, context) => {
// onFinalize, like onEnd, requires a flag that tells whether the gesture finished gracefully
handlers.onFinalize?.(event, context.success ?? false);
},

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if emulating the new API using the old one is the best approach.

});
const handlerProperties = {
handlerType: handler.handlerName,
handlerTag: handler.handlerTag,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a potential problem, as handlerTag is initialized in the attachHandlers method which is called in the useEffect hook. This has a value of -1 here.

onGestureEvent: reaGestureHandler,
onHandlerStateChange: reaGestureHandler,
};
// @ts-ignore @typescript-eslint/ban-ts-comment
decorateChildrenWithTag({ props }, handlerProperties);
}
}

if (preparedGesture.firstExecution) {
gestureConfig?.initialize?.();
}
Expand Down
11 changes: 11 additions & 0 deletions src/handlers/gestures/reanimatedWrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,17 @@ let Reanimated: {
) => unknown;
useSharedValue: <T>(value: T) => SharedValue<T>;
setGestureState: (handlerTag: number, newState: number) => void;
useAnimatedGestureHandler: (
handlers: {
onStart?: (event: any, context: any) => void;
onActive?: (event: any, context: any) => void;
onEnd?: (event: any, context: any) => void;
onFail?: (event: any, context: any) => void;
onCancel?: (event: any, context: any) => void;
onFinish?: (event: any, context: any) => void;
},
dependencies?: any
) => void;
};

try {
Expand Down
Loading