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
5 changes: 5 additions & 0 deletions .changeset/olive-bugs-prove.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@lynx-js/gesture-runtime': minor
---

Initialize `'@lynx-js/gesture-runtime`
1 change: 1 addition & 0 deletions biome.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@

"packages/testing-library/**",
"packages/react/testing-library/**",
"packages/lynx/gesture-runtime/__test__/**",
],
"rules": {
// We are migrating from ESLint to Biome
Expand Down
2 changes: 2 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ export default tseslint.config(
'packages/testing-library/**',
'packages/react/testing-library/**',

// gesture-runtime-testing
'packages/lynx/gesture-runtime/__test__/**',
// TODO: enable eslint for tailwind-preset
// tailwind-preset
'packages/tailwind-preset/**',
Expand Down
Empty file.
21 changes: 21 additions & 0 deletions packages/lynx/gesture-runtime/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Lynx Gesture Runtime

`@lynx-js/gesture-runtime` provides typed gesture primitives and simple composition utilities for Lynx.

## Install

- `pnpm add @lynx-js/gesture-runtime @lynx-js/react`

## Usage

```tsx
import { useGesture, PanGesture } from '@lynx-js/gesture-runtime';

export default function Example() {
const pan = useGesture(PanGesture).onUpdate((event, stateManager) => {
'main thread';
stateManager.active();
});
return <view main-thread:gesture={pan}></view>;
}
```
323 changes: 323 additions & 0 deletions packages/lynx/gesture-runtime/__test__/gesture-callback.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,323 @@
// Copyright 2025 The Lynx Authors. All rights reserved.
// Licensed under the Apache License Version 2.0 that can be found in the
// LICENSE file in the root directory of this source tree.
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import type { MockInstance } from 'vitest';

import { useRef } from '@lynx-js/react';
import { act, render } from '@lynx-js/react/testing-library';

import { PanGesture } from '../src/index.js';
import { useGesture } from '../src/useGesture.js';
import {
MockGestureManager,
genEventObj,
triggerGestureCallback,
} from './utils/callback.js';

describe('gestures mt', () => {
let spySetGesture: MockInstance;
let _gestureNode: any;

beforeEach(() => {
spySetGesture = vi.spyOn(
lynxTestingEnv.mainThread.globalThis,
'__SetGestureDetector',
).mockImplementation(function(
node: any,
id: number,
type: number,
config: any,
relationMap: Record<string, number[]>,
) {
node.gesture = {
id,
type,
config,
relationMap,
};

_gestureNode = node;
});
});

afterEach(() => {
spySetGesture.mockRestore();
_gestureNode = null;
});

test('bind gestures should call papi correctly', async () => {
let _panGesture;
const mockGestureManager = new MockGestureManager();

const App = () => {
const panGesture = useGesture(PanGesture);
panGesture.onBegin((event, stateManager) => {
'main thread';
globalThis._eventObj = event;
});

_panGesture = panGesture;
return (
<view>
<view main-thread:gesture={panGesture}></view>
</view>
);
};

await act(() => {
const { container } = render(<App />, {
enableMainThread: true,
enableBackgroundThread: true,
});
});

await act(() => {
const eventObj = genEventObj(_gestureNode, {
params: { x: 0, y: 1 },
});

triggerGestureCallback(
_gestureNode,
'onBegin',
eventObj,
mockGestureManager,
);
});

expect(globalThis._eventObj.params.x).toBe(0);
expect(globalThis._eventObj.params.y).toBe(1);

delete globalThis._eventObj;
});

test('stateManager.active should call __SetGestureDetector correctly', async () => {
let _panGesture;
const mockGestureManager = new MockGestureManager();

const App = () => {
const panGesture = useGesture(PanGesture);
panGesture.onBegin((event, stateManager) => {
'main thread';
stateManager.active();
});

_panGesture = panGesture;
return (
<view>
<view main-thread:gesture={panGesture}></view>
</view>
);
};

await act(() => {
const { container } = render(<App />, {
enableMainThread: true,
enableBackgroundThread: true,
});
});

await act(() => {
const eventObj = genEventObj(_gestureNode, {
params: { x: 0, y: 1 },
});

triggerGestureCallback(
_gestureNode,
'onBegin',
eventObj,
mockGestureManager,
);
});

expect(mockGestureManager.__SetGestureState).toBeCalledWith(
_gestureNode,
_gestureNode.gesture.id,
1,
);
});

test('stateManager.fail should call __SetGestureDetector correctly', async () => {
let _panGesture;
const mockGestureManager = new MockGestureManager();

const App = () => {
const panGesture = useGesture(PanGesture);
panGesture.onBegin((event, stateManager) => {
'main thread';
stateManager.fail();
});

_panGesture = panGesture;
return (
<view>
<view main-thread:gesture={panGesture}></view>
</view>
);
};

await act(() => {
const { container } = render(<App />, {
enableMainThread: true,
enableBackgroundThread: true,
});
});

await act(() => {
const eventObj = genEventObj(_gestureNode, {
params: { x: 0, y: 1 },
});

triggerGestureCallback(
_gestureNode,
'onBegin',
eventObj,
mockGestureManager,
);
});

expect(mockGestureManager.__SetGestureState).toBeCalledWith(
_gestureNode,
_gestureNode.gesture.id,
2,
);
});

test('stateManager.end should call __SetGestureDetector correctly', async () => {
let _panGesture;
const mockGestureManager = new MockGestureManager();

const App = () => {
const panGesture = useGesture(PanGesture);
panGesture.onBegin((event, stateManager) => {
'main thread';
stateManager.end();
});

_panGesture = panGesture;
return (
<view>
<view main-thread:gesture={panGesture}></view>
</view>
);
};

await act(() => {
const { container } = render(<App />, {
enableMainThread: true,
enableBackgroundThread: true,
});
});

await act(() => {
const eventObj = genEventObj(_gestureNode, {
params: { x: 0, y: 1 },
});

triggerGestureCallback(
_gestureNode,
'onBegin',
eventObj,
mockGestureManager,
);
});

expect(mockGestureManager.__SetGestureState).toBeCalledWith(
_gestureNode,
_gestureNode.gesture.id,
3,
);
});

test('stateManager.consumeGesture should call __ConsumeGesture correctly', async () => {
let _panGesture;
const mockGestureManager = new MockGestureManager();

const App = () => {
const panGesture = useGesture(PanGesture);
panGesture.onBegin((event, stateManager) => {
'main thread';
stateManager.consumeGesture(true);
});

_panGesture = panGesture;
return (
<view>
<view main-thread:gesture={panGesture}></view>
</view>
);
};

await act(() => {
const { container } = render(<App />, {
enableMainThread: true,
enableBackgroundThread: true,
});
});

await act(() => {
const eventObj = genEventObj(_gestureNode, {
params: { x: 0, y: 1 },
});

triggerGestureCallback(
_gestureNode,
'onBegin',
eventObj,
mockGestureManager,
);
});

expect(mockGestureManager.__ConsumeGesture).toBeCalledWith(
_gestureNode,
_gestureNode.gesture.id,
{ consume: true, inner: true },
);
});

test('stateManager.interceptGesture should call __ConsumeGesture correctly', async () => {
let _panGesture;
const mockGestureManager = new MockGestureManager();

const App = () => {
const panGesture = useGesture(PanGesture);
panGesture.onBegin((event, stateManager) => {
'main thread';
stateManager.interceptGesture(true);
});

_panGesture = panGesture;
return (
<view>
<view main-thread:gesture={panGesture}></view>
</view>
);
};

await act(() => {
const { container } = render(<App />, {
enableMainThread: true,
enableBackgroundThread: true,
});
});

await act(() => {
const eventObj = genEventObj(_gestureNode, {
params: { x: 0, y: 1 },
});

triggerGestureCallback(
_gestureNode,
'onBegin',
eventObj,
mockGestureManager,
);
});

expect(mockGestureManager.__ConsumeGesture).toBeCalledWith(
_gestureNode,
_gestureNode.gesture.id,
{ consume: true, inner: false },
);
});
});
Loading
Loading