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

Conversation

piaskowyk
Copy link
Member

@piaskowyk piaskowyk commented Dec 15, 2021

Description

I want to add support for useReanimatedGestureHandler in Jest tests and I want to make it user-friendly. So I added configuration for events, ready to use in Jest.

Related PR: software-mansion/react-native-reanimated#2747

Test plan

  1. Apply patch - https://patch-diff.githubusercontent.com/raw/software-mansion/react-native-reanimated/pull/2747.patch
  2. Run:
yarn test -i Event

Example usage

import { 
  fireGestureHandlerPan,
  ghTagEventMacro
} from '../src/jestUtils'

// ...

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

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

// ...

test('test fireGestureHandlerPan', () => {
  const begin = jest.fn();
  const { getByText } = render(<App begin={begin} ... />);

  firePanGestureHandler(
    getByText('PanGestureHandlerTest'),
    { 
      configBegin: { x: 1, y: 1 },
      configProgress: [{ x: 2, y: 2 }, { x: 3, y: 3 }],
      configEnd: { x: 4, y: 4 }
    },
  );
  
  expect(eventFunctions.begin).toHaveBeenCalledTimes(1);
});

@piaskowyk piaskowyk requested a review from j-piasecki December 15, 2021 12:01
@piaskowyk piaskowyk marked this pull request as ready for review December 16, 2021 19:36
src/jestUtils.ts Outdated Show resolved Hide resolved
src/jestUtils.ts Outdated Show resolved Hide resolved
@piaskowyk piaskowyk requested a review from j-piasecki December 27, 2021 10:26
);
};

const sendProgressEventFirst: SendEventWrapper = (
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
const sendProgressEventFirst: SendEventWrapper = (
const sendActivateEvent: SendEventWrapper = (

Since it's sending a StateChangeEvent I would put the state in the method name like it is with other states.

src/jestUtils.ts Outdated
if (eventData?.onFail) {
states = { oldState: 2, state: 1 };
} else if (eventData?.onCancel) {
states = { oldState: 2, state: 2 };
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
states = { oldState: 2, state: 2 };
states = { oldState: 2, state: 3 };

Is it a typo, or is there a reason for it?

src/jestUtils.ts Outdated
Comment on lines 225 to 235
handlers.forEach((handler) => {
sendEndEvent(handler, baseEventData, eventData?.configEnd);
});

handlers.forEach((handler) => {
sendFailEvent(handler, baseEventData, eventData?.onFail);
});

handlers.forEach((handler) => {
sendCancelEvent(handler, baseEventData, eventData?.onCancel);
});
Copy link
Member

Choose a reason for hiding this comment

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

Only one of those should be sent at a time - gesture cannot end and fail or be cancelled at the same time. (END state means that the gesture finished because the user stopped interaction, i.e. the gesture finished "successfully").

src/jestUtils.ts Outdated
Comment on lines 246 to 250
const config: HandlerProperties[] =
component?._fiber?.stateNode?.props?.ghTagContainer;
if (config) {
return config.filter((handler) => handler.handlerType === handlerType);
}
Copy link
Member

Choose a reason for hiding this comment

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

I think this approach will fail when there are two handlers of the same type attached to the same view, then there is no way to send an event to only one of them.
This has a use case for detecting a single and double tap on a view, or a fling with separate handlers for each direction.

src/jestUtils.ts Outdated

export const fireFlingGestureHandler = (
component: any,
eventData?: FireGestureHandlerConfig<RotationConfig>
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
eventData?: FireGestureHandlerConfig<RotationConfig>
eventData?: FireGestureHandlerConfig<TapConfig>

I'm not sure if sharing a config type between Tap and Fling is a good idea even though they have exactly the same properties. It may be confusing when looking at the API.

src/jestUtils.ts Outdated

export const firePinchGestureHandler = (
component: any,
eventData?: FireGestureHandlerConfig<RotationConfig>
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
eventData?: FireGestureHandlerConfig<RotationConfig>
eventData?: FireGestureHandlerConfig<PinchConfig>

src/jestUtils.ts Outdated
Comment on lines 237 to 239
handlers.forEach((handler) => {
sendFinishEvent(handler, baseEventData, eventData);
});
Copy link
Member

Choose a reason for hiding this comment

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

There is no need to handle onFinish separately. useAnimatedGestureHandler implements a state machine that calls it when it receives a StateChangeEvent where oldState is either ACTIVE or BEGAN, and the state is not (so it must be END, FAILED, CANCELLED or UNDETERMINED).

});
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.

Comment on lines 426 to 429
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.

src/jestUtils.ts Outdated
Comment on lines 377 to 383
if (Array.isArray(component.props.children)) {
for (const child of component.props.children) {
decorateChildrenWithTag(child, handlerProperties);
}
} else if (typeof component.props.children === 'object') {
decorateChildrenWithTag(component.props.children, handlerProperties);
}
Copy link
Member

Choose a reason for hiding this comment

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

I think this may fail if you were to use your own component as a child of GestureDetector, then its inner structure wouldn't be decorated. In case of gesture handlers it shouldn't matter that much since they required a native view as a direct child.

@jakub-gonet
Copy link
Member

Superseded by #1844.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants