@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 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"}
@@ -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"}
@@ -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