Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,8 @@ class RNGestureHandlerTouchEvent private constructor() : Event<RNGestureHandlerT
putInt("eventType", handler.touchEventType)
putInt("pointerType", handler.pointerType)

handler.consumeChangedTouchesPayload()?.let {
putArray("changedTouches", it)
}

handler.consumeAllTouchesPayload()?.let {
putArray("allTouches", it)
}
putArray("changedTouches", handler.consumeChangedTouchesPayload() ?: Arguments.createArray())
putArray("allTouches", handler.consumeAllTouchesPayload() ?: Arguments.createArray())

if (handler.isAwaiting && handler.state == GestureHandler.STATE_ACTIVE) {
putInt("state", GestureHandler.STATE_BEGAN)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@
const panGesture = renderHook(() =>
usePanGesture({
disableReanimated: true,
onBegin: (e) => onBegin(e),

Check warning on line 31 in packages/react-native-gesture-handler/src/__tests__/api_v3.test.tsx

View workflow job for this annotation

GitHub Actions / check

Unsafe return of an `any` typed value
onActivate: (e) => onStart(e),

Check warning on line 32 in packages/react-native-gesture-handler/src/__tests__/api_v3.test.tsx

View workflow job for this annotation

GitHub Actions / check

Unsafe return of an `any` typed value
})
).result.current;

Expand All @@ -43,6 +43,88 @@
expect(onBegin).toHaveBeenCalledTimes(1);
expect(onStart).toHaveBeenCalledTimes(1);
});

test('Pan gesture drops malformed event without crashing', () => {
// On Android a touch event may be serialized without the `allTouches` key
// when its payload is lost in a race (e.g. rapid taps cancelling the
// gesture). Such an event has no `oldState`, no `allTouches` and no
// `handlerData`, so it used to be misclassified as an update event and
// crash the pan change calculator with
// "Cannot read property 'translationX' of undefined".
const onUpdate = jest.fn();
const onTouchesUp = jest.fn();

const panGesture = renderHook(() =>
usePanGesture({
disableReanimated: true,
onUpdate: (e) => onUpdate(e),

Check warning on line 60 in packages/react-native-gesture-handler/src/__tests__/api_v3.test.tsx

View workflow job for this annotation

GitHub Actions / check

Unsafe return of an `any` typed value
onTouchesUp: (e) => onTouchesUp(e),

Check warning on line 61 in packages/react-native-gesture-handler/src/__tests__/api_v3.test.tsx

View workflow job for this annotation

GitHub Actions / check

Unsafe return of an `any` typed value
})
).result.current;

const { jsEventHandler } = panGesture.detectorCallbacks;

const malformedTouchEvent = {
handlerTag: panGesture.handlerTag,
state: State.ACTIVE,
eventType: 3, // TouchEventType.TOUCHES_UP
numberOfTouches: 0,
changedTouches: [{ id: 0, x: 0, y: 0, absoluteX: 0, absoluteY: 0 }],
// no `allTouches`, no `oldState`, no `handlerData`
};

expect(() => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
jsEventHandler?.(malformedTouchEvent as any);

Check warning on line 78 in packages/react-native-gesture-handler/src/__tests__/api_v3.test.tsx

View workflow job for this annotation

GitHub Actions / check

Unsafe argument of type `any` assigned to a parameter of type `GestureHandlerEventWithHandlerData<PanHandlerData, PanExtendedHandlerData>`
}).not.toThrow();

expect(onUpdate).not.toHaveBeenCalled();
expect(onTouchesUp).not.toHaveBeenCalled();
});

test('Pan gesture handles valid update events after a malformed one', () => {
const onUpdate = jest.fn();

const panGesture = renderHook(() =>
usePanGesture({
disableReanimated: true,
onUpdate: (e) => onUpdate(e),

Check warning on line 91 in packages/react-native-gesture-handler/src/__tests__/api_v3.test.tsx

View workflow job for this annotation

GitHub Actions / check

Unsafe return of an `any` typed value
})
).result.current;

const { jsEventHandler } = panGesture.detectorCallbacks;

jsEventHandler?.({
handlerTag: panGesture.handlerTag,
state: State.ACTIVE,
eventType: 3,
numberOfTouches: 0,
changedTouches: [],
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any);

jsEventHandler?.({
handlerTag: panGesture.handlerTag,
state: State.ACTIVE,
handlerData: {
translationX: 10,
translationY: 5,
x: 10,
y: 5,
absoluteX: 10,
absoluteY: 5,
velocityX: 0,
velocityY: 0,
stylusData: undefined,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any,
});

expect(onUpdate).toHaveBeenCalledTimes(1);
expect(onUpdate).toHaveBeenCalledWith(
expect.objectContaining({ changeX: 10, changeY: 5 })
);
});
});

describe('[API v3] Components', () => {
Expand Down
45 changes: 45 additions & 0 deletions packages/react-native-gesture-handler/src/__tests__/utils.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { withPrevAndCurrent } from '../utils';
import { getChangeEventCalculator } from '../v3/hooks/utils/eventUtils';
import type { GestureUpdateEventWithHandlerData } from '../v3/types';

describe('withPrevAndCurrent', () => {
test('returns transformed elements', () => {
Expand All @@ -14,3 +16,46 @@ describe('withPrevAndCurrent', () => {
expect(withPrevAndCurrent([], concat)).toEqual([]);
});
});

describe('getChangeEventCalculator', () => {
type TestHandlerData = { translationX: number; changeX?: number };

const diffCalculator = (
current: TestHandlerData,
previous: TestHandlerData | null
) => ({
changeX: previous
? current.translationX - previous.translationX
: current.translationX,
});

const calculator = getChangeEventCalculator(diffCalculator);

const makeEvent = (handlerData?: TestHandlerData) =>
({
handlerTag: 1,
state: 4,
handlerData,
}) as GestureUpdateEventWithHandlerData<TestHandlerData>;

test('computes change payload for well-formed events', () => {
const first = calculator(makeEvent({ translationX: 10 }));
expect(first.handlerData).toEqual({ translationX: 10, changeX: 10 });

const second = calculator(
makeEvent({ translationX: 25 }),
makeEvent({ translationX: 10 })
);
expect(second.handlerData).toEqual({ translationX: 25, changeX: 15 });
});

test('returns event untouched when handlerData is missing', () => {
// Regression: a malformed event without `handlerData` used to crash the
// diff calculator with "Cannot read property 'translationX' of undefined".
const malformed = makeEvent(undefined);

expect(() => calculator(malformed)).not.toThrow();
expect(calculator(malformed)).toBe(malformed);
expect(malformed.handlerData).toBeUndefined();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,11 @@ export function eventHandler<
return;
}

// Guard against malformed events
if (eventWithData.handlerData === undefined) {
return;
}

if (!dispatchesAnimatedEvents) {
handleUpdateEvent(
eventWithData,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,12 @@ export function getChangeEventCalculator<TExtendedHandlerData>(
) => {
'worklet';
const currentEventData = current.handlerData;

// Guard against malformed events
if (currentEventData === undefined) {
return current;
}

const previousEventData = previous ? previous.handlerData : null;

const changePayload = diffCalculator(currentEventData, previousEventData);
Expand Down
Loading