Skip to content
Draft
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
@@ -0,0 +1,195 @@
import { renderHook } from '@testing-library/react-native';

import { createGestureController } from '../jestUtils';
import { State } from '../State';
import type { PanGesture, TapGesture } from '../v3/hooks/gestures';
import { usePanGesture, useTapGesture } from '../v3/hooks/gestures';

function mockedContinuousCallbacks() {
const order: string[] = [];

return {
order,
callbacks: {
onBegin: jest.fn(() => order.push('onBegin')),
onActivate: jest.fn(() => order.push('onActivate')),
onUpdate: jest.fn(() => order.push('onUpdate')),
onDeactivate: jest.fn(() => order.push('onDeactivate')),
onFinalize: jest.fn(() => order.push('onFinalize')),
},
};
}

function mockedDiscreteCallbacks() {
const order: string[] = [];

return {
order,
callbacks: {
onBegin: jest.fn(() => order.push('onBegin')),
onActivate: jest.fn(() => order.push('onActivate')),
onDeactivate: jest.fn(() => order.push('onDeactivate')),
onFinalize: jest.fn(() => order.push('onFinalize')),
},
};
}

function assertGestureControllerTypes(pan: PanGesture, tap: TapGesture) {
const panController = createGestureController(pan);
panController.begin({ x: 1 });
panController.update({ translationX: 1, velocityX: 2 });
// @ts-expect-error unknown pan payload field
panController.update({ translationXX: 1 });
// @ts-expect-error raw state-machine fields are managed internally
panController.begin({ state: State.BEGAN });

const tapController = createGestureController(tap);
tapController.begin({ x: 1 });
// @ts-expect-error tap payloads do not include pan translation fields
tapController.update({ translationX: 1 });

const stringController = createGestureController<{ translationX: number }>(
'pan-id'
);
stringController.update({ translationX: 1 });
// @ts-expect-error explicit payload type is respected for string targets
stringController.update({ translationXX: 1 });
}

void assertGestureControllerTypes;

describe('createGestureController', () => {
test('allows assertions after each gesture lifecycle step', () => {
const { order, callbacks } = mockedContinuousCallbacks();
const pan = renderHook(() =>
usePanGesture({ disableReanimated: true, ...callbacks })
).result.current;
const gesture = createGestureController(pan);

gesture.begin({ translationX: 0 });

expect(gesture.getState()).toBe(State.BEGAN);
expect(order).toEqual(['onBegin']);
expect(callbacks.onBegin).toHaveBeenCalledWith(
expect.objectContaining({ translationX: 0 })
);

gesture.activate({ translationX: 10 });

expect(gesture.getState()).toBe(State.ACTIVE);
expect(order).toEqual(['onBegin', 'onActivate']);
expect(callbacks.onActivate).toHaveBeenCalledWith(
expect.objectContaining({ translationX: 10 })
);

gesture.update({ translationX: 50 });

expect(gesture.getState()).toBe(State.ACTIVE);
expect(order).toEqual(['onBegin', 'onActivate', 'onUpdate']);
expect(callbacks.onUpdate).toHaveBeenCalledWith(
expect.objectContaining({ translationX: 50 })
);

gesture.end({ translationX: 50 });

expect(gesture.getState()).toBe(State.END);
expect(order).toEqual([
'onBegin',
'onActivate',
'onUpdate',
'onDeactivate',
'onFinalize',
]);
expect(callbacks.onFinalize).toHaveBeenCalledWith(
expect.objectContaining({ canceled: false, translationX: 50 })
);
});

test('can fail a gesture before activation', () => {
const { order, callbacks } = mockedDiscreteCallbacks();
const tap = renderHook(() =>
useTapGesture({ disableReanimated: true, ...callbacks })
).result.current;
const gesture = createGestureController(tap);

gesture.begin({ x: 10 });
gesture.fail({ x: 10 });

expect(gesture.getState()).toBe(State.FAILED);
expect(order).toEqual(['onBegin', 'onFinalize']);
expect(callbacks.onActivate).not.toHaveBeenCalled();
expect(callbacks.onDeactivate).not.toHaveBeenCalled();
expect(callbacks.onFinalize).toHaveBeenCalledWith(
expect.objectContaining({ canceled: true, x: 10 })
);
});

test('resolves gesture test ID strings internally', () => {
const { order, callbacks } = mockedDiscreteCallbacks();
renderHook(() =>
useTapGesture({
testID: 'controlled-tap',
disableReanimated: true,
...callbacks,
})
);
const gesture = createGestureController('controlled-tap');

gesture.begin();
gesture.activate();
gesture.end();

expect(order).toEqual([
'onBegin',
'onActivate',
'onDeactivate',
'onFinalize',
]);
});

test('guards against invalid lifecycle order', () => {
const tap = renderHook(() => useTapGesture({ disableReanimated: true }))
.result.current;
const gesture = createGestureController(tap);

expect(() => gesture.update()).toThrow(
'Cannot update gesture from UNDETERMINED state.'
);

gesture.begin();

expect(() => gesture.begin()).toThrow(
'Cannot begin gesture from BEGAN state.'
);
});

test('rejects raw state-machine fields in event payloads', () => {
const tap = renderHook(() => useTapGesture({ disableReanimated: true }))
.result.current;
const gesture = createGestureController(tap);

expect(() => gesture.begin({ state: State.BEGAN } as never)).toThrow(
"createGestureController manages 'state' internally."
);
});

test('disabled gestures are no-ops', () => {
const { order, callbacks } = mockedContinuousCallbacks();
const pan = renderHook(() =>
usePanGesture({
disableReanimated: true,
enabled: false,
...callbacks,
})
).result.current;
const gesture = createGestureController(pan);

gesture.begin();
gesture.activate();
gesture.update({ translationX: 50 });
gesture.end();

expect(gesture.getState()).toBe(State.UNDETERMINED);
expect(order).toEqual([]);
});
});
7 changes: 6 additions & 1 deletion packages/react-native-gesture-handler/src/jestUtils/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
export { fireGestureHandler, getByGestureTestId } from './jestUtils';
export type { GestureController, GestureControllerEvent } from './jestUtils';
export {
createGestureController,
fireGestureHandler,
getByGestureTestId,
} from './jestUtils';
Loading
Loading