@sigx/lynx-testing 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +236 -0
- package/dist/fire-event.d.ts +57 -0
- package/dist/fire-event.d.ts.map +1 -0
- package/dist/flush.d.ts +29 -0
- package/dist/flush.d.ts.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +305 -0
- package/dist/index.js.map +1 -0
- package/dist/mt/index.d.ts +148 -0
- package/dist/mt/index.d.ts.map +1 -0
- package/dist/mt/index.js +114 -0
- package/dist/mt/index.js.map +1 -0
- package/dist/mt/setup.d.ts +50 -0
- package/dist/mt/setup.d.ts.map +1 -0
- package/dist/mt/setup.js +2 -0
- package/dist/queries.d.ts +11 -0
- package/dist/queries.d.ts.map +1 -0
- package/dist/render.d.ts +38 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/setup-BUzWSuVx.js +55 -0
- package/dist/setup-BUzWSuVx.js.map +1 -0
- package/dist/test-node.d.ts +29 -0
- package/dist/test-node.d.ts.map +1 -0
- package/dist/test-renderer.d.ts +6 -0
- package/dist/test-renderer.d.ts.map +1 -0
- package/package.json +76 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-2026 Andreas Ekdahl
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
# @sigx/lynx-testing
|
|
2
|
+
|
|
3
|
+
Vitest-native testing utilities for sigx-lynx components — render, fire events, query the rendered tree, and wait for reactive updates.
|
|
4
|
+
|
|
5
|
+
Renders into an in-memory `TestNode` tree (no Lynx runtime, no PAPI mocks for the BG side). Pair with vitest in your project — no Jest, no preset.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add -D @sigx/lynx-testing vitest
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick start
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import { describe, it, expect } from 'vitest';
|
|
15
|
+
import { render, fireEvent, act } from '@sigx/lynx-testing';
|
|
16
|
+
import { component, signal, jsx } from '@sigx/lynx';
|
|
17
|
+
|
|
18
|
+
it('updates on tap', async () => {
|
|
19
|
+
const count = signal({ value: 0 });
|
|
20
|
+
const Counter = component(() => {
|
|
21
|
+
return () => jsx('view', {
|
|
22
|
+
bindtap: () => { count.value++; },
|
|
23
|
+
children: [jsx('text', { children: String(count.value) })],
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const { container, getByType, getByText } = render(jsx(Counter, {}));
|
|
28
|
+
expect(getByText('0')).toBeTruthy();
|
|
29
|
+
|
|
30
|
+
await act(() => fireEvent.tap(getByType('view')));
|
|
31
|
+
expect(getByText('1')).toBeTruthy();
|
|
32
|
+
});
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## API
|
|
36
|
+
|
|
37
|
+
### `render(element, { appContext? }) → RenderResult`
|
|
38
|
+
|
|
39
|
+
Mounts a JSX element into a fresh `TestNode` tree. Returns:
|
|
40
|
+
|
|
41
|
+
- `container: TestNode` — the root node
|
|
42
|
+
- `unmount(): void` — tears down the render
|
|
43
|
+
- `getByType(type) / getAllByType(type) / queryByType(type)` — find by element name (`'view'`, `'text'`, `'scroll-view'`, …)
|
|
44
|
+
- `getByText(text) / queryByText(text)` — substring match on text content
|
|
45
|
+
- `getByProp(key, value)` — find by an arbitrary prop value (`getByProp('id', 'submit')`)
|
|
46
|
+
- `debug(): string` — pretty-print the rendered tree
|
|
47
|
+
|
|
48
|
+
`get*` throws if no node matches; `query*` returns `null`.
|
|
49
|
+
|
|
50
|
+
### `fireEvent`
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
fireEvent.tap(node, { x?, y? })
|
|
54
|
+
fireEvent.touchStart(node, { touches?, changedTouches? })
|
|
55
|
+
fireEvent.touchMove(node, { touches?, changedTouches? })
|
|
56
|
+
fireEvent.touchEnd(node, { touches?, changedTouches? })
|
|
57
|
+
fireEvent.touchCancel(node, { touches?, changedTouches? })
|
|
58
|
+
fireEvent.scroll(node, { detail: { scrollTop?, scrollLeft?, deltaX?, deltaY?, ... } })
|
|
59
|
+
fireEvent.input(node, { detail: { value? } })
|
|
60
|
+
fireEvent.longPress(node)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Each method dispatches both the `bind*` form (`bindtap`, `bindscroll`, …) and the camelCase form (`onTap`, `onScroll`, …) so component-vs-element handler shapes both fire.
|
|
64
|
+
|
|
65
|
+
### `touch(pageX, pageY, identifier = 1)`
|
|
66
|
+
|
|
67
|
+
Helper for building synthetic touch objects:
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
fireEvent.touchMove(view, { touches: [touch(150, 200)] });
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### `act(fn)` / `waitForUpdate()`
|
|
74
|
+
|
|
75
|
+
Reactive flush helpers. Wrap signal mutations in `act` so the renderer commits before you assert:
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
await act(() => {
|
|
79
|
+
state.count = 5;
|
|
80
|
+
state.name = 'Alice';
|
|
81
|
+
});
|
|
82
|
+
expect(getByText('5')).toBeTruthy();
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`waitForUpdate()` is the bare flush — useful when the mutation already happened (e.g. inside an event handler).
|
|
86
|
+
|
|
87
|
+
## Vitest config
|
|
88
|
+
|
|
89
|
+
A minimal `vitest.config.ts` for an app using sigx-lynx:
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
import { defineConfig } from 'vitest/config';
|
|
93
|
+
|
|
94
|
+
export default defineConfig({
|
|
95
|
+
oxc: {
|
|
96
|
+
jsx: { runtime: 'automatic', importSource: 'sigx' },
|
|
97
|
+
},
|
|
98
|
+
test: {
|
|
99
|
+
environment: 'happy-dom',
|
|
100
|
+
include: ['src/**/__tests__/**/*.test.{ts,tsx}'],
|
|
101
|
+
globals: true,
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Testing MT-thread components
|
|
107
|
+
|
|
108
|
+
`render()` mounts the **BG side** of your component. Worklet props (`main-thread-bindtap`, `main-thread:ref`, `'main thread'`-marked function bodies) are rendered as inert handlers — the SWC worklet transform does not run, and the MT runtime is not booted. So `render()`-driven tests cover JSX shape + signal-driven re-renders, not worklet behavior.
|
|
109
|
+
|
|
110
|
+
For end-to-end MT coverage, use the `@sigx/lynx-testing/mt` subpath. It boots the upstream `@lynx-js/react/worklet-runtime`, runs the SWC `'main thread'` transform on real source, eval's the extracted `registerWorkletInternal(...)` calls, and hands back the registered worklets so you can drive them with fabricated events.
|
|
111
|
+
|
|
112
|
+
### Setup
|
|
113
|
+
|
|
114
|
+
Add a separate vitest config that picks up `*.mt.test.ts` files. The MT harness is heavier than the BG `render()` path — keeping it in its own config avoids paying the bootstrap cost on every BG test.
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
// vitest.mt.config.ts
|
|
118
|
+
import { defineConfig } from 'vitest/config';
|
|
119
|
+
|
|
120
|
+
export default defineConfig({
|
|
121
|
+
test: {
|
|
122
|
+
environment: 'happy-dom',
|
|
123
|
+
globals: true,
|
|
124
|
+
include: ['__tests__/**\/*.mt.test.ts'],
|
|
125
|
+
setupFiles: ['@sigx/lynx-testing/mt/setup'],
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
```jsonc
|
|
131
|
+
// package.json
|
|
132
|
+
{
|
|
133
|
+
"scripts": {
|
|
134
|
+
"test:mt": "vitest run --config vitest.mt.config.ts"
|
|
135
|
+
},
|
|
136
|
+
"devDependencies": {
|
|
137
|
+
"@lynx-js/react": "^0.119.0",
|
|
138
|
+
"@sigx/lynx-runtime-main": "workspace:^",
|
|
139
|
+
"@sigx/lynx-testing": "workspace:^",
|
|
140
|
+
"vitest": "^4"
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`@lynx-js/react` and `@sigx/lynx-runtime-main` are peer dependencies of `@sigx/lynx-testing/mt` and must be installed by the consumer (they ship the worklet runtime + the MT bootstrap that the setup file imports).
|
|
146
|
+
|
|
147
|
+
### Worked example
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
// __tests__/my-button.mt.test.ts
|
|
151
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
152
|
+
import {
|
|
153
|
+
compileMTWorklets,
|
|
154
|
+
fabricateTapEvent,
|
|
155
|
+
makeRef,
|
|
156
|
+
} from '@sigx/lynx-testing/mt';
|
|
157
|
+
import { readFileSync } from 'fs';
|
|
158
|
+
import { resolve } from 'path';
|
|
159
|
+
|
|
160
|
+
const SRC = resolve(__dirname, '../src/components/MyButton.tsx');
|
|
161
|
+
|
|
162
|
+
describe('MyButton — MT worklets', () => {
|
|
163
|
+
let onBegin: Function;
|
|
164
|
+
let onStart: Function;
|
|
165
|
+
|
|
166
|
+
beforeEach(() => {
|
|
167
|
+
// Compiles the source through the SWC LEPUS transform, eval's the
|
|
168
|
+
// emitted `registerWorkletInternal(...)` calls, and returns the
|
|
169
|
+
// worklets in source order.
|
|
170
|
+
const worklets = compileMTWorklets({
|
|
171
|
+
filename: SRC,
|
|
172
|
+
source: readFileSync(SRC, 'utf8'),
|
|
173
|
+
});
|
|
174
|
+
expect(worklets).toHaveLength(2); // your component's worklet count
|
|
175
|
+
[onBegin, onStart] = worklets;
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('onBegin sets pressed-state styles', () => {
|
|
179
|
+
const setStyleProperties = vi.fn();
|
|
180
|
+
const ctx = {
|
|
181
|
+
_c: {
|
|
182
|
+
elRef: makeRef({ setStyleProperties }, 1),
|
|
183
|
+
opacity: 0.6,
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
onBegin.call(ctx, fabricateTapEvent());
|
|
187
|
+
expect(setStyleProperties).toHaveBeenCalledWith({ opacity: 0.6 });
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### API
|
|
193
|
+
|
|
194
|
+
#### `compileMTWorklets({ filename, source, runtimePkg? }) → Function[]`
|
|
195
|
+
|
|
196
|
+
Compile a `.tsx` source through the SWC LEPUS transform and register every `'main thread'`-marked function as a worklet on the live runtime that `setup.ts` bootstrapped. Returns the worklets in source order (top-to-bottom). Indexing into the returned array maps to your component's worklet declarations:
|
|
197
|
+
|
|
198
|
+
| Component shape | Index | Worklet |
|
|
199
|
+
|---|---|---|
|
|
200
|
+
| `Gesture.Pan().onBegin().onStart().onUpdate().onEnd()` | `[0]` | onBegin |
|
|
201
|
+
| | `[1]` | onStart |
|
|
202
|
+
| | `[2]` | onUpdate |
|
|
203
|
+
| | `[3]` | onEnd |
|
|
204
|
+
|
|
205
|
+
Pass `runtimePkg` if your project uses something other than `@sigx/lynx-runtime-main` (rare).
|
|
206
|
+
|
|
207
|
+
#### `fabricatePanEvent({ pageX, pageY? }) / fabricateTapEvent({ pageX?, pageY? })`
|
|
208
|
+
|
|
209
|
+
Synthetic gesture-event payloads matching what Lynx's iOS arena delivers to MT worklets. **Important:** `pageX/pageY` are nested under `e.params` (NOT top-level on `e`). This mirrors `LynxBaseGestureHandler.m::eventParamsFromTouchEvent`. Worklets that read `e.pageX` will get `undefined` against this fabricated event — they should read `e.params.pageX`.
|
|
210
|
+
|
|
211
|
+
#### `makeRef<T>(current, id?) → { current, _wvid }`
|
|
212
|
+
|
|
213
|
+
Synthetic `MainThreadRef` shape. Worklets read `ref.current.value` and may mutate it.
|
|
214
|
+
|
|
215
|
+
#### `getWorkletMap() → Record<string, Function>`
|
|
216
|
+
|
|
217
|
+
Direct access to `lynxWorkletImpl._workletMap`. Useful when you need to look up a specific `_wkltId` rather than rely on source order. Throws if `setup.ts` didn't run.
|
|
218
|
+
|
|
219
|
+
#### `getJsContext() / resetJsContextSpy()`
|
|
220
|
+
|
|
221
|
+
Read or reset the JS-context spy that the lynx mock installs. Use to assert `dispatchEvent` calls (e.g. `Lynx.Sigx.AvPublish` from a worklet's `runOnBackground`).
|
|
222
|
+
|
|
223
|
+
#### `extractRegistrations(lepusCode) → string`
|
|
224
|
+
|
|
225
|
+
Extract `registerWorkletInternal(...)` calls from a LEPUS-target transform output. `compileMTWorklets()` calls this internally; export it for callers that want to roll their own compile flow.
|
|
226
|
+
|
|
227
|
+
### Lynx native quirks worth knowing about
|
|
228
|
+
|
|
229
|
+
These are real arena behaviors observed on iOS Lynx 3.5 / Android Lynx 3.6 — your worklets must accommodate them. Not bugs in this harness; the harness exposes them:
|
|
230
|
+
|
|
231
|
+
1. **`Gesture.Pan` requires an empty `.onBegin()` on iOS.** The native handler only sets `_isInvokedBegin` inside an onBegin handler, and `onStart`/`onEnd` short-circuit if that flag is false. Register a no-op onBegin on Pan or onStart never fires.
|
|
232
|
+
2. **Pan event payload is nested under `e.params`.** Top-level `e` has only dispatch metadata (`type`, `timestamp`, `target`, `currentTarget`); pageX/pageY/scrollX/etc. live in `e.params` (and a duplicate `e.detail`). The fabricators above match this shape.
|
|
233
|
+
3. **iOS arena fires `Tap.onEnd` ~6ms after touchstart for sibling-composed gestures.** `Gesture.Simultaneous(Tap, LongPress)` looks like it should let both gestures resolve independently, but iOS dispatches a fail/reset path that fires Tap.onEnd before recognition completes. `Tap.onStart` then never fires. Workaround in `<Pressable>`: register only `Tap.onBegin` + `Tap.onStart` (no Tap.onEnd) and emit press from `LongPress.onEnd`'s no-`longPressFired`-and-no-movement fallback. See `packages/gestures/src/components/Pressable.tsx` for the full pattern.
|
|
234
|
+
4. **`<scroll-view>` doesn't participate in the new gesture arena.** Its UIKit `panGestureRecognizer` runs independently of `LynxGestureArenaManager`, so a Pan registered on a descendant fires concurrently with the parent scroll. `<ScrollView>` exposes a `useScrollContext`-published `dragging` signal that descendants flip during their drag lifecycle as the workaround.
|
|
235
|
+
|
|
236
|
+
This pattern catches regressions like the Phase 2 "cannot read property bind of undefined" bug — silent through every source-shape regex test, would have failed at the e2e harness's `expect(workletFns.length).toBe(N)` assertion.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fire synthetic events on TestNode elements.
|
|
3
|
+
*/
|
|
4
|
+
import { TestNode } from './test-node.js';
|
|
5
|
+
interface SyntheticTouch {
|
|
6
|
+
identifier?: number;
|
|
7
|
+
x?: number;
|
|
8
|
+
y?: number;
|
|
9
|
+
pageX?: number;
|
|
10
|
+
pageY?: number;
|
|
11
|
+
clientX?: number;
|
|
12
|
+
clientY?: number;
|
|
13
|
+
}
|
|
14
|
+
interface SyntheticTouchEvent {
|
|
15
|
+
touches?: SyntheticTouch[];
|
|
16
|
+
changedTouches?: SyntheticTouch[];
|
|
17
|
+
}
|
|
18
|
+
interface SyntheticScrollEvent {
|
|
19
|
+
detail?: {
|
|
20
|
+
scrollTop?: number;
|
|
21
|
+
scrollLeft?: number;
|
|
22
|
+
scrollHeight?: number;
|
|
23
|
+
scrollWidth?: number;
|
|
24
|
+
deltaX?: number;
|
|
25
|
+
deltaY?: number;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
interface SyntheticInputEvent {
|
|
29
|
+
detail?: {
|
|
30
|
+
value?: string;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/** Create a normalized touch object with defaults. */
|
|
34
|
+
export declare function touch(pageX: number, pageY: number, identifier?: number): SyntheticTouch;
|
|
35
|
+
export declare const fireEvent: {
|
|
36
|
+
/** Fire bindtap / onTap event. */
|
|
37
|
+
tap(node: TestNode, data?: {
|
|
38
|
+
x?: number;
|
|
39
|
+
y?: number;
|
|
40
|
+
}): void;
|
|
41
|
+
/** Fire bindtouchstart event. */
|
|
42
|
+
touchStart(node: TestNode, data?: SyntheticTouchEvent): void;
|
|
43
|
+
/** Fire bindtouchmove event. */
|
|
44
|
+
touchMove(node: TestNode, data?: SyntheticTouchEvent): void;
|
|
45
|
+
/** Fire bindtouchend event. */
|
|
46
|
+
touchEnd(node: TestNode, data?: SyntheticTouchEvent): void;
|
|
47
|
+
/** Fire bindtouchcancel event. */
|
|
48
|
+
touchCancel(node: TestNode, data?: SyntheticTouchEvent): void;
|
|
49
|
+
/** Fire bindscroll event. */
|
|
50
|
+
scroll(node: TestNode, data?: SyntheticScrollEvent): void;
|
|
51
|
+
/** Fire bindinput event. */
|
|
52
|
+
input(node: TestNode, data?: SyntheticInputEvent): void;
|
|
53
|
+
/** Fire bindlongpress event. */
|
|
54
|
+
longPress(node: TestNode): void;
|
|
55
|
+
};
|
|
56
|
+
export {};
|
|
57
|
+
//# sourceMappingURL=fire-event.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fire-event.d.ts","sourceRoot":"","sources":["../src/fire-event.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE1C,UAAU,cAAc;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,UAAU,mBAAmB;IAC3B,OAAO,CAAC,EAAE,cAAc,EAAE,CAAC;IAC3B,cAAc,CAAC,EAAE,cAAc,EAAE,CAAC;CACnC;AAED,UAAU,oBAAoB;IAC5B,MAAM,CAAC,EAAE;QACP,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,CAAC;CACH;AAED,UAAU,mBAAmB;IAC3B,MAAM,CAAC,EAAE;QACP,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,CAAC;CACH;AAED,sDAAsD;AACtD,wBAAgB,KAAK,CACnB,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EACb,UAAU,SAAI,GACb,cAAc,CAEhB;AAmBD,eAAO,MAAM,SAAS;IACpB,kCAAkC;IAClC,GAAG,OAAO,QAAQ,SAAS;QAAE,CAAC,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI;IAgB5D,iCAAiC;IACjC,UAAU,OAAO,QAAQ,SAAS,mBAAmB,GAAG,IAAI;IAI5D,gCAAgC;IAChC,SAAS,OAAO,QAAQ,SAAS,mBAAmB,GAAG,IAAI;IAI3D,+BAA+B;IAC/B,QAAQ,OAAO,QAAQ,SAAS,mBAAmB,GAAG,IAAI;IAI1D,kCAAkC;IAClC,WAAW,OAAO,QAAQ,SAAS,mBAAmB,GAAG,IAAI;IAI7D,6BAA6B;IAC7B,MAAM,OAAO,QAAQ,SAAS,oBAAoB,GAAG,IAAI;IAoBzD,4BAA4B;IAC5B,KAAK,OAAO,QAAQ,SAAS,mBAAmB,GAAG,IAAI;IAYvD,gCAAgC;IAChC,SAAS,OAAO,QAAQ,GAAG,IAAI;CAWhC,CAAC"}
|
package/dist/flush.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilities for waiting on reactive updates in tests.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Wait for all pending reactive effects and microtasks to complete.
|
|
6
|
+
* Use after mutating signal state to ensure the rendered tree is updated.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* state.count = 5;
|
|
11
|
+
* await waitForUpdate();
|
|
12
|
+
* expect(getByText(container, '5')).toBeTruthy();
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export declare function waitForUpdate(): Promise<void>;
|
|
16
|
+
/**
|
|
17
|
+
* Run a callback and wait for reactive effects to flush.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```ts
|
|
21
|
+
* await act(() => {
|
|
22
|
+
* state.count++;
|
|
23
|
+
* state.name = 'Alice';
|
|
24
|
+
* });
|
|
25
|
+
* // Tree is now updated
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export declare function act(fn: () => void | Promise<void>): Promise<void>;
|
|
29
|
+
//# sourceMappingURL=flush.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flush.d.ts","sourceRoot":"","sources":["../src/flush.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAQ7C;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,GAAG,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAGvE"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { render } from './render.js';
|
|
2
|
+
export type { RenderResult } from './render.js';
|
|
3
|
+
export { TestNode } from './test-node.js';
|
|
4
|
+
export { fireEvent, touch } from './fire-event.js';
|
|
5
|
+
export { waitForUpdate, act } from './flush.js';
|
|
6
|
+
export { getByType, getAllByType, getByText, queryByType, queryByText, getByProp, } from './queries.js';
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,YAAY,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC1C,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,EACL,SAAS,EACT,YAAY,EACZ,SAAS,EACT,WAAW,EACX,WAAW,EACX,SAAS,GACV,MAAM,cAAc,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import { createRenderer as e } from "@sigx/runtime-core/internals";
|
|
2
|
+
//#region src/test-node.ts
|
|
3
|
+
var t = class {
|
|
4
|
+
type;
|
|
5
|
+
props = {};
|
|
6
|
+
children = [];
|
|
7
|
+
parent = null;
|
|
8
|
+
text;
|
|
9
|
+
_handlers = /* @__PURE__ */ new Map();
|
|
10
|
+
_style = {};
|
|
11
|
+
_class = "";
|
|
12
|
+
constructor(e) {
|
|
13
|
+
this.type = e;
|
|
14
|
+
}
|
|
15
|
+
findByType(e) {
|
|
16
|
+
for (let t of this.children) {
|
|
17
|
+
if (t.type === e) return t;
|
|
18
|
+
let n = t.findByType(e);
|
|
19
|
+
if (n) return n;
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
findAllByType(e) {
|
|
24
|
+
let t = [];
|
|
25
|
+
for (let n of this.children) n.type === e && t.push(n), t.push(...n.findAllByType(e));
|
|
26
|
+
return t;
|
|
27
|
+
}
|
|
28
|
+
findByText(e) {
|
|
29
|
+
if (this.text !== void 0 && String(this.text).includes(e)) return this;
|
|
30
|
+
for (let t of this.children) {
|
|
31
|
+
let n = t.findByText(e);
|
|
32
|
+
if (n) return n;
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
textContent() {
|
|
37
|
+
return this.text === void 0 ? this.children.map((e) => e.textContent()).join("") : String(this.text);
|
|
38
|
+
}
|
|
39
|
+
toDebugString(e = 0) {
|
|
40
|
+
let t = " ".repeat(e);
|
|
41
|
+
if (this.type === "#text") return `${t}${JSON.stringify(this.text)}`;
|
|
42
|
+
if (this.type === "#comment") return `${t}<!-- -->`;
|
|
43
|
+
let n = Object.keys(this.props).length > 0 ? " " + Object.entries(this.props).map(([e, t]) => `${e}=${JSON.stringify(t)}`).join(" ") : "";
|
|
44
|
+
if (this.children.length === 0) return `${t}<${this.type}${n} />`;
|
|
45
|
+
let r = this.children.map((t) => t.toDebugString(e + 1)).join("\n");
|
|
46
|
+
return `${t}<${this.type}${n}>\n${r}\n${t}</${this.type}>`;
|
|
47
|
+
}
|
|
48
|
+
}, n = e({
|
|
49
|
+
createElement(e) {
|
|
50
|
+
return new t(e);
|
|
51
|
+
},
|
|
52
|
+
createText(e) {
|
|
53
|
+
let n = new t("#text");
|
|
54
|
+
return n.text = e, n;
|
|
55
|
+
},
|
|
56
|
+
createComment(e) {
|
|
57
|
+
return new t("#comment");
|
|
58
|
+
},
|
|
59
|
+
setText(e, t) {
|
|
60
|
+
e.text = t;
|
|
61
|
+
},
|
|
62
|
+
setElementText(e, n) {
|
|
63
|
+
e.children = [];
|
|
64
|
+
let r = new t("#text");
|
|
65
|
+
r.text = n, r.parent = e, e.children.push(r);
|
|
66
|
+
},
|
|
67
|
+
insert(e, t, n) {
|
|
68
|
+
if (e.parent) {
|
|
69
|
+
let t = e.parent.children.indexOf(e);
|
|
70
|
+
t !== -1 && e.parent.children.splice(t, 1);
|
|
71
|
+
}
|
|
72
|
+
if (e.parent = t, n) {
|
|
73
|
+
let r = t.children.indexOf(n);
|
|
74
|
+
if (r !== -1) {
|
|
75
|
+
t.children.splice(r, 0, e);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
t.children.push(e);
|
|
80
|
+
},
|
|
81
|
+
remove(e) {
|
|
82
|
+
if (e.parent) {
|
|
83
|
+
let t = e.parent.children.indexOf(e);
|
|
84
|
+
t !== -1 && e.parent.children.splice(t, 1), e.parent = null;
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
patchProp(e, t, n, r) {
|
|
88
|
+
t === "style" ? (e._style = r ?? {}, e.props[t] = r) : t === "class" ? (e._class = r ?? "", e.props[t] = r) : t.startsWith("bind") || t.startsWith("catch") || t.startsWith("on") || t.startsWith("main-thread-bind") || t.startsWith("main-thread-catch") || t.startsWith("global-") ? (typeof r == "function" ? e._handlers.set(t, r) : e._handlers.delete(t), e.props[t] = r) : r == null ? delete e.props[t] : e.props[t] = r;
|
|
89
|
+
},
|
|
90
|
+
parentNode(e) {
|
|
91
|
+
return e.parent;
|
|
92
|
+
},
|
|
93
|
+
nextSibling(e) {
|
|
94
|
+
if (!e.parent) return null;
|
|
95
|
+
let t = e.parent.children.indexOf(e);
|
|
96
|
+
return e.parent.children[t + 1] ?? null;
|
|
97
|
+
},
|
|
98
|
+
cloneNode(e) {
|
|
99
|
+
return new t(e.type);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
//#endregion
|
|
103
|
+
//#region src/queries.ts
|
|
104
|
+
function r(e, t) {
|
|
105
|
+
let n = e.findByType(t);
|
|
106
|
+
if (!n) throw Error(`No element found with type "${t}"`);
|
|
107
|
+
return n;
|
|
108
|
+
}
|
|
109
|
+
function i(e, t) {
|
|
110
|
+
return e.findAllByType(t);
|
|
111
|
+
}
|
|
112
|
+
function a(e, t) {
|
|
113
|
+
let n = e.findByText(t);
|
|
114
|
+
if (!n) throw Error(`No element found with text "${t}"`);
|
|
115
|
+
return n;
|
|
116
|
+
}
|
|
117
|
+
function o(e, t) {
|
|
118
|
+
return e.findByType(t);
|
|
119
|
+
}
|
|
120
|
+
function s(e, t) {
|
|
121
|
+
return e.findByText(t);
|
|
122
|
+
}
|
|
123
|
+
function c(e, t, n) {
|
|
124
|
+
function r(e) {
|
|
125
|
+
if (e.props[t] === n) return e;
|
|
126
|
+
for (let t of e.children) {
|
|
127
|
+
let e = r(t);
|
|
128
|
+
if (e) return e;
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
let i = r(e);
|
|
133
|
+
if (!i) throw Error(`No element found with ${t}="${n}"`);
|
|
134
|
+
return i;
|
|
135
|
+
}
|
|
136
|
+
//#endregion
|
|
137
|
+
//#region src/render.ts
|
|
138
|
+
function l(e, l) {
|
|
139
|
+
let u = new t("root");
|
|
140
|
+
return n.render(e, u, l?.appContext ?? void 0), {
|
|
141
|
+
container: u,
|
|
142
|
+
unmount: () => {
|
|
143
|
+
n.render(null, u);
|
|
144
|
+
},
|
|
145
|
+
getByType: (e) => r(u, e),
|
|
146
|
+
getAllByType: (e) => i(u, e),
|
|
147
|
+
getByText: (e) => a(u, e),
|
|
148
|
+
queryByType: (e) => o(u, e),
|
|
149
|
+
queryByText: (e) => s(u, e),
|
|
150
|
+
getByProp: (e, t) => c(u, e, t),
|
|
151
|
+
debug: () => u.toDebugString()
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
//#endregion
|
|
155
|
+
//#region src/fire-event.ts
|
|
156
|
+
function u(e, t, n = 1) {
|
|
157
|
+
return {
|
|
158
|
+
identifier: n,
|
|
159
|
+
x: e,
|
|
160
|
+
y: t,
|
|
161
|
+
pageX: e,
|
|
162
|
+
pageY: t,
|
|
163
|
+
clientX: e,
|
|
164
|
+
clientY: t
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function d(e, t, n) {
|
|
168
|
+
let r = e._handlers.get(t);
|
|
169
|
+
r && r(n);
|
|
170
|
+
}
|
|
171
|
+
function f(e) {
|
|
172
|
+
return {
|
|
173
|
+
type: "touch",
|
|
174
|
+
timestamp: Date.now(),
|
|
175
|
+
touches: e?.touches ?? [],
|
|
176
|
+
changedTouches: e?.changedTouches ?? e?.touches ?? [],
|
|
177
|
+
target: {
|
|
178
|
+
id: "",
|
|
179
|
+
dataset: {},
|
|
180
|
+
uid: 0
|
|
181
|
+
},
|
|
182
|
+
currentTarget: {
|
|
183
|
+
id: "",
|
|
184
|
+
dataset: {},
|
|
185
|
+
uid: 0
|
|
186
|
+
},
|
|
187
|
+
detail: {}
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
var p = {
|
|
191
|
+
tap(e, t) {
|
|
192
|
+
let n = t?.x ?? 0, r = t?.y ?? 0, i = {
|
|
193
|
+
type: "tap",
|
|
194
|
+
timestamp: Date.now(),
|
|
195
|
+
target: {
|
|
196
|
+
id: "",
|
|
197
|
+
dataset: {},
|
|
198
|
+
uid: 0
|
|
199
|
+
},
|
|
200
|
+
currentTarget: {
|
|
201
|
+
id: "",
|
|
202
|
+
dataset: {},
|
|
203
|
+
uid: 0
|
|
204
|
+
},
|
|
205
|
+
detail: {
|
|
206
|
+
x: n,
|
|
207
|
+
y: r
|
|
208
|
+
},
|
|
209
|
+
touches: [u(n, r)],
|
|
210
|
+
changedTouches: [u(n, r)]
|
|
211
|
+
};
|
|
212
|
+
d(e, "bindtap", i), d(e, "onTap", i);
|
|
213
|
+
},
|
|
214
|
+
touchStart(e, t) {
|
|
215
|
+
d(e, "bindtouchstart", f(t));
|
|
216
|
+
},
|
|
217
|
+
touchMove(e, t) {
|
|
218
|
+
d(e, "bindtouchmove", f(t));
|
|
219
|
+
},
|
|
220
|
+
touchEnd(e, t) {
|
|
221
|
+
d(e, "bindtouchend", f(t));
|
|
222
|
+
},
|
|
223
|
+
touchCancel(e, t) {
|
|
224
|
+
d(e, "bindtouchcancel", f(t));
|
|
225
|
+
},
|
|
226
|
+
scroll(e, t) {
|
|
227
|
+
let n = {
|
|
228
|
+
type: "scroll",
|
|
229
|
+
timestamp: Date.now(),
|
|
230
|
+
target: {
|
|
231
|
+
id: "",
|
|
232
|
+
dataset: {},
|
|
233
|
+
uid: 0
|
|
234
|
+
},
|
|
235
|
+
currentTarget: {
|
|
236
|
+
id: "",
|
|
237
|
+
dataset: {},
|
|
238
|
+
uid: 0
|
|
239
|
+
},
|
|
240
|
+
detail: {
|
|
241
|
+
scrollTop: 0,
|
|
242
|
+
scrollLeft: 0,
|
|
243
|
+
scrollHeight: 0,
|
|
244
|
+
scrollWidth: 0,
|
|
245
|
+
deltaX: 0,
|
|
246
|
+
deltaY: 0,
|
|
247
|
+
...t?.detail
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
d(e, "bindscroll", n), d(e, "onScroll", n);
|
|
251
|
+
},
|
|
252
|
+
input(e, t) {
|
|
253
|
+
let n = {
|
|
254
|
+
type: "input",
|
|
255
|
+
timestamp: Date.now(),
|
|
256
|
+
target: {
|
|
257
|
+
id: "",
|
|
258
|
+
dataset: {},
|
|
259
|
+
uid: 0
|
|
260
|
+
},
|
|
261
|
+
currentTarget: {
|
|
262
|
+
id: "",
|
|
263
|
+
dataset: {},
|
|
264
|
+
uid: 0
|
|
265
|
+
},
|
|
266
|
+
detail: {
|
|
267
|
+
value: "",
|
|
268
|
+
...t?.detail
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
d(e, "bindinput", n), d(e, "onInput", n);
|
|
272
|
+
},
|
|
273
|
+
longPress(e) {
|
|
274
|
+
let t = {
|
|
275
|
+
type: "longpress",
|
|
276
|
+
timestamp: Date.now(),
|
|
277
|
+
target: {
|
|
278
|
+
id: "",
|
|
279
|
+
dataset: {},
|
|
280
|
+
uid: 0
|
|
281
|
+
},
|
|
282
|
+
currentTarget: {
|
|
283
|
+
id: "",
|
|
284
|
+
dataset: {},
|
|
285
|
+
uid: 0
|
|
286
|
+
},
|
|
287
|
+
detail: {}
|
|
288
|
+
};
|
|
289
|
+
d(e, "bindlongpress", t), d(e, "onLongpress", t);
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
//#endregion
|
|
293
|
+
//#region src/flush.ts
|
|
294
|
+
function m() {
|
|
295
|
+
return new Promise((e) => {
|
|
296
|
+
Promise.resolve().then(() => new Promise((e) => setTimeout(e, 0))).then(e);
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
async function h(e) {
|
|
300
|
+
await e(), await m();
|
|
301
|
+
}
|
|
302
|
+
//#endregion
|
|
303
|
+
export { t as TestNode, h as act, p as fireEvent, i as getAllByType, c as getByProp, a as getByText, r as getByType, s as queryByText, o as queryByType, l as render, u as touch, m as waitForUpdate };
|
|
304
|
+
|
|
305
|
+
//# sourceMappingURL=index.js.map
|