Skip to content

Latest commit

 

History

History
275 lines (218 loc) · 9.61 KB

File metadata and controls

275 lines (218 loc) · 9.61 KB
id testing
title Testing with Jest
sidebar_position 4

import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem';

Setup

Jest configuration

In order to use functions provided by Gesture Handler, add react-native-gesture-handler to transformIgnorePatterns in jest config.

transformIgnorePatterns: [
  'node_modules/(?!((jest-)?react-native|react-native-gesture-handler)/)',
],

:::note Be careful when adding multiple entries to transformIgnorePatterns. Since Jest ignores a file if it matches any pattern in the list, splitting negative lookaheads into separate strings often causes them to override each other. It is safer to combine your exceptions into a single regex using the | operator. See the Jest documentation for an example. :::

Mocking native modules

In order to load mocks provided by RNGH, add the following to your jest config:

"setupFiles": ["./node_modules/react-native-gesture-handler/jestSetup.js"]

Example jest config

```js module.exports = { preset: '@react-native/jest-preset', transformIgnorePatterns: [ 'node_modules/(?!((jest-)?react-native|react-native-gesture-handler)/)', ], setupFiles: ['react-native-gesture-handler/jestSetup.js'], }; ``` ```json "jest": { "preset": "@react-native/jest-preset", "transformIgnorePatterns": [ "node_modules/(?!((jest-)?react-native|react-native-gesture-handler)/)" ], "setupFiles": [ "react-native-gesture-handler/jestSetup.js" ] } ```

Testing Gestures' and Gesture handlers' callbacks

RNGH provides the following APIs for triggering selected handlers:

createGestureController

<CollapsibleCode label="Show composed types definitions" expandedLabel="Hide composed types definitions" lineBounds={[0, 3]} src={` import { createGestureController } from 'react-native-gesture-handler/jest-utils';

createGestureController: (componentOrGesture) => GestureController;

export interface GestureController< TEventPayload extends Record<string, unknown> = Record<string, unknown>,

{ begin: (event?: GestureControllerEvent) => void; activate: (event?: GestureControllerEvent) => void; update: (event?: GestureControllerEvent) => void; end: (event?: GestureControllerEvent) => void; fail: (event?: GestureControllerEvent) => void; cancel: (event?: GestureControllerEvent) => void; }

export type GestureControllerEvent< TEventPayload extends Record<string, unknown> = Record<string, unknown>,

= Partial & { handlerTag?: never; nativeEvent?: never; oldState?: never; state?: never; }; `}/>

Creates an imperative controller that dispatches gesture lifecycle events one step at a time. This allows the test to assert application state between lifecycle steps without manually supplying state, oldState, or handlerTag.

componentOrGesture can be:

  • A Gesture Handler component found using a Jest query such as getByTestId.
  • A gesture object.
  • A gesture test ID string.

When a gesture object is passed directly, the event payload type is inferred from the gesture. Every lifecycle method accepts an optional partial event payload and fills omitted handler-specific properties with defaults.

The controller exposes the following methods:

Method Behavior
begin() Starts a stream and calls onBegin. If the previous stream finished, the controller resets it before beginning.
activate() Activates a begun stream and calls onActivate.
update() Dispatches an update for an active stream and calls onUpdate. It can be called multiple times.
end() Ends a begun or active stream. Calls onDeactivate if active, then onFinalize with canceled: false.
fail() Fails a begun or active stream. Calls onDeactivate if active, then onFinalize with canceled: true.
cancel() Cancels a begun or active stream. Calls onDeactivate if active, then onFinalize with canceled: true.

Calling begin() again after end(), fail(), or cancel() starts another stream with the same controller. Calling methods in an invalid order throws an error. State-machine fields such as state, oldState, handlerTag, and nativeEvent cannot be supplied in event payloads because the controller manages them internally.

test('updates application state after each gesture step', () => {
  const onBegin = jest.fn();
  const onActivate = jest.fn();
  const onUpdate = jest.fn();
  const onDeactivate = jest.fn();
  const onFinalize = jest.fn();

  const panGesture = renderHook(() =>
    usePanGesture({
      disableReanimated: true,
      onBegin,
      onActivate,
      onUpdate,
      onDeactivate,
      onFinalize,
    })
  ).result.current;

  const controller = createGestureController(panGesture);

  controller.begin();
  expect(onBegin).toHaveBeenCalledTimes(1);

  controller.activate();
  expect(onActivate).toHaveBeenCalledTimes(1);

  controller.update({ translationX: 50 });
  expect(onUpdate).toHaveBeenCalledWith(
    expect.objectContaining({ translationX: 50 })
  );

  controller.end();
  expect(onDeactivate).toHaveBeenCalledTimes(1);
  expect(onFinalize).toHaveBeenCalledWith(
    expect.objectContaining({ canceled: false })
  );
});

:::note createGestureController controls lifecycle events directly. It does not generate pointer input, run platform gesture recognizers, or evaluate relations between gestures. :::

fireGestureHandler

import { fireGestureHandler } from 'react-native-gesture-handler/jest-utils';

fireGestureHandler: (componentOrGesture, eventList) => void;

Simulates one event stream (i.e. event sequence starting with BEGIN state and ending with one of END/FAIL/CANCEL states), calling appropriate callbacks associated with given gesture handler.

  • componentOrGesture - Either Gesture Handler component found by Jest queries (e.g. getByTestId) or Gesture found by getByGestureTestId()

  • eventList - Event data passed to appropriate callback. RNGH fills event list if required data is missing using these rules:

    • oldState is filled using state of the previous event. BEGIN events use UNDETERMINED value as previous event.
    • Events after first ACTIVE state can omit state field.
    • Handler specific data is filled (e.g. numberOfTouches, x fields) with defaults.
    • Missing BEGIN and END events are added with data copied from first and last passed event, respectively.
    • If first event doesn't have state field, the ACTIVE state is assumed.

Some eventList examples:

const oldStateFilled = [
  { state: State.BEGAN },
  { state: State.ACTIVE },
  { state: State.END },
]; // three events with specified state are fired.

const implicitActiveState = [
  { state: State.BEGAN },
  { state: State.ACTIVE },
  { x: 5 },
  { state: State.END },
]; // 4 events, including two ACTIVE events (second one has overridden additional data).

const implicitBegin = [
  { x: 1, y: 11 },
  { x: 2, y: 12, state: State.FAILED },
]; // 3 events, including implicit BEGAN, one ACTIVE, and one FAILED event with additional data.

const implicitBeginAndEnd = [
  { x: 5, y: 15 },
  { x: 6, y: 16 },
  { x: 7, y: 17 },
]; // 5 events, including 3 ACTIVE events and implicit BEGAN and END events. BEGAN uses first event's additional data, END uses last event's additional data.

const allImplicits = []; // 3 events, one BEGIN, one ACTIVE, one END with defaults.

getByGestureTestId

import { getByGestureTestId } from 'react-native-gesture-handler/jest-utils';

getByGestureTestId: (testID: string) => Gesture;

Returns opaque data type associated with gesture. Gesture is found via testID attribute in rendered components.

:::warning testID must be unique among components rendered in test. :::

fireGestureHandler example

Extracted from RNGH tests, check api_v3.test.tsx for full implementation.

test('Pan gesture', () => {
  const onBegin = jest.fn();
  const onStart = jest.fn();

  const panGesture = renderHook(() =>
    usePanGesture({
      disableReanimated: true,
      onBegin: (e) => onBegin(e),
      onActivate: (e) => onStart(e),
    })
  ).result.current;

  fireGestureHandler(panGesture, [
    { oldState: State.UNDETERMINED, state: State.BEGAN },
    { oldState: State.BEGAN, state: State.ACTIVE },
    { oldState: State.ACTIVE, state: State.ACTIVE },
    { oldState: State.ACTIVE, state: State.END },
  ]);

  expect(onBegin).toHaveBeenCalledTimes(1);
  expect(onStart).toHaveBeenCalledTimes(1);
});