rnxsim 0.1.417 → 0.1.419

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.
Files changed (66) hide show
  1. package/cli/cloud-client.ts +104 -32
  2. package/cli/cloud-dispatch.ts +38 -4
  3. package/cli/commands/inspect/actions.ts +313 -385
  4. package/cli/commands/inspect/core.ts +27 -12
  5. package/cli/commands/inspect/resolve-target.ts +2 -3
  6. package/cli/commands/inspect/settle.ts +10 -0
  7. package/cli/commands/inspect/wait-ready.ts +2 -0
  8. package/cli/commands/inspect.ts +31 -48
  9. package/cli/commands/platform.ts +41 -10
  10. package/cli/outbound-endpoints.ts +1 -1
  11. package/cli/self-invocation.ts +2 -2
  12. package/cli/shell-init.ts +1 -1
  13. package/dist-lib/agent-daemon-client.cjs +1 -1
  14. package/dist-lib/agent-events.cjs +1 -1
  15. package/dist-lib/agent-identity.cjs +1 -1
  16. package/dist-lib/agent-sessions.cjs +1 -1
  17. package/dist-lib/attached-projects.cjs +1 -1
  18. package/dist-lib/auth/shared-session.cjs +1 -1
  19. package/dist-lib/backend-origin.cjs +1 -1
  20. package/dist-lib/beta.cjs +1 -1
  21. package/dist-lib/beta.mjs +1 -1
  22. package/dist-lib/bridge-constants.cjs +1 -1
  23. package/dist-lib/bridge-contract-input.cjs +1 -1
  24. package/dist-lib/bridge-contract-input.mjs +1 -1
  25. package/dist-lib/bridge-contract.cjs +7 -1
  26. package/dist-lib/bridge-contract.mjs +5 -1
  27. package/dist-lib/capture-contract.cjs +1 -1
  28. package/dist-lib/capture-contract.mjs +1 -1
  29. package/dist-lib/cli-constants.cjs +1 -1
  30. package/dist-lib/cloud-contract.cjs +1 -1
  31. package/dist-lib/cloud-contract.mjs +1 -1
  32. package/dist-lib/config.cjs +1 -1
  33. package/dist-lib/detox/index.cjs +1 -1
  34. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  35. package/dist-lib/home-paths.cjs +1 -1
  36. package/dist-lib/host/bridge-host.cjs +1 -1
  37. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  38. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  39. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  40. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  41. package/dist-lib/host/websocket-proxy.cjs +1 -1
  42. package/dist-lib/index.cjs +631 -749
  43. package/dist-lib/jump-to-source-babel.cjs +1 -1
  44. package/dist-lib/menu.cjs +1 -1
  45. package/dist-lib/menu.mjs +1 -1
  46. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  47. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  48. package/dist-lib/metro-production-bundle.cjs +1 -1
  49. package/dist-lib/metro-production-bundle.mjs +1 -1
  50. package/dist-lib/metro.cjs +1 -1
  51. package/dist-lib/profiles.cjs +1 -1
  52. package/dist-lib/public-brand.cjs +1 -1
  53. package/dist-lib/react-native-host-modules.cjs +1 -1
  54. package/dist-lib/react-native-host-modules.mjs +1 -1
  55. package/dist-lib/render-mode.cjs +1 -1
  56. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  57. package/dist-lib/sdk.cjs +1209 -1342
  58. package/dist-lib/sdk.mjs +1209 -1342
  59. package/dist-lib/skills.cjs +63 -2
  60. package/dist-lib/vite.cjs +1 -1
  61. package/package.json +1 -1
  62. package/src/bridge-contract.ts +3 -0
  63. package/src/host/bridge-host.ts +1 -1
  64. package/src/host/plane-host.ts +1 -1
  65. package/src/native-dev-bundle-url.ts +1 -1
  66. package/src/vite-plugin.ts +1 -1
package/dist-lib/sdk.mjs CHANGED
@@ -1,660 +1,12 @@
1
- /*! rnx v0.1.417 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.419 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  var __defProp = Object.defineProperty;
3
3
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
4
4
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
5
5
 
6
- // cli/commands/inspect/settling.ts
7
- async function waitForSootsimIdle({
8
- bridge,
9
- simId,
10
- maxMs,
11
- pollMs = 50,
12
- stablePolls = 3,
13
- strict = false
14
- }) {
15
- const result = await bridge.send(
16
- {
17
- type: "evaluate",
18
- simId,
19
- code: `(async () => {
20
- const start = Date.now()
21
- const deadline = start + ${Math.max(0, Math.round(maxMs))}
22
- const pollMs = ${Math.max(1, Math.round(pollMs))}
23
- const requiredStablePolls = ${Math.max(1, Math.round(stablePolls))}
24
- const strict = ${strict ? "true" : "false"}
25
- const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
26
-
27
- // route-aware settle: a tap that pushes/pops a screen is async \u2014 the
28
- // old screen still renders for a moment, then the new one mounts and
29
- // its content loads. a pure layout-hash check reports "idle" on the
30
- // outgoing screen or on the incoming skeleton, so a driver re-taps
31
- // and stacks duplicate navigations. drain any in-flight screen
32
- // transition FIRST, bounded by the overall budget. when no
33
- // transition is happening this returns in ~80ms (startWindowMs), so
34
- // a plain button tap barely pays for it.
35
- try {
36
- const wfst = window.__sootsimTest?.waitForScreenTransitions
37
- if (typeof wfst === 'function') {
38
- const remaining = deadline - Date.now()
39
- if (remaining > 120) {
40
- await wfst({
41
- timeoutMs: Math.min(remaining - 80, 4000),
42
- settleMs: 64,
43
- startWindowMs: 80,
44
- })
45
- }
46
- }
47
- } catch {}
48
-
49
- const readSnapshot = async () => {
50
- let animating = false
51
- let layoutDirty = false
52
- let pendingFetches = 0
53
- let requestTotal = 0
54
- let requestInFlight = 0
55
- let renderStatsAvailable = false
56
- try {
57
- const stats = await window.__sootsimRenderHost?.queryStats?.()
58
- if (stats) {
59
- renderStatsAvailable = true
60
- animating =
61
- stats.hasActiveAnims === true ||
62
- stats.hasActiveNativeAnimations === true ||
63
- stats.hasPendingAnimationFrames === true
64
- layoutDirty =
65
- stats.layoutDirty === true && stats.renderRequested === true
66
- // a freshly-pushed screen showing a skeleton is layout-stable
67
- // but not actually settled \u2014 its data/images are still in
68
- // flight. treat bounded image-loader fetches as not-idle so
69
- // settle waits for real content, capped by the budget.
70
- const pf = stats.memory && stats.memory.imageLoader
71
- ? stats.memory.imageLoader.pendingFetches
72
- : 0
73
- pendingFetches = typeof pf === 'number' ? pf : 0
74
- }
75
- } catch {}
76
- try {
77
- const counts = await window.__sootsimTest?.getRequestCounts?.()
78
- requestTotal = typeof counts?.total === 'number' ? counts.total : 0
79
- requestInFlight = typeof counts?.inFlight === 'number' ? counts.inFlight : 0
80
- } catch {}
81
- const root = window.__sootsimRoot
82
- const nodes = []
83
- if (root) {
84
- const walk = (n) => {
85
- if (n.layout && n.layout.width > 0) {
86
- nodes.push(Math.round(n.layout.x) + ',' + Math.round(n.layout.y) + ',' + Math.round(n.layout.width))
87
- }
88
- for (const c of n.children || []) walk(c)
89
- }
90
- walk(root)
91
- }
92
- return { layout: nodes.join(';'), animating, layoutDirty, pendingFetches, renderStatsAvailable, requestTotal, requestInFlight }
93
- }
94
-
95
- // how long generic background network is allowed to keep us waiting AFTER
96
- // the screen is otherwise visually settled. apps with continuous traffic
97
- // (polling, realtime sockets, a tap that fires 20-50 fetches) never reach
98
- // requestInFlight===0 / a stable requestTotal, so requiring full network
99
- // quiet burns the ENTIRE budget on every command \u2014 the leading idle that
100
- // failed PR-preview recordings for network-heavy apps (3pc fight-pulse).
101
- // layout-stability + the image-loader already prove visible content
102
- // loaded (a data load changes layout; an image load shows in the loader),
103
- // so the generic request counters are a courtesy, not a gate: honor them
104
- // only within this grace once the screen is visually still.
105
- const networkQuietGraceMs = 1200
106
- let lastLayout = ''
107
- let lastRequestTotal = -1
108
- let stable = 0
109
- let visuallyStillSince = 0
110
- while (Date.now() < deadline) {
111
- const snapshot = await readSnapshot()
112
- const strictOk =
113
- !strict ||
114
- (snapshot.renderStatsAvailable && !snapshot.animating && !snapshot.layoutDirty)
115
- // visually settled: no animations, no image content loading, layout
116
- // unchanged since the last poll.
117
- const visuallyStill =
118
- strictOk && snapshot.pendingFetches === 0 && snapshot.layout === lastLayout
119
- // generic network quiet: nothing in flight and no new requests issued.
120
- const networkQuiet =
121
- snapshot.requestInFlight === 0 && snapshot.requestTotal === lastRequestTotal
122
- if (visuallyStill) {
123
- stable++
124
- if (visuallyStillSince === 0) visuallyStillSince = Date.now()
125
- // settle once the screen has held still long enough AND either the
126
- // network is genuinely quiet OR it has refused to quiet within the
127
- // grace (continuous background traffic must not block forever).
128
- if (
129
- stable >= requiredStablePolls &&
130
- (networkQuiet || Date.now() - visuallyStillSince >= networkQuietGraceMs)
131
- ) {
132
- return { settled: true, elapsed: Date.now() - start }
133
- }
134
- } else {
135
- stable = 0
136
- visuallyStillSince = 0
137
- }
138
- lastLayout = snapshot.layout
139
- lastRequestTotal = snapshot.requestTotal
140
- await sleep(pollMs)
141
- }
142
- return { settled: false, elapsed: Date.now() - start }
143
- })()`
144
- },
145
- {
146
- timeoutMs: maxMs + 1e3
147
- }
148
- );
149
- const { elapsed, settled } = result ?? {};
150
- return {
151
- elapsed: typeof elapsed === "number" ? elapsed : maxMs,
152
- settled: settled === true
153
- };
154
- }
155
-
156
- // cli/commands/inspect/actions.ts
157
- var SCREEN_EVAL = `(() => {
158
- const spec = window.SootSim?.state?.engineSnapshot?.windowState?.deviceSpec
159
- return spec && spec.width > 0 && spec.height > 0
160
- ? { width: spec.width, height: spec.height }
161
- : null
162
- })()`;
163
- var DEFAULT_AGENT_TIMING = {
164
- initialWaitMs: 3e3,
165
- deadlineMs: 5e3,
166
- retryWaitMs: 700
167
- };
168
- var DEFAULT_INTERACTIVE_TIMING = {
169
- initialWaitMs: 1200,
170
- deadlineMs: 2500,
171
- retryWaitMs: 700
172
- };
173
- function tapTiming(agent, opts = {}) {
174
- return {
175
- ...agent ? DEFAULT_AGENT_TIMING : DEFAULT_INTERACTIVE_TIMING,
176
- ...opts
177
- };
178
- }
179
- function isTapSuccess(result) {
180
- if (!result || result.hit === false || result.ok === false) return false;
181
- if (result.requestedTargetMatched === false) return false;
182
- if (result.pointerTapHandled === false && !result.keyboardOpened && !result.isTextInput) {
183
- return false;
184
- }
185
- return true;
186
- }
187
- function tapTargetFromPayload(payload, textFallback) {
188
- return {
189
- nodeId: payload.target?.nodeId ?? payload.match?.nodeId ?? payload.node?.nodeId ?? null,
190
- id: payload.target?.id ?? payload.match?.id ?? payload.node?.id ?? null,
191
- testID: payload.target?.testID ?? payload.match?.testID ?? payload.node?.testID ?? null,
192
- text: payload.target?.text ?? payload.target?.accessibilityLabel ?? payload.match?.text ?? payload.match?.accessibilityLabel ?? payload.node?.text ?? textFallback ?? null,
193
- type: payload.target?.type ?? payload.match?.type ?? payload.node?.type ?? null
194
- };
195
- }
196
- async function tapResolvedTarget(bridge, args) {
197
- const timing = tapTiming(!!args.agent, args.timing);
198
- let attempts = 0;
199
- let lastPayload = null;
200
- let lastResult = null;
201
- try {
202
- await waitForSootsimIdle({
203
- bridge,
204
- maxMs: timing.initialWaitMs,
205
- pollMs: 32,
206
- stablePolls: 2
207
- });
208
- } catch {
209
- }
210
- const deadline = Date.now() + timing.deadlineMs;
211
- let lastOffscreenAt = null;
212
- while (Date.now() <= deadline || attempts === 0) {
213
- attempts++;
214
- const payload = await args.resolve();
215
- lastPayload = payload;
216
- if (payload?.error === "bridge-not-ready" || payload?.ambiguous || payload?.nthOutOfRange) {
217
- return { payload, result: null, attempts, failure: "special" };
218
- }
219
- if (payload && typeof payload.cx === "number" && typeof payload.cy === "number") {
220
- if (payload.offscreen) {
221
- lastResult = {
222
- hit: false,
223
- reason: "offscreen",
224
- x: payload.cx,
225
- y: payload.cy,
226
- screen: payload.screen
227
- };
228
- const at = `${Math.round(payload.cx)},${Math.round(payload.cy)}`;
229
- if (lastOffscreenAt === at) {
230
- return { payload, result: lastResult, attempts, failure: "missed" };
231
- }
232
- lastOffscreenAt = at;
233
- } else {
234
- lastOffscreenAt = null;
235
- const requestedTarget = tapTargetFromPayload(payload, args.textFallback);
236
- const verification = await bridge.send({
237
- type: "evaluate",
238
- code: `(async () => {
239
- const t = window.__sootsimTest
240
- if (
241
- !t ||
242
- typeof t.inspectAt !== 'function' ||
243
- typeof t.resolveTapTarget !== 'function'
244
- ) {
245
- return { error: 'tap-target-inspection-unavailable' }
246
- }
247
- const requestedTarget = ${JSON.stringify(requestedTarget)}
248
- const inspected = await t.inspectAt(${payload.cx}, ${payload.cy})
249
- const resolved =
250
- inspected && typeof inspected.nodeId === 'number'
251
- ? await t.resolveTapTarget(inspected.nodeId)
252
- : null
253
- const node = (resolved && resolved.target) || inspected
254
- const coordinateTarget = node
255
- ? {
256
- nodeId: node.nodeId ?? null,
257
- id: node.id ?? null,
258
- testID: node.testID ?? null,
259
- text: node.text ?? node.accessibilityLabel ?? null,
260
- type: node.type ?? null,
261
- }
262
- : null
263
- const coordinateAncestors = Array.isArray(inspected?.ancestors)
264
- ? inspected.ancestors
265
- : []
266
- const ancestorMatchesRequested = coordinateAncestors.some(
267
- (ancestor) =>
268
- (typeof requestedTarget.nodeId === 'number' &&
269
- ancestor?.nodeId === requestedTarget.nodeId) ||
270
- (requestedTarget.testID &&
271
- ancestor?.testID === requestedTarget.testID) ||
272
- (requestedTarget.id && ancestor?.id === requestedTarget.id),
273
- )
274
- const requestedTargetMatched =
275
- coordinateTarget === null
276
- ? false
277
- : typeof requestedTarget.nodeId === 'number' &&
278
- typeof coordinateTarget.nodeId === 'number'
279
- ? requestedTarget.nodeId === coordinateTarget.nodeId ||
280
- ancestorMatchesRequested
281
- : requestedTarget.testID && coordinateTarget.testID
282
- ? requestedTarget.testID === coordinateTarget.testID
283
- || ancestorMatchesRequested
284
- : requestedTarget.id && coordinateTarget.id
285
- ? requestedTarget.id === coordinateTarget.id
286
- || ancestorMatchesRequested
287
- : ancestorMatchesRequested
288
- if (!requestedTargetMatched) {
289
- return {
290
- requestedTargetMatched,
291
- requestedTarget,
292
- coordinateTarget,
293
- }
294
- }
295
- if (typeof t.activatePressAt !== 'function') {
296
- return { error: 'tap-target-activation-unavailable' }
297
- }
298
- const activation = await t.activatePressAt(
299
- ${payload.cx},
300
- ${payload.cy},
301
- requestedTarget.nodeId,
302
- )
303
- if (activation?.ok) {
304
- window.dispatchEvent(
305
- new CustomEvent('sootsim:agentAction', {
306
- detail: {
307
- type: 'tap',
308
- x: ${payload.cx},
309
- y: ${payload.cy},
310
- target: requestedTarget,
311
- },
312
- }),
313
- )
314
- }
315
- return {
316
- activation,
317
- requestedTargetMatched,
318
- requestedTarget,
319
- coordinateTarget,
320
- }
321
- })()`
322
- });
323
- if (verification?.error) {
324
- return {
325
- payload,
326
- result: {
327
- hit: false,
328
- reason: verification.error,
329
- requestedTarget
330
- },
331
- attempts,
332
- failure: "special"
333
- };
334
- }
335
- if (verification?.requestedTargetMatched === false) {
336
- return {
337
- payload,
338
- result: {
339
- hit: false,
340
- reason: "target-covered",
341
- ...verification
342
- },
343
- attempts,
344
- failure: "missed"
345
- };
346
- }
347
- const dispatched = verification?.activation?.tap ?? verification?.activation;
348
- const result = dispatched && typeof dispatched === "object" ? { ...dispatched, ...verification?.activation, ...verification } : { hit: !!dispatched, ...verification };
349
- lastResult = result;
350
- if (isTapSuccess(result)) return { payload, result, attempts };
351
- }
352
- }
353
- const remaining = deadline - Date.now();
354
- if (remaining <= 0) break;
355
- try {
356
- await waitForSootsimIdle({
357
- bridge,
358
- maxMs: Math.min(remaining, timing.retryWaitMs),
359
- pollMs: 32,
360
- stablePolls: 2
361
- });
362
- } catch {
363
- await new Promise((resolve) => setTimeout(resolve, Math.min(120, remaining)));
364
- }
365
- }
366
- return {
367
- payload: lastPayload,
368
- result: lastResult,
369
- attempts,
370
- failure: lastPayload && typeof lastPayload.cx === "number" ? "missed" : "not-found"
371
- };
372
- }
373
- async function tapCoordinates(bridge, x, y, target) {
374
- return bridge.send({
375
- type: "tap",
376
- x,
377
- y,
378
- ...target ? { target } : {}
379
- });
380
- }
381
- async function tapById(bridge, query, opts = {}) {
382
- const arg = JSON.stringify(query);
383
- return tapResolvedTarget(bridge, {
384
- agent: opts.agent,
385
- timing: opts.timing,
386
- resolve: () => bridge.send({
387
- type: "evaluate",
388
- code: `(async () => {
389
- const t = window.__sootsimTest
390
- if (!t) return null
391
- const n = (await t.findByTestId(${arg})) || (await t.findById(${arg}))
392
- if (!n || !n.absolutePosition || !n.layout) return { cx: null }
393
- const resolved =
394
- typeof n.nodeId === 'number' && typeof t.resolveTapTarget === 'function'
395
- ? await t.resolveTapTarget(n.nodeId)
396
- : null
397
- const target = (resolved && resolved.target) || n
398
- const cx =
399
- resolved && typeof resolved.cx === 'number'
400
- ? resolved.cx
401
- : n.absolutePosition.x + (n.layout.width || 0) / 2
402
- const cy =
403
- resolved && typeof resolved.cy === 'number'
404
- ? resolved.cy
405
- : n.absolutePosition.y + (n.layout.height || 0) / 2
406
- const scr = ${SCREEN_EVAL}
407
- const offscreen =
408
- !!scr && (cx < 0 || cy < 0 || cx > scr.width || cy > scr.height)
409
- return {
410
- cx,
411
- cy,
412
- ...(offscreen ? { offscreen: true, screen: scr } : {}),
413
- match: {
414
- nodeId: n.nodeId ?? null,
415
- id: n.id,
416
- testID: n.testID,
417
- text: n.text ?? n.accessibilityLabel ?? null,
418
- type: n.type,
419
- },
420
- target: {
421
- nodeId: target.nodeId ?? null,
422
- id: target.id,
423
- testID: target.testID,
424
- text:
425
- target.text ??
426
- target.accessibilityLabel ??
427
- n.text ??
428
- n.accessibilityLabel ??
429
- null,
430
- type: target.type,
431
- },
432
- strategy: (resolved && resolved.strategy) || 'matched-node',
433
- }
434
- })()`
435
- })
436
- });
437
- }
438
- async function tapByText(bridge, query, options = {}, opts = {}) {
439
- const filterArg = JSON.stringify({
440
- query,
441
- exact: !!options.exact,
442
- role: options.role ?? null,
443
- within: options.within ?? null,
444
- minX: options.minX ?? null,
445
- maxX: options.maxX ?? null,
446
- minY: options.minY ?? null,
447
- maxY: options.maxY ?? null,
448
- near: options.near ?? null,
449
- nth: options.nth ?? null,
450
- first: !!options.first
451
- });
452
- return tapResolvedTarget(bridge, {
453
- agent: opts.agent,
454
- timing: opts.timing,
455
- textFallback: query,
456
- resolve: () => bridge.send({
457
- type: "evaluate",
458
- code: `(async () => {
459
- const t = window.__sootsimTest
460
- if (!t) return { error: 'bridge-not-ready' }
461
- const F = ${filterArg}
462
-
463
- const res = await t.queryTextCandidates({
464
- query: F.query,
465
- exact: !!F.exact,
466
- ...(F.role ? { role: F.role } : {}),
467
- })
468
-
469
- let candidates = res.candidates || []
470
-
471
- candidates = candidates.filter((c) => {
472
- const ax = c.info.absolutePosition && c.info.absolutePosition.x
473
- const ay = c.info.absolutePosition && c.info.absolutePosition.y
474
- if (F.minX !== null && !(ax >= F.minX)) return false
475
- if (F.maxX !== null && !(ax <= F.maxX)) return false
476
- if (F.minY !== null && !(ay >= F.minY)) return false
477
- if (F.maxY !== null && !(ay <= F.maxY)) return false
478
- if (F.within && !c.ancestorTestIDs.includes(F.within)) return false
479
- return true
480
- })
481
-
482
- if (F.near) {
483
- candidates.sort((a, b) => {
484
- const ax = (a.info.absolutePosition && a.info.absolutePosition.x) || 0
485
- const ay = (a.info.absolutePosition && a.info.absolutePosition.y) || 0
486
- const bx = (b.info.absolutePosition && b.info.absolutePosition.x) || 0
487
- const by = (b.info.absolutePosition && b.info.absolutePosition.y) || 0
488
- return (
489
- Math.hypot(ax - F.near.x, ay - F.near.y) -
490
- Math.hypot(bx - F.near.x, by - F.near.y)
491
- )
492
- })
493
- } else {
494
- candidates.sort((a, b) => {
495
- const ay = (a.info.absolutePosition && a.info.absolutePosition.y) || 0
496
- const by = (b.info.absolutePosition && b.info.absolutePosition.y) || 0
497
- if (Math.abs(ay - by) > 2) return ay - by
498
- const ax = (a.info.absolutePosition && a.info.absolutePosition.x) || 0
499
- const bx = (b.info.absolutePosition && b.info.absolutePosition.x) || 0
500
- return ax - bx
501
- })
502
- }
503
-
504
- const total = candidates.length
505
- if (total === 0) return { matched: 0, total: 0 }
506
-
507
- let idx = 0
508
- if (F.nth !== null) {
509
- idx = F.nth < 0 ? total + F.nth : F.nth
510
- if (idx < 0 || idx >= total) {
511
- return { matched: 0, total, nthOutOfRange: true, nth: F.nth }
512
- }
513
- } else if (total > 1 && !F.first && !F.near) {
514
- return {
515
- ambiguous: true,
516
- total,
517
- candidates: candidates.slice(0, 10).map((c, i) => ({
518
- idx: i,
519
- nodeId: c.info.nodeId,
520
- type: c.info.type,
521
- testID: c.info.testID,
522
- text: (c.info.text || '').slice(0, 60),
523
- abs: c.info.absolutePosition,
524
- layout: c.info.layout
525
- ? {
526
- width: Math.round(c.info.layout.width || 0),
527
- height: Math.round(c.info.layout.height || 0),
528
- }
529
- : null,
530
- ancestorTestIDs: (c.ancestorTestIDs || []).slice(0, 5),
531
- })),
532
- }
533
- }
534
-
535
- const picked = candidates[idx]
536
- const n = picked.info
537
- if (!n.absolutePosition || !n.layout) return { matched: 0, total }
538
-
539
- const resolved =
540
- typeof n.nodeId === 'number' &&
541
- typeof t.resolveTapTarget === 'function'
542
- ? await t.resolveTapTarget(n.nodeId)
543
- : null
544
- const target = (resolved && resolved.target) || n
545
- const cx =
546
- resolved && typeof resolved.cx === 'number'
547
- ? resolved.cx
548
- : n.absolutePosition.x + (n.layout.width || 0) / 2
549
- const cy =
550
- resolved && typeof resolved.cy === 'number'
551
- ? resolved.cy
552
- : n.absolutePosition.y + (n.layout.height || 0) / 2
553
- const scr = ${SCREEN_EVAL}
554
- const offscreen =
555
- !!scr && (cx < 0 || cy < 0 || cx > scr.width || cy > scr.height)
556
- return {
557
- cx,
558
- cy,
559
- ...(offscreen ? { offscreen: true, screen: scr } : {}),
560
- match: {
561
- nodeId: n.nodeId ?? null,
562
- id: n.id,
563
- testID: n.testID,
564
- type: n.type,
565
- },
566
- target: {
567
- nodeId: target.nodeId ?? null,
568
- id: target.id,
569
- testID: target.testID,
570
- text:
571
- target.text ??
572
- target.accessibilityLabel ??
573
- n.text ??
574
- n.accessibilityLabel ??
575
- null,
576
- type: target.type,
577
- },
578
- strategy: (resolved && resolved.strategy) || 'matched-node',
579
- total,
580
- idx,
581
- }
582
- })()`
583
- })
584
- });
585
- }
586
- async function tapBest(bridge, query, opts = {}) {
587
- const arg = JSON.stringify(query);
588
- return tapResolvedTarget(bridge, {
589
- agent: opts.agent,
590
- timing: opts.timing,
591
- textFallback: query,
592
- resolve: async () => {
593
- const payload = await bridge.send({
594
- type: "evaluate",
595
- code: `(async () => {
596
- const t = window.__sootsimTest
597
- if (!t) return { error: 'bridge-not-ready' }
598
- const byTestId =
599
- (await t.findByTestId(${arg})) || (await t.findById(${arg}))
600
- if (byTestId && byTestId.absolutePosition && byTestId.layout) {
601
- return {
602
- strategy: 'testid',
603
- node: {
604
- nodeId: byTestId.nodeId ?? null,
605
- id: byTestId.id,
606
- testID: byTestId.testID,
607
- type: byTestId.type,
608
- text: byTestId.text,
609
- absolutePosition: byTestId.absolutePosition,
610
- layout: byTestId.layout,
611
- },
612
- screen: ${SCREEN_EVAL},
613
- }
614
- }
615
- const byText = await t.findByText(${arg})
616
- if (byText && byText.absolutePosition && byText.layout) {
617
- return {
618
- strategy: 'text',
619
- node: {
620
- nodeId: byText.nodeId ?? null,
621
- id: byText.id,
622
- testID: byText.testID,
623
- type: byText.type,
624
- text: byText.text,
625
- absolutePosition: byText.absolutePosition,
626
- layout: byText.layout,
627
- },
628
- screen: ${SCREEN_EVAL},
629
- }
630
- }
631
- return { strategy: 'none' }
632
- })()`
633
- });
634
- if (!payload || !("node" in payload)) return payload;
635
- const node = payload.node;
636
- const cx = node.absolutePosition.x + node.layout.width / 2;
637
- const cy = node.absolutePosition.y + node.layout.height / 2;
638
- const screen = payload.screen ?? null;
639
- const offscreen = !!screen && (cx < 0 || cy < 0 || cx > screen.width || cy > screen.height);
640
- return {
641
- ...payload,
642
- cx,
643
- cy,
644
- ...offscreen ? { offscreen: true, screen } : {},
645
- target: {
646
- id: node.id,
647
- testID: node.testID,
648
- text: payload.strategy === "text" ? query : node.text,
649
- type: node.type
650
- }
651
- };
652
- }
653
- });
654
- }
655
-
656
6
  // src/bridge-contract.ts
657
7
  var SEMANTIC_READY_CONTENT_NODE_FLOOR = 100;
8
+ var PUBLIC_NO_NETWORK_MARKER = "public RNX Cloud simulators have no network";
9
+ var HEADLESS_TENANT_FETCH_HUNG_MARKER = "headless tenant fetch hung";
658
10
  function readyProbeHasContent(probe) {
659
11
  return probe.liveFrameActive || probe.targets > 0 || probe.nodes >= SEMANTIC_READY_CONTENT_NODE_FLOOR;
660
12
  }
@@ -1119,778 +471,1293 @@ async function inspectGetLayout(bridge, opts = {}) {
1119
471
  w: Math.round(node.geometry.width),
1120
472
  h: Math.round(node.geometry.height)
1121
473
  }
1122
- }));
474
+ }));
475
+ }
476
+ const raw = await bridge.send({
477
+ type: "evaluate",
478
+ code: layoutGetNativeEval(opts)
479
+ });
480
+ return Array.isArray(raw) ? raw : [];
481
+ }
482
+ function formatLayoutElements(elements, opts) {
483
+ if (elements.length === 0) return " no visible elements found";
484
+ const lines = elements.map((el) => {
485
+ const ident = el.testID ? `#${el.testID}` : el.selector ? el.selector : el.role ? `[${el.role}]` : `<${el.type}>`;
486
+ const parts = [ident, `@(${el.box.x},${el.box.y})`, `${el.box.w}x${el.box.h}`];
487
+ if (el.borderRadius !== void 0 && el.borderRadius > 0) {
488
+ parts.push(`radius:${el.borderRadius}`);
489
+ }
490
+ const p = el.padding;
491
+ if (p && (p.t || p.r || p.b || p.l)) {
492
+ const uniform = p.t === p.r && p.r === p.b && p.b === p.l;
493
+ parts.push(uniform ? `pad:${p.t}` : `pad:${p.t},${p.r},${p.b},${p.l}`);
494
+ }
495
+ if (el.fontSize !== void 0 && el.fontSize > 0) {
496
+ parts.push(`font:${el.fontSize}`);
497
+ }
498
+ if (opts?.styling) {
499
+ if (el.fontWeight) parts.push(`weight:${el.fontWeight}`);
500
+ if (el.color) parts.push(`color:${el.color}`);
501
+ if (el.bg) parts.push(`bg:${el.bg}`);
502
+ if (el.contrast) {
503
+ const score = el.contrast.ratio == null ? `?/${el.contrast.required}` : `${el.contrast.ratio}/${el.contrast.required}${el.contrast.ratio < el.contrast.required ? " FAIL" : ""}`;
504
+ parts.push(`contrast:${score}`);
505
+ if (el.contrast.background) parts.push(`on:${el.contrast.background}`);
506
+ }
507
+ if (el.opacity != null) parts.push(`opacity:${el.opacity}`);
508
+ if (el.textDecoration) parts.push(`deco:${el.textDecoration}`);
509
+ if (el.overflow) parts.push(`overflow:${el.overflow}`);
510
+ if (el.borderWidth) {
511
+ parts.push(
512
+ el.borderColor ? `border:${el.borderWidth}/${el.borderColor}` : `border:${el.borderWidth}`
513
+ );
514
+ }
515
+ }
516
+ if (el.text) {
517
+ const text = el.text.length > 40 ? `${el.text.slice(0, 39)}\u2026` : el.text;
518
+ parts.push(`"${text}"`);
519
+ }
520
+ return ` ${parts.join(" ")}`;
521
+ });
522
+ return lines.join("\n");
523
+ }
524
+ async function inspectAccessibilityTree(bridge) {
525
+ const result = await bridge.send({
526
+ type: "query",
527
+ query: { kind: "find", selector: { visible: true } }
528
+ });
529
+ if (!isSemanticQueryResult(result)) return [];
530
+ return result.nodes.flatMap((node) => {
531
+ if (!node.role && !node.label && !node.text && !node.pressable) return [];
532
+ return [
533
+ {
534
+ role: node.role ?? (node.pressable ? "button" : node.type === "text" ? "statictext" : "none"),
535
+ label: node.label ?? node.text ?? null,
536
+ hint: null,
537
+ state: null,
538
+ testID: node.testID ?? null,
539
+ position: {
540
+ x: Math.round(node.geometry.x),
541
+ y: Math.round(node.geometry.y)
542
+ },
543
+ size: {
544
+ w: Math.round(node.geometry.width),
545
+ h: Math.round(node.geometry.height)
546
+ }
547
+ }
548
+ ];
549
+ });
550
+ }
551
+ function resolveFindMode(q) {
552
+ if (q.testId) {
553
+ return { mode: "testid", selector: { testID: q.testId } };
554
+ }
555
+ if (q.role) {
556
+ return { mode: "role", selector: { role: q.role } };
557
+ }
558
+ if (q.type) {
559
+ return { mode: "type", selector: { type: q.type } };
560
+ }
561
+ if (q.pressable) {
562
+ return { mode: "pressable", selector: { pressable: true } };
563
+ }
564
+ if (q.interactive) {
565
+ return { mode: "interactive-targets", selector: { pressable: true } };
566
+ }
567
+ if (q.visible) {
568
+ return { mode: "visible", selector: { visible: true } };
569
+ }
570
+ if (q.text) {
571
+ return { mode: "text", selector: { text: q.text } };
572
+ }
573
+ return null;
574
+ }
575
+ async function inspectFind(bridge, q) {
576
+ const resolved = resolveFindMode(q);
577
+ if (!resolved) return null;
578
+ const result = await bridge.send({
579
+ type: "query",
580
+ query: { kind: "find", selector: resolved.selector }
581
+ });
582
+ if (!isSemanticQueryResult(result)) return { mode: resolved.mode, result: [] };
583
+ const nodes = result.nodes.map(
584
+ (node) => ({
585
+ type: node.type,
586
+ nodeId: node.nodeId,
587
+ ...node.testID ? { testID: node.testID } : {},
588
+ ...node.text ? { text: node.text } : {},
589
+ ...node.pressable ? { pressable: true } : {},
590
+ ...node.role ? { accessibilityRole: node.role } : {},
591
+ ...node.label ? { accessibilityLabel: node.label } : {},
592
+ absolutePosition: { x: node.geometry.x, y: node.geometry.y },
593
+ layout: { width: node.geometry.width, height: node.geometry.height }
594
+ })
595
+ );
596
+ const resultValue = resolved.mode === "testid" || resolved.mode === "text" ? nodes[0] ?? null : nodes;
597
+ return { mode: resolved.mode, result: resultValue };
598
+ }
599
+ function rankInteractive(nodes) {
600
+ return [...nodes].sort((a, b) => scoreInteractive(b) - scoreInteractive(a));
601
+ }
602
+ function scoreInteractive(n) {
603
+ let score = 0;
604
+ if (n.testID) score += 100;
605
+ if (typeof n.text === "string" && n.text.trim().length > 0) score += 60;
606
+ if (typeof n.accessibilityLabel === "string" && n.accessibilityLabel.trim().length > 0) {
607
+ score += 30;
608
+ }
609
+ if (n.accessibilityRole) score += 15;
610
+ const w = n.layout?.width ?? 0;
611
+ const h = n.layout?.height ?? 0;
612
+ const area = w * h;
613
+ if (area >= 400 && area <= 6e4) score += 25;
614
+ else if (area > 6e4) score -= 20;
615
+ const y = n.absolutePosition?.y ?? 0;
616
+ if (y < 0) score -= 30;
617
+ return score;
618
+ }
619
+ function tapCommandForNode(n) {
620
+ if (n.testID) return `rnx do tap-id ${shellEscape(n.testID)}`;
621
+ const text = typeof n.text === "string" ? n.text.trim() : "";
622
+ if (text.length > 0 && text.length <= 80) return `rnx do tap-text ${shellEscape(text)}`;
623
+ const x = Math.round(((n.absolutePosition?.x ?? 0) + (n.layout?.width ?? 0) / 2) * 10) / 10;
624
+ const y = Math.round(((n.absolutePosition?.y ?? 0) + (n.layout?.height ?? 0) / 2) * 10) / 10;
625
+ return `rnx do tap ${x} ${y}`;
626
+ }
627
+ function shellEscape(value) {
628
+ if (/^[A-Za-z0-9_./@:-]+$/.test(value)) return value;
629
+ return `'${value.replace(/'/g, `'\\''`)}'`;
630
+ }
631
+ function waitReadyReason(status) {
632
+ if (status.bridgeError) {
633
+ if (status.bridgeError.includes(HEADLESS_TENANT_FETCH_HUNG_MARKER) || status.bridgeError.includes(PUBLIC_NO_NETWORK_MARKER)) {
634
+ return status.bridgeError;
635
+ }
636
+ return `sim unreachable: ${status.bridgeError} \u2014 the target tab is closed or no sim is connected (check \`rnx list\`, reopen with \`rnx open\`)`;
637
+ }
638
+ const waitingOnGuestFetch = status.externalReady === false && /https?:\/\//.test(status.externalStatus);
639
+ const base = status.externalError ? `guest app errored: ${status.externalError}` : waitingOnGuestFetch ? `waiting on a guest fetch: ${status.externalStatus}` : status.loadingText ? `still showing "${status.loadingText}"` : status.externalReady === false ? "guest app is still loading" : status.flag !== true && status.targets > 0 ? "native content is rendered but the ready signal has not settled" : status.flag !== true ? "guest app has not emitted sootsim:externalAppReady" : status.targets <= 0 ? "ready flag emitted but no visible app content is inspectable yet" : "node tree is still changing";
640
+ return !status.externalError && status.suppressedEntryError ? `${base} (suppressed entry error: ${status.suppressedEntryError})` : base;
641
+ }
642
+ async function inspectWaitReady(bridge, timeoutMs = 2e4, options = {}) {
643
+ const start = Date.now();
644
+ let result;
645
+ try {
646
+ result = await bridge.send(
647
+ {
648
+ type: "waitFor",
649
+ waitForOptions: { condition: { type: "ready" }, timeoutMs }
650
+ },
651
+ { timeoutMs: timeoutMs + 1e3 }
652
+ );
653
+ } catch (error) {
654
+ const bridgeError = error instanceof Error ? error.message : String(error);
655
+ if (bridgeError.startsWith("multiple sims are connected:") || bridgeError.startsWith("saved sim ")) {
656
+ throw error;
657
+ }
658
+ result = {
659
+ matched: false,
660
+ elapsedMs: Date.now() - start,
661
+ polls: 0,
662
+ error: bridgeError
663
+ };
664
+ }
665
+ const probe = result.probe;
666
+ const externalError = probe?.externalError ?? "";
667
+ const status = {
668
+ ready: result.matched && !externalError,
669
+ elapsedMs: result.elapsedMs,
670
+ nodes: probe?.nodes ?? 0,
671
+ targets: probe?.targets ?? 0,
672
+ liveFrameActive: probe?.liveFrameActive ?? false,
673
+ liveFrameChannels: probe?.liveFrameChannels ?? 0,
674
+ liveFramePublishes: probe?.liveFramePublishes ?? 0,
675
+ flag: probe?.flag,
676
+ loadingText: probe?.loadingText ?? "",
677
+ externalReady: probe?.externalReady ?? null,
678
+ externalStatus: probe?.externalStatus ?? "",
679
+ externalError,
680
+ suppressedEntryError: probe?.suppressedEntryError ?? "",
681
+ errors: probe?.errors ?? 0,
682
+ bridgeError: result.error ?? ""
683
+ };
684
+ if (!status.ready && options.onProgress) options.onProgress(status);
685
+ return status;
686
+ }
687
+ async function inspectWaitSelector(bridge, testId, timeoutMs = 5e3, opts = {}) {
688
+ const gone = opts.gone === true;
689
+ const result = await bridge.send(
690
+ {
691
+ type: "waitFor",
692
+ waitForOptions: {
693
+ condition: {
694
+ type: gone ? "absent" : "present",
695
+ selector: { testID: testId }
696
+ },
697
+ timeoutMs
698
+ }
699
+ },
700
+ { timeoutMs: timeoutMs + 1e3 }
701
+ );
702
+ if (result?.error) throw new Error(result.error);
703
+ if (result?.node && !isSimSemanticTree([result.node])) {
704
+ throw new Error("waitFor returned an invalid semantic node");
1123
705
  }
1124
- const raw = await bridge.send({
706
+ return {
707
+ found: result?.matched === true,
708
+ ...result?.node ? { node: result.node } : {},
709
+ elapsed: result?.elapsedMs ?? timeoutMs
710
+ };
711
+ }
712
+ function consoleEntriesEval(level, limit) {
713
+ const getter = level === "error" ? "getErrors" : "getWarnings";
714
+ return `(() => {
715
+ const out = []
716
+ const seen = new Set()
717
+ const norm = (s) => (typeof s === 'string' ? s : (() => { try { return JSON.stringify(s) } catch { return String(s) } })())
718
+ const key = (ts, args) => Math.round((ts || 0) / 250) + '|' + (Array.isArray(args) ? args.map(norm).join(' ') : norm(args))
719
+ const push = (e) => {
720
+ if (!e) return
721
+ const ts = typeof e.timestamp === 'number' ? e.timestamp : (typeof e.ts === 'number' ? e.ts : 0)
722
+ const k = key(ts, e.args)
723
+ if (seen.has(k)) return
724
+ seen.add(k)
725
+ out.push({ timestamp: ts, args: Array.isArray(e.args) ? e.args : [e.args], stack: e.stack || undefined, source: e.source || undefined })
726
+ }
727
+ // page-realm ws-bridge buffer first (richest stacks for page-origin errors)
728
+ try { for (const e of (window.__sootsimConsole?.${getter}(${limit}) || [])) push(e) } catch {}
729
+ // engine observability store \u2014 the always-on render-worker / forwarded path
730
+ try {
731
+ const obs = window.__sootsimObservability
732
+ const snap = obs && obs.logs && typeof obs.logs.getSnapshot === 'function' ? obs.logs.getSnapshot() : []
733
+ for (const e of snap) if (e && e.level === ${JSON.stringify(level)}) push(e)
734
+ } catch {}
735
+ return out.sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0)).slice(-${limit})
736
+ })()`;
737
+ }
738
+ async function inspectErrors(bridge, limit = 20) {
739
+ const result = await bridge.send({
1125
740
  type: "evaluate",
1126
- code: layoutGetNativeEval(opts)
741
+ code: consoleEntriesEval("error", limit)
1127
742
  });
1128
- return Array.isArray(raw) ? raw : [];
743
+ return Array.isArray(result) ? result : [];
1129
744
  }
1130
- function formatLayoutElements(elements, opts) {
1131
- if (elements.length === 0) return " no visible elements found";
1132
- const lines = elements.map((el) => {
1133
- const ident = el.testID ? `#${el.testID}` : el.selector ? el.selector : el.role ? `[${el.role}]` : `<${el.type}>`;
1134
- const parts = [ident, `@(${el.box.x},${el.box.y})`, `${el.box.w}x${el.box.h}`];
1135
- if (el.borderRadius !== void 0 && el.borderRadius > 0) {
1136
- parts.push(`radius:${el.borderRadius}`);
1137
- }
1138
- const p = el.padding;
1139
- if (p && (p.t || p.r || p.b || p.l)) {
1140
- const uniform = p.t === p.r && p.r === p.b && p.b === p.l;
1141
- parts.push(uniform ? `pad:${p.t}` : `pad:${p.t},${p.r},${p.b},${p.l}`);
1142
- }
1143
- if (el.fontSize !== void 0 && el.fontSize > 0) {
1144
- parts.push(`font:${el.fontSize}`);
1145
- }
1146
- if (opts?.styling) {
1147
- if (el.fontWeight) parts.push(`weight:${el.fontWeight}`);
1148
- if (el.color) parts.push(`color:${el.color}`);
1149
- if (el.bg) parts.push(`bg:${el.bg}`);
1150
- if (el.contrast) {
1151
- const score = el.contrast.ratio == null ? `?/${el.contrast.required}` : `${el.contrast.ratio}/${el.contrast.required}${el.contrast.ratio < el.contrast.required ? " FAIL" : ""}`;
1152
- parts.push(`contrast:${score}`);
1153
- if (el.contrast.background) parts.push(`on:${el.contrast.background}`);
1154
- }
1155
- if (el.opacity != null) parts.push(`opacity:${el.opacity}`);
1156
- if (el.textDecoration) parts.push(`deco:${el.textDecoration}`);
1157
- if (el.overflow) parts.push(`overflow:${el.overflow}`);
1158
- if (el.borderWidth) {
1159
- parts.push(
1160
- el.borderColor ? `border:${el.borderWidth}/${el.borderColor}` : `border:${el.borderWidth}`
1161
- );
1162
- }
1163
- }
1164
- if (el.text) {
1165
- const text = el.text.length > 40 ? `${el.text.slice(0, 39)}\u2026` : el.text;
1166
- parts.push(`"${text}"`);
745
+ async function inspectWarnings(bridge, limit = 20) {
746
+ const result = await bridge.send({
747
+ type: "evaluate",
748
+ code: consoleEntriesEval("warn", limit)
749
+ });
750
+ return Array.isArray(result) ? result : [];
751
+ }
752
+ var MERGED_CONSOLE_COUNT_EVAL = `(() => {
753
+ const norm = (s) => (typeof s === 'string' ? s : (() => { try { return JSON.stringify(s) } catch { return String(s) } })())
754
+ const key = (lvl, ts, args) => lvl + '|' + Math.round((ts || 0) / 250) + '|' + (Array.isArray(args) ? args.map(norm).join(' ') : norm(args))
755
+ const seen = new Set()
756
+ let errors = 0
757
+ let warnings = 0
758
+ const add = (lvl, ts, args) => {
759
+ if (lvl !== 'error' && lvl !== 'warn') return
760
+ const k = key(lvl, ts, args)
761
+ if (seen.has(k)) return
762
+ seen.add(k)
763
+ if (lvl === 'error') errors++
764
+ else warnings++
1167
765
  }
1168
- return ` ${parts.join(" ")}`;
766
+ try {
767
+ const c = window.__sootsimConsole
768
+ for (const e of (c?.getErrors?.(200) || [])) add('error', e?.timestamp ?? e?.ts, e?.args)
769
+ for (const e of (c?.getWarnings?.(200) || [])) add('warn', e?.timestamp ?? e?.ts, e?.args)
770
+ } catch {}
771
+ try {
772
+ const obs = window.__sootsimObservability
773
+ const snap = obs && obs.logs && typeof obs.logs.getSnapshot === 'function' ? obs.logs.getSnapshot() : []
774
+ for (const e of snap) if (e) add(e.level, e.ts, e.args)
775
+ } catch {}
776
+ return { errors, warnings, total: errors + warnings }
777
+ })()`;
778
+ async function clearConsole(bridge) {
779
+ await bridge.send({
780
+ type: "evaluate",
781
+ code: 'window.__sootsimConsole?.clear(); window.__sootsimObservability?.logs?.clear?.(); "cleared"'
1169
782
  });
1170
- return lines.join("\n");
1171
783
  }
1172
- async function inspectAccessibilityTree(bridge) {
784
+ async function inspectRequests(bridge, opts = {}) {
785
+ const limit = opts.limit ?? 20;
786
+ const method = opts.failed === false ? "getRequests" : "getFailedRequests";
1173
787
  const result = await bridge.send({
1174
- type: "query",
1175
- query: { kind: "find", selector: { visible: true } }
788
+ type: "call",
789
+ path: `__sootsimTest.${method}`,
790
+ args: [limit]
1176
791
  });
1177
- if (!isSemanticQueryResult(result)) return [];
1178
- return result.nodes.flatMap((node) => {
1179
- if (!node.role && !node.label && !node.text && !node.pressable) return [];
1180
- return [
1181
- {
1182
- role: node.role ?? (node.pressable ? "button" : node.type === "text" ? "statictext" : "none"),
1183
- label: node.label ?? node.text ?? null,
1184
- hint: null,
1185
- state: null,
1186
- testID: node.testID ?? null,
1187
- position: {
1188
- x: Math.round(node.geometry.x),
1189
- y: Math.round(node.geometry.y)
1190
- },
1191
- size: {
1192
- w: Math.round(node.geometry.width),
1193
- h: Math.round(node.geometry.height)
1194
- }
1195
- }
1196
- ];
792
+ return Array.isArray(result) ? result : [];
793
+ }
794
+ async function clearRequests(bridge) {
795
+ await bridge.send({ type: "call", path: "__sootsimTest.clearRequests", args: [] });
796
+ }
797
+ async function inspectScrollStateAt(bridge, x, y) {
798
+ const result = await bridge.send({
799
+ type: "call",
800
+ path: "__sootsimTest.getScrollStateAt",
801
+ args: [x, y]
1197
802
  });
803
+ if (!result || typeof result !== "object") return null;
804
+ const state = result;
805
+ return {
806
+ ...state,
807
+ scrollOffsetX: typeof state.scrollOffsetX === "number" ? state.scrollOffsetX : state.offsetX,
808
+ scrollOffsetY: typeof state.scrollOffsetY === "number" ? state.scrollOffsetY : state.offsetY
809
+ };
1198
810
  }
1199
- function resolveFindMode(q) {
1200
- if (q.testId) {
1201
- return { mode: "testid", selector: { testID: q.testId } };
1202
- }
1203
- if (q.role) {
1204
- return { mode: "role", selector: { role: q.role } };
1205
- }
1206
- if (q.type) {
1207
- return { mode: "type", selector: { type: q.type } };
811
+ async function inspectLogs(bridge) {
812
+ if (bridge.plane === "cloud") {
813
+ const state = await bridge.send({
814
+ type: "state",
815
+ stateOptions: {
816
+ screenshot: false,
817
+ tree: false,
818
+ route: false,
819
+ errors: false,
820
+ logs: true,
821
+ logLimit: 100
822
+ }
823
+ });
824
+ const recentLogs = state && typeof state === "object" && Array.isArray(Reflect.get(state, "recentLogs")) ? Reflect.get(state, "recentLogs") : [];
825
+ return recentLogs.flatMap((entry, index) => {
826
+ if (!entry || typeof entry !== "object") return [];
827
+ const text = Reflect.get(entry, "text");
828
+ const rawLevel = Reflect.get(entry, "level");
829
+ if (typeof text !== "string") return [];
830
+ const level = rawLevel === "log" || rawLevel === "info" || rawLevel === "warn" || rawLevel === "error" || rawLevel === "debug" ? rawLevel : "error";
831
+ const at = Reflect.get(entry, "at");
832
+ const ts = typeof at === "number" && Number.isFinite(at) ? at : 0;
833
+ return [
834
+ {
835
+ id: `cloud-${ts}-${index}`,
836
+ source: "rnx-cloud",
837
+ level,
838
+ ts,
839
+ args: [text],
840
+ stack: null
841
+ }
842
+ ];
843
+ });
1208
844
  }
1209
- if (q.pressable) {
1210
- return { mode: "pressable", selector: { pressable: true } };
845
+ const res = await bridge.send({
846
+ type: "evaluate",
847
+ code: `(() => {
848
+ const obs = window.__sootsimObservability;
849
+ if (!obs) return { ok: false };
850
+ return { ok: true, entries: obs.logs.getSnapshot() };
851
+ })()`
852
+ });
853
+ if (!res || !res.ok) return [];
854
+ return res.entries ?? [];
855
+ }
856
+ async function clearLogs(bridge) {
857
+ await bridge.send({
858
+ type: "evaluate",
859
+ code: 'window.__sootsimObservability?.logs.clear(); "cleared"'
860
+ });
861
+ }
862
+ function isForwardedWorkerLogTwin(a, b) {
863
+ if (a.source === b.source) return false;
864
+ if (a.level !== b.level) return false;
865
+ if (Math.abs(a.ts - b.ts) > 1e3) return false;
866
+ if (a.args.length !== b.args.length) return false;
867
+ const sources = /* @__PURE__ */ new Set([a.source, b.source]);
868
+ if (!sources.has("sootsim-worker")) return false;
869
+ if (!sources.has("render-worker") && !sources.has("forwarded-render-worker")) {
870
+ return false;
1211
871
  }
1212
- if (q.interactive) {
1213
- return { mode: "interactive-targets", selector: { pressable: true } };
872
+ return a.args.every((arg, index) => arg === b.args[index]);
873
+ }
874
+ function filterLogEntries(entries, opts = {}) {
875
+ let out = [];
876
+ for (const entry of entries) {
877
+ if (out.some((candidate) => isForwardedWorkerLogTwin(candidate, entry))) {
878
+ continue;
879
+ }
880
+ out.push(entry);
1214
881
  }
1215
- if (q.visible) {
1216
- return { mode: "visible", selector: { visible: true } };
882
+ if (!opts.showInternal) {
883
+ out = out.filter((e) => {
884
+ const first = e.args[0];
885
+ return !(typeof first === "string" && /^\[(?:rnx|sootsim)(?:\s[^\]]*)?\]/.test(first));
886
+ });
1217
887
  }
1218
- if (q.text) {
1219
- return { mode: "text", selector: { text: q.text } };
888
+ if (opts.level) out = out.filter((e) => opts.level.has(e.level));
889
+ if (opts.filter) {
890
+ const lf = opts.filter.toLowerCase();
891
+ out = out.filter((e) => e.args.join(" ").toLowerCase().includes(lf));
1220
892
  }
1221
- return null;
893
+ return out;
1222
894
  }
1223
- async function inspectFind(bridge, q) {
1224
- const resolved = resolveFindMode(q);
1225
- if (!resolved) return null;
1226
- const result = await bridge.send({
1227
- type: "query",
1228
- query: { kind: "find", selector: resolved.selector }
895
+ async function inspectTimelineSummary(bridge, query) {
896
+ return await bridge.send({
897
+ type: "call",
898
+ path: "SootSim.bridges.timeline.summary",
899
+ args: [query]
1229
900
  });
1230
- if (!isSemanticQueryResult(result)) return { mode: resolved.mode, result: [] };
1231
- const nodes = result.nodes.map(
1232
- (node) => ({
1233
- type: node.type,
1234
- nodeId: node.nodeId,
1235
- ...node.testID ? { testID: node.testID } : {},
1236
- ...node.text ? { text: node.text } : {},
1237
- ...node.pressable ? { pressable: true } : {},
1238
- ...node.role ? { accessibilityRole: node.role } : {},
1239
- ...node.label ? { accessibilityLabel: node.label } : {},
1240
- absolutePosition: { x: node.geometry.x, y: node.geometry.y },
1241
- layout: { width: node.geometry.width, height: node.geometry.height }
1242
- })
1243
- );
1244
- const resultValue = resolved.mode === "testid" || resolved.mode === "text" ? nodes[0] ?? null : nodes;
1245
- return { mode: resolved.mode, result: resultValue };
1246
901
  }
1247
- function rankInteractive(nodes) {
1248
- return [...nodes].sort((a, b) => scoreInteractive(b) - scoreInteractive(a));
902
+ async function inspectTimelineRecent(bridge, query) {
903
+ return await bridge.send({
904
+ type: "call",
905
+ path: "SootSim.bridges.timeline.recent",
906
+ args: [query]
907
+ });
1249
908
  }
1250
- function scoreInteractive(n) {
1251
- let score = 0;
1252
- if (n.testID) score += 100;
1253
- if (typeof n.text === "string" && n.text.trim().length > 0) score += 60;
1254
- if (typeof n.accessibilityLabel === "string" && n.accessibilityLabel.trim().length > 0) {
1255
- score += 30;
909
+ async function detectOpenNativeUI(bridge) {
910
+ let events;
911
+ try {
912
+ const res = await inspectTimelineRecent(bridge, {
913
+ kinds: ["alert", "actionsheet"],
914
+ limit: 80
915
+ });
916
+ events = res.events;
917
+ } catch {
918
+ return [];
1256
919
  }
1257
- if (n.accessibilityRole) score += 15;
1258
- const w = n.layout?.width ?? 0;
1259
- const h = n.layout?.height ?? 0;
1260
- const area = w * h;
1261
- if (area >= 400 && area <= 6e4) score += 25;
1262
- else if (area > 6e4) score -= 20;
1263
- const y = n.absolutePosition?.y ?? 0;
1264
- if (y < 0) score -= 30;
1265
- return score;
920
+ const latest = /* @__PURE__ */ new Map();
921
+ for (const ev of events) latest.set(ev.kind, ev);
922
+ const out = [];
923
+ const check = (kind, label) => {
924
+ const ev = latest.get(kind);
925
+ if (!ev || !ev.data || typeof ev.data !== "object") return;
926
+ const d = ev.data;
927
+ if (d.phase !== "show") return;
928
+ out.push({ label, title: typeof d.title === "string" ? d.title : null });
929
+ };
930
+ check("alert", "Alert");
931
+ check("actionsheet", "ActionSheet");
932
+ return out;
1266
933
  }
1267
- function tapCommandForNode(n) {
1268
- if (n.testID) return `rnx do tap-id ${shellEscape(n.testID)}`;
1269
- const text = typeof n.text === "string" ? n.text.trim() : "";
1270
- if (text.length > 0 && text.length <= 80) return `rnx do tap-text ${shellEscape(text)}`;
1271
- const x = Math.round(((n.absolutePosition?.x ?? 0) + (n.layout?.width ?? 0) / 2) * 10) / 10;
1272
- const y = Math.round(((n.absolutePosition?.y ?? 0) + (n.layout?.height ?? 0) / 2) * 10) / 10;
1273
- return `rnx do tap ${x} ${y}`;
934
+ async function inspectTimelineAdvanceCursor(bridge, cursorKey, watermark) {
935
+ await bridge.send({
936
+ type: "call",
937
+ path: "SootSim.bridges.timeline.cursorAdvance",
938
+ args: [cursorKey, watermark]
939
+ });
1274
940
  }
1275
- function shellEscape(value) {
1276
- if (/^[A-Za-z0-9_./@:-]+$/.test(value)) return value;
1277
- return `'${value.replace(/'/g, `'\\''`)}'`;
941
+ async function inspectKeyboard(bridge) {
942
+ const result = await bridge.send({
943
+ type: "evaluate",
944
+ code: `(() => {
945
+ const kb = window.__sootsimKeyboard
946
+ if (!kb || typeof kb.getLayout !== 'function') {
947
+ return { error: 'keyboard bridge getLayout() not available' }
948
+ }
949
+ return kb.getLayout()
950
+ })()`
951
+ });
952
+ return result ?? { error: "keyboard bridge returned no result" };
1278
953
  }
1279
- function waitReadyReason(status) {
1280
- if (status.bridgeError) {
1281
- return `sim unreachable: ${status.bridgeError} \u2014 the target tab is closed or no sim is connected (check \`rnx list\`, reopen with \`rnx open\`)`;
1282
- }
1283
- const base = status.externalError ? `guest app errored: ${status.externalError}` : status.loadingText ? `still showing "${status.loadingText}"` : status.externalReady === false ? "guest app is still loading" : status.flag !== true && status.targets > 0 ? "native content is rendered but the ready signal has not settled" : status.flag !== true ? "guest app has not emitted sootsim:externalAppReady" : status.targets <= 0 ? "ready flag emitted but no visible app content is inspectable yet" : "node tree is still changing";
1284
- return !status.externalError && status.suppressedEntryError ? `${base} (suppressed entry error: ${status.suppressedEntryError})` : base;
954
+ function isShellCommandUnavailable(error) {
955
+ const message = error instanceof Error ? error.message : String(error);
956
+ return message.includes("call target not found: SootSim.bridges.mainShell") || message.includes("test bridge unavailable before app-in-worker boot");
1285
957
  }
1286
- async function inspectWaitReady(bridge, timeoutMs = 2e4, options = {}) {
1287
- const start = Date.now();
1288
- let result;
1289
- try {
1290
- result = await bridge.send(
1291
- {
1292
- type: "waitFor",
1293
- waitForOptions: { condition: { type: "ready" }, timeoutMs }
1294
- },
1295
- { timeoutMs: timeoutMs + 1e3 }
1296
- );
1297
- } catch (error) {
1298
- const bridgeError = error instanceof Error ? error.message : String(error);
1299
- if (bridgeError.startsWith("multiple sims are connected:") || bridgeError.startsWith("saved sim ")) {
1300
- throw error;
958
+ async function getShellState(bridge, readyTimeoutMs = 0) {
959
+ const deadline = Date.now() + Math.max(0, readyTimeoutMs);
960
+ while (true) {
961
+ try {
962
+ return await bridge.send({
963
+ type: "call",
964
+ path: "SootSim.bridges.mainShell.getState",
965
+ args: []
966
+ });
967
+ } catch (error) {
968
+ if (!isShellCommandUnavailable(error) || Date.now() >= deadline) throw error;
969
+ await new Promise((r) => setTimeout(r, 50));
1301
970
  }
1302
- result = {
1303
- matched: false,
1304
- elapsedMs: Date.now() - start,
1305
- polls: 0,
1306
- error: bridgeError
1307
- };
1308
971
  }
1309
- const probe = result.probe;
1310
- const externalError = probe?.externalError ?? "";
1311
- const status = {
1312
- ready: result.matched && !externalError,
1313
- elapsedMs: result.elapsedMs,
1314
- nodes: probe?.nodes ?? 0,
1315
- targets: probe?.targets ?? 0,
1316
- liveFrameActive: probe?.liveFrameActive ?? false,
1317
- liveFrameChannels: probe?.liveFrameChannels ?? 0,
1318
- liveFramePublishes: probe?.liveFramePublishes ?? 0,
1319
- flag: probe?.flag,
1320
- loadingText: probe?.loadingText ?? "",
1321
- externalReady: probe?.externalReady ?? null,
1322
- externalStatus: probe?.externalStatus ?? "",
1323
- externalError,
1324
- suppressedEntryError: probe?.suppressedEntryError ?? "",
1325
- errors: probe?.errors ?? 0,
1326
- bridgeError: result.error ?? ""
1327
- };
1328
- if (!status.ready && options.onProgress) options.onProgress(status);
1329
- return status;
1330
972
  }
1331
- async function inspectWaitSelector(bridge, testId, timeoutMs = 5e3, opts = {}) {
1332
- const gone = opts.gone === true;
1333
- const result = await bridge.send(
1334
- {
1335
- type: "waitFor",
1336
- waitForOptions: {
1337
- condition: {
1338
- type: gone ? "absent" : "present",
1339
- selector: { testID: testId }
1340
- },
1341
- timeoutMs
1342
- }
1343
- },
1344
- { timeoutMs: timeoutMs + 1e3 }
1345
- );
1346
- if (result?.error) throw new Error(result.error);
1347
- if (result?.node && !isSimSemanticTree([result.node])) {
1348
- throw new Error("waitFor returned an invalid semantic node");
1349
- }
973
+ async function inspectScreens(bridge) {
974
+ const payload = await bridge.send({
975
+ type: "evaluate",
976
+ code: `(async () => {
977
+ const test = window.__sootsimTest
978
+ const kb = window.__sootsimKeyboard
979
+ const navSnap =
980
+ test && typeof test.getNavigationSnapshot === 'function'
981
+ ? await test.getNavigationSnapshot()
982
+ : null
983
+ const route =
984
+ test && typeof test.getRouteInfo === 'function'
985
+ ? await test.getRouteInfo()
986
+ : null
987
+ const keyboard =
988
+ kb && typeof kb.getLayout === 'function'
989
+ ? (() => {
990
+ const layout = kb.getLayout()
991
+ return layout ? {
992
+ visible: layout.visible,
993
+ mode: layout.mode,
994
+ spec: layout.spec
995
+ ? {
996
+ keyboardType: layout.spec.keyboardType,
997
+ returnKeyType: layout.spec.returnKeyType,
998
+ }
999
+ : null,
1000
+ } : null
1001
+ })()
1002
+ : null
1003
+ return { nav: navSnap, route, keyboard }
1004
+ })()`
1005
+ });
1006
+ const shell = await getShellState(bridge, 500).catch(() => null);
1350
1007
  return {
1351
- found: result?.matched === true,
1352
- ...result?.node ? { node: result.node } : {},
1353
- elapsed: result?.elapsedMs ?? timeoutMs
1008
+ shell,
1009
+ nav: payload?.nav ?? null,
1010
+ route: payload?.route ?? null,
1011
+ keyboard: payload?.keyboard ?? null
1354
1012
  };
1355
1013
  }
1356
- function consoleEntriesEval(level, limit) {
1357
- const getter = level === "error" ? "getErrors" : "getWarnings";
1358
- return `(() => {
1359
- const out = []
1360
- const seen = new Set()
1361
- const norm = (s) => (typeof s === 'string' ? s : (() => { try { return JSON.stringify(s) } catch { return String(s) } })())
1362
- const key = (ts, args) => Math.round((ts || 0) / 250) + '|' + (Array.isArray(args) ? args.map(norm).join(' ') : norm(args))
1363
- const push = (e) => {
1364
- if (!e) return
1365
- const ts = typeof e.timestamp === 'number' ? e.timestamp : (typeof e.ts === 'number' ? e.ts : 0)
1366
- const k = key(ts, e.args)
1367
- if (seen.has(k)) return
1368
- seen.add(k)
1369
- out.push({ timestamp: ts, args: Array.isArray(e.args) ? e.args : [e.args], stack: e.stack || undefined, source: e.source || undefined })
1370
- }
1371
- // page-realm ws-bridge buffer first (richest stacks for page-origin errors)
1372
- try { for (const e of (window.__sootsimConsole?.${getter}(${limit}) || [])) push(e) } catch {}
1373
- // engine observability store \u2014 the always-on render-worker / forwarded path
1374
- try {
1375
- const obs = window.__sootsimObservability
1376
- const snap = obs && obs.logs && typeof obs.logs.getSnapshot === 'function' ? obs.logs.getSnapshot() : []
1377
- for (const e of snap) if (e && e.level === ${JSON.stringify(level)}) push(e)
1378
- } catch {}
1379
- return out.sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0)).slice(-${limit})
1380
- })()`;
1014
+ var DEBUG_CHANNELS = [
1015
+ "portals",
1016
+ "sheets",
1017
+ "layout",
1018
+ "onlayout",
1019
+ "animated",
1020
+ "render",
1021
+ "touch",
1022
+ "yoga"
1023
+ ];
1024
+ async function inspectDebugStatus(bridge) {
1025
+ return bridge.send({ type: "evaluate", code: "window.__sootsimDebug.status()" });
1381
1026
  }
1382
- async function inspectErrors(bridge, limit = 20) {
1383
- const result = await bridge.send({
1027
+ async function inspectDebugFlags(bridge) {
1028
+ return bridge.send({ type: "evaluate", code: "window.__sootsimDebug.flags()" });
1029
+ }
1030
+ async function inspectDebugFind(bridge, target) {
1031
+ return bridge.send({
1384
1032
  type: "evaluate",
1385
- code: consoleEntriesEval("error", limit)
1033
+ code: `window.__sootsimTest.debugFind(${JSON.stringify(target)})`
1386
1034
  });
1387
- return Array.isArray(result) ? result : [];
1388
1035
  }
1389
- async function inspectWarnings(bridge, limit = 20) {
1390
- const result = await bridge.send({
1036
+ async function inspectDebugRecent(bridge, channel, limit = 50) {
1037
+ const code = channel && channel !== "all" ? `window.__sootsimDebug.recent(${JSON.stringify(channel)}, ${limit})` : `window.__sootsimDebug.recent(undefined, ${limit})`;
1038
+ return bridge.send({ type: "evaluate", code });
1039
+ }
1040
+ async function setDebugChannels(bridge, action, channels) {
1041
+ const args = channels.length > 0 ? channels.map((c) => JSON.stringify(c)).join(", ") : action === "disable" ? "'all'" : "";
1042
+ return bridge.send({
1391
1043
  type: "evaluate",
1392
- code: consoleEntriesEval("warn", limit)
1044
+ code: `window.__sootsimDebug.${action}(${args})`
1393
1045
  });
1394
- return Array.isArray(result) ? result : [];
1395
1046
  }
1396
- var MERGED_CONSOLE_COUNT_EVAL = `(() => {
1397
- const norm = (s) => (typeof s === 'string' ? s : (() => { try { return JSON.stringify(s) } catch { return String(s) } })())
1398
- const key = (lvl, ts, args) => lvl + '|' + Math.round((ts || 0) / 250) + '|' + (Array.isArray(args) ? args.map(norm).join(' ') : norm(args))
1399
- const seen = new Set()
1400
- let errors = 0
1401
- let warnings = 0
1402
- const add = (lvl, ts, args) => {
1403
- if (lvl !== 'error' && lvl !== 'warn') return
1404
- const k = key(lvl, ts, args)
1405
- if (seen.has(k)) return
1406
- seen.add(k)
1407
- if (lvl === 'error') errors++
1408
- else warnings++
1409
- }
1410
- try {
1411
- const c = window.__sootsimConsole
1412
- for (const e of (c?.getErrors?.(200) || [])) add('error', e?.timestamp ?? e?.ts, e?.args)
1413
- for (const e of (c?.getWarnings?.(200) || [])) add('warn', e?.timestamp ?? e?.ts, e?.args)
1414
- } catch {}
1415
- try {
1416
- const obs = window.__sootsimObservability
1417
- const snap = obs && obs.logs && typeof obs.logs.getSnapshot === 'function' ? obs.logs.getSnapshot() : []
1418
- for (const e of snap) if (e) add(e.level, e.ts, e.args)
1419
- } catch {}
1420
- return { errors, warnings, total: errors + warnings }
1421
- })()`;
1422
- async function clearConsole(bridge) {
1423
- await bridge.send({
1047
+ async function inspectMemory(bridge) {
1048
+ if (bridge.plane === "cloud") {
1049
+ const value = await bridge.send({ type: "memory" });
1050
+ const rawWasmHeapBytes = typeof value === "object" && value !== null ? Reflect.get(value, "wasmHeapBytes") : null;
1051
+ const wasmHeapBytes = typeof rawWasmHeapBytes === "number" ? rawWasmHeapBytes : null;
1052
+ return {
1053
+ tenant: {
1054
+ nodesRegistered: null,
1055
+ nodesDetached: null,
1056
+ detachedTypes: null,
1057
+ objects: null,
1058
+ activeNativeAnimations: null,
1059
+ imageLoader: null,
1060
+ workerHeap: null,
1061
+ wasmHeapBytes
1062
+ },
1063
+ shell: null,
1064
+ compositor: null,
1065
+ hostHeap: null
1066
+ };
1067
+ }
1068
+ const result = await bridge.send({
1424
1069
  type: "evaluate",
1425
- code: 'window.__sootsimConsole?.clear(); window.__sootsimObservability?.logs?.clear?.(); "cleared"'
1070
+ code: `(async () => {
1071
+ const host = window.__sootsimRenderHost
1072
+ const stats = host?.queryStats ? await host.queryStats() : null
1073
+ const shellRaw = window.__sootsimShellMemory
1074
+ ? await window.__sootsimShellMemory()
1075
+ : null
1076
+ const shell = shellRaw && !shellRaw.error ? shellRaw : null
1077
+ // the compositor paints home + app:one + app:two, so every app-surface
1078
+ // picture and raster image is counted here and nowhere else.
1079
+ let compositor = null
1080
+ try {
1081
+ const cs = await window.__sootsimCompositor?.getStats?.(false, true)
1082
+ compositor = cs?.memory ?? null
1083
+ } catch {}
1084
+ const normalize = (m) =>
1085
+ m
1086
+ ? {
1087
+ nodesRegistered: m.nodesRegistered ?? null,
1088
+ nodesDetached: m.nodesDetached ?? null,
1089
+ detachedTypes: m.detachedTypes ?? null,
1090
+ objects: m.objects ?? null,
1091
+ activeNativeAnimations: m.activeNativeAnimations ?? null,
1092
+ imageLoader: m.imageLoader ?? null,
1093
+ workerHeap: m.workerHeap ?? null,
1094
+ wasmHeapBytes: m.wasmHeapBytes ?? null,
1095
+ }
1096
+ : null
1097
+ const hostMem = performance.memory
1098
+ ? {
1099
+ usedJSHeapSize: performance.memory.usedJSHeapSize,
1100
+ totalJSHeapSize: performance.memory.totalJSHeapSize,
1101
+ jsHeapSizeLimit: performance.memory.jsHeapSizeLimit,
1102
+ }
1103
+ : null
1104
+ return {
1105
+ tenant: normalize(stats?.memory),
1106
+ shell: normalize(shell),
1107
+ compositor: normalize(compositor),
1108
+ hostHeap: hostMem,
1109
+ }
1110
+ })()`
1426
1111
  });
1112
+ return result ?? { tenant: null, shell: null, compositor: null, hostHeap: null };
1113
+ }
1114
+ function formatTimelineSummary(summary) {
1115
+ if (summary.total === 0) return "nothing recorded";
1116
+ const parts = [];
1117
+ const ORDER = [
1118
+ "error",
1119
+ "warning",
1120
+ "console",
1121
+ "fetch",
1122
+ "toast",
1123
+ "alert",
1124
+ "actionsheet",
1125
+ "picker",
1126
+ "notification",
1127
+ "screen",
1128
+ "route",
1129
+ "keyboard",
1130
+ "app-launch",
1131
+ "shell",
1132
+ "scroll",
1133
+ "gesture",
1134
+ "text-input",
1135
+ "react-commit",
1136
+ "animation",
1137
+ "reanimated"
1138
+ ];
1139
+ const seen = /* @__PURE__ */ new Set();
1140
+ for (const k of ORDER) {
1141
+ const n = summary.byKind[k];
1142
+ if (n) {
1143
+ parts.push(`${n} ${k}${n === 1 ? "" : "s"}`);
1144
+ seen.add(k);
1145
+ }
1146
+ }
1147
+ for (const [k, n] of Object.entries(summary.byKind)) {
1148
+ if (!seen.has(k) && n) parts.push(`${n} ${k}${n === 1 ? "" : "s"}`);
1149
+ }
1150
+ return parts.join(" \xB7 ");
1151
+ }
1152
+ var NOISY_TIMELINE_KINDS = /* @__PURE__ */ new Set([
1153
+ "react-commit",
1154
+ "layout",
1155
+ "scroll"
1156
+ ]);
1157
+ function formatTimelineRelativeTime(t, anchor) {
1158
+ if (anchor === null) return new Date(t).toLocaleTimeString();
1159
+ const dt = (t - anchor) / 1e3;
1160
+ const sign = dt >= 0 ? "+" : "";
1161
+ return `${sign}${dt.toFixed(2)}s`;
1162
+ }
1163
+ function formatTimelinePayload(kind, d) {
1164
+ switch (kind) {
1165
+ case "app-launch":
1166
+ return d.phase === "launch" ? `launch ${d.appName ?? d.toAppId ?? ""}` : `dismiss ${d.appName ?? d.fromAppId ?? ""} \u2192 ${d.toAppId ?? ""}`;
1167
+ case "toast":
1168
+ return `"${d.text ?? ""}"${d.durationMs ? ` (${d.durationMs}ms)` : ""}`;
1169
+ case "keyboard":
1170
+ return `${d.phase ?? "?"}${d.heightPx ? ` h=${d.heightPx}` : ""}${d.mode ? ` ${d.mode}` : ""}`;
1171
+ case "screen":
1172
+ return `${d.phase ?? "?"} ${d.name ?? d.activeName ?? ""}`;
1173
+ case "flow-step":
1174
+ return `${d.status ?? "?"} ${d.stepName ?? "step"}${d.durationMs ? ` ${d.durationMs}ms` : ""}`;
1175
+ case "route":
1176
+ return `${d.phase ?? "?"} ${d.path ?? d.pathname ?? ""}`;
1177
+ case "alert":
1178
+ case "actionsheet":
1179
+ case "picker":
1180
+ return `${d.phase ?? "?"} ${d.title ?? d.message ?? ""}`;
1181
+ case "notification":
1182
+ return `${d.title ?? ""}${d.body ? ` \u2014 ${d.body}` : ""}`;
1183
+ case "fetch":
1184
+ return `${d.method ?? "GET"} ${d.url ?? ""}${d.status ? ` -> ${d.status}` : ""}`;
1185
+ case "console":
1186
+ case "console-log":
1187
+ return `${d.level ?? "log"}: ${(d.message ?? "").toString().slice(0, 120)}`;
1188
+ case "shell":
1189
+ return `${d.event ?? d.type ?? d.phase ?? ""}`;
1190
+ case "scroll":
1191
+ return `${d.phase ?? "?"} ${d.target ?? ""}`;
1192
+ case "gesture":
1193
+ return `${d.phase ?? "?"} ${d.type ?? ""}`;
1194
+ case "text-input":
1195
+ return `${d.phase ?? "?"}${d.value !== void 0 ? ` "${String(d.value).slice(0, 40)}"` : ""}`;
1196
+ case "layout":
1197
+ return `${d.kind ?? "?"} ${d.testID ?? d.type ?? ""}${d.skipped ? ` skipped:${d.reason ?? "unknown"}` : ""}`;
1198
+ case "react-commit": {
1199
+ const slowest = d.slowest;
1200
+ return `${d.fiberCount ?? "?"} fibers ${d.durationMs ?? "?"}ms${slowest?.displayName ? ` \xB7 ${slowest.displayName} ${slowest.durationMs ?? "?"}ms` : ""}`;
1201
+ }
1202
+ case "frame":
1203
+ return `${d.totalMs ?? "?"}ms${d.renderMs ? ` \xB7 render ${d.renderMs}ms` : ""}${d.layoutMs ? ` \xB7 layout ${d.layoutMs}ms` : ""}${d.copyMs ? ` \xB7 copy ${d.copyMs}ms` : ""}`;
1204
+ case "reanimated":
1205
+ case "animation":
1206
+ return `${d.kind ?? ""} ${d.target ?? ""}${d.durationMs ? ` ${d.durationMs}ms` : ""}`;
1207
+ }
1208
+ return "";
1427
1209
  }
1428
- async function inspectRequests(bridge, opts = {}) {
1429
- const limit = opts.limit ?? 20;
1430
- const method = opts.failed === false ? "getRequests" : "getFailedRequests";
1431
- const result = await bridge.send({
1432
- type: "call",
1433
- path: `__sootsimTest.${method}`,
1434
- args: [limit]
1435
- });
1436
- return Array.isArray(result) ? result : [];
1210
+ function formatTimelineEvent(event, anchor) {
1211
+ const ts = formatTimelineRelativeTime(event.t, anchor).padStart(8);
1212
+ const ctx = event.context.padEnd(6);
1213
+ const kind = `[${event.kind}]`.padEnd(15);
1214
+ const d = event.data;
1215
+ const payload = d && typeof d === "object" ? formatTimelinePayload(event.kind, d) : "";
1216
+ return ` ${ts} ${ctx} ${kind} ${payload}`;
1437
1217
  }
1438
- async function clearRequests(bridge) {
1439
- await bridge.send({ type: "call", path: "__sootsimTest.clearRequests", args: [] });
1218
+ function formatTimelineEvents(events) {
1219
+ if (!events.length) return "";
1220
+ const anchor = events[0]?.t ?? null;
1221
+ return events.map((e) => formatTimelineEvent(e, anchor)).join("\n");
1440
1222
  }
1441
- async function inspectScrollStateAt(bridge, x, y) {
1223
+
1224
+ // cli/commands/inspect/resolve-target.ts
1225
+ async function resolveTargetCoords(bridge, target) {
1442
1226
  const result = await bridge.send({
1443
- type: "call",
1444
- path: "__sootsimTest.getScrollStateAt",
1445
- args: [x, y]
1227
+ type: "resolve",
1228
+ selector: target.mode === "testid" ? { testID: target.value } : { text: target.value }
1446
1229
  });
1447
- if (!result || typeof result !== "object") return null;
1448
- const state = result;
1230
+ if (result === null) return null;
1231
+ if (typeof result !== "object") throw new Error("resolve returned an invalid result");
1232
+ const match = Reflect.get(result, "match");
1233
+ const resolvedTarget = Reflect.get(result, "target");
1234
+ const semanticNodes = [match, resolvedTarget];
1235
+ const point = Reflect.get(result, "point");
1236
+ const x = point !== null && typeof point === "object" ? Reflect.get(point, "x") : void 0;
1237
+ const y = point !== null && typeof point === "object" ? Reflect.get(point, "y") : void 0;
1238
+ if (!isSimSemanticTree(semanticNodes) || typeof x !== "number" || !Number.isFinite(x) || typeof y !== "number" || !Number.isFinite(y)) {
1239
+ throw new Error("resolve returned an invalid result");
1240
+ }
1241
+ const semanticMatch = semanticNodes[0];
1242
+ if (!semanticMatch) throw new Error("resolve returned an invalid result");
1449
1243
  return {
1450
- ...state,
1451
- scrollOffsetX: typeof state.scrollOffsetX === "number" ? state.scrollOffsetX : state.offsetX,
1452
- scrollOffsetY: typeof state.scrollOffsetY === "number" ? state.scrollOffsetY : state.offsetY
1244
+ x,
1245
+ y,
1246
+ testID: semanticMatch.testID ?? null,
1247
+ text: semanticMatch.text ?? (target.mode === "text" ? target.value : semanticMatch.label),
1248
+ type: semanticMatch.type
1453
1249
  };
1454
1250
  }
1455
- async function inspectLogs(bridge) {
1456
- if (bridge.plane === "cloud") {
1457
- const state = await bridge.send({
1458
- type: "state",
1459
- stateOptions: {
1460
- screenshot: false,
1461
- tree: false,
1462
- route: false,
1463
- errors: false,
1464
- logs: true,
1465
- logLimit: 100
1251
+
1252
+ // cli/commands/inspect/settling.ts
1253
+ async function waitForSootsimIdle({
1254
+ bridge,
1255
+ simId,
1256
+ maxMs,
1257
+ pollMs = 50,
1258
+ stablePolls = 3,
1259
+ strict = false
1260
+ }) {
1261
+ const result = await bridge.send(
1262
+ {
1263
+ type: "evaluate",
1264
+ simId,
1265
+ code: `(async () => {
1266
+ const start = Date.now()
1267
+ const deadline = start + ${Math.max(0, Math.round(maxMs))}
1268
+ const pollMs = ${Math.max(1, Math.round(pollMs))}
1269
+ const requiredStablePolls = ${Math.max(1, Math.round(stablePolls))}
1270
+ const strict = ${strict ? "true" : "false"}
1271
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
1272
+
1273
+ // route-aware settle: a tap that pushes/pops a screen is async \u2014 the
1274
+ // old screen still renders for a moment, then the new one mounts and
1275
+ // its content loads. a pure layout-hash check reports "idle" on the
1276
+ // outgoing screen or on the incoming skeleton, so a driver re-taps
1277
+ // and stacks duplicate navigations. drain any in-flight screen
1278
+ // transition FIRST, bounded by the overall budget. when no
1279
+ // transition is happening this returns in ~80ms (startWindowMs), so
1280
+ // a plain button tap barely pays for it.
1281
+ try {
1282
+ const wfst = window.__sootsimTest?.waitForScreenTransitions
1283
+ if (typeof wfst === 'function') {
1284
+ const remaining = deadline - Date.now()
1285
+ if (remaining > 120) {
1286
+ await wfst({
1287
+ timeoutMs: Math.min(remaining - 80, 4000),
1288
+ settleMs: 64,
1289
+ startWindowMs: 80,
1290
+ })
1291
+ }
1292
+ }
1293
+ } catch {}
1294
+
1295
+ const readSnapshot = async () => {
1296
+ let animating = false
1297
+ let layoutDirty = false
1298
+ let pendingFetches = 0
1299
+ let requestTotal = 0
1300
+ let requestInFlight = 0
1301
+ let renderStatsAvailable = false
1302
+ try {
1303
+ const stats = await window.__sootsimRenderHost?.queryStats?.()
1304
+ if (stats) {
1305
+ renderStatsAvailable = true
1306
+ animating =
1307
+ stats.hasActiveAnims === true ||
1308
+ stats.hasActiveNativeAnimations === true ||
1309
+ stats.hasPendingAnimationFrames === true
1310
+ layoutDirty =
1311
+ stats.layoutDirty === true && stats.renderRequested === true
1312
+ // a freshly-pushed screen showing a skeleton is layout-stable
1313
+ // but not actually settled \u2014 its data/images are still in
1314
+ // flight. treat bounded image-loader fetches as not-idle so
1315
+ // settle waits for real content, capped by the budget.
1316
+ const pf = stats.memory && stats.memory.imageLoader
1317
+ ? stats.memory.imageLoader.pendingFetches
1318
+ : 0
1319
+ pendingFetches = typeof pf === 'number' ? pf : 0
1320
+ }
1321
+ } catch {}
1322
+ try {
1323
+ const counts = await window.__sootsimTest?.getRequestCounts?.()
1324
+ requestTotal = typeof counts?.total === 'number' ? counts.total : 0
1325
+ requestInFlight = typeof counts?.inFlight === 'number' ? counts.inFlight : 0
1326
+ } catch {}
1327
+ const root = window.__sootsimRoot
1328
+ const nodes = []
1329
+ if (root) {
1330
+ const walk = (n) => {
1331
+ if (n.layout && n.layout.width > 0) {
1332
+ nodes.push(Math.round(n.layout.x) + ',' + Math.round(n.layout.y) + ',' + Math.round(n.layout.width))
1333
+ }
1334
+ for (const c of n.children || []) walk(c)
1335
+ }
1336
+ walk(root)
1337
+ }
1338
+ return { layout: nodes.join(';'), animating, layoutDirty, pendingFetches, renderStatsAvailable, requestTotal, requestInFlight }
1466
1339
  }
1467
- });
1468
- const recentLogs = state && typeof state === "object" && Array.isArray(Reflect.get(state, "recentLogs")) ? Reflect.get(state, "recentLogs") : [];
1469
- return recentLogs.flatMap((entry, index) => {
1470
- if (!entry || typeof entry !== "object") return [];
1471
- const text = Reflect.get(entry, "text");
1472
- const rawLevel = Reflect.get(entry, "level");
1473
- if (typeof text !== "string") return [];
1474
- const level = rawLevel === "log" || rawLevel === "info" || rawLevel === "warn" || rawLevel === "error" || rawLevel === "debug" ? rawLevel : "error";
1475
- const at = Reflect.get(entry, "at");
1476
- const ts = typeof at === "number" && Number.isFinite(at) ? at : 0;
1477
- return [
1478
- {
1479
- id: `cloud-${ts}-${index}`,
1480
- source: "rnx-cloud",
1481
- level,
1482
- ts,
1483
- args: [text],
1484
- stack: null
1340
+
1341
+ // how long generic background network is allowed to keep us waiting AFTER
1342
+ // the screen is otherwise visually settled. apps with continuous traffic
1343
+ // (polling, realtime sockets, a tap that fires 20-50 fetches) never reach
1344
+ // requestInFlight===0 / a stable requestTotal, so requiring full network
1345
+ // quiet burns the ENTIRE budget on every command \u2014 the leading idle that
1346
+ // failed PR-preview recordings for network-heavy apps (3pc fight-pulse).
1347
+ // layout-stability + the image-loader already prove visible content
1348
+ // loaded (a data load changes layout; an image load shows in the loader),
1349
+ // so the generic request counters are a courtesy, not a gate: honor them
1350
+ // only within this grace once the screen is visually still.
1351
+ const networkQuietGraceMs = 1200
1352
+ let lastLayout = ''
1353
+ let lastRequestTotal = -1
1354
+ let stable = 0
1355
+ let visuallyStillSince = 0
1356
+ while (Date.now() < deadline) {
1357
+ const snapshot = await readSnapshot()
1358
+ const strictOk =
1359
+ !strict ||
1360
+ (snapshot.renderStatsAvailable && !snapshot.animating && !snapshot.layoutDirty)
1361
+ // visually settled: no animations, no image content loading, layout
1362
+ // unchanged since the last poll.
1363
+ const visuallyStill =
1364
+ strictOk && snapshot.pendingFetches === 0 && snapshot.layout === lastLayout
1365
+ // generic network quiet: nothing in flight and no new requests issued.
1366
+ const networkQuiet =
1367
+ snapshot.requestInFlight === 0 && snapshot.requestTotal === lastRequestTotal
1368
+ if (visuallyStill) {
1369
+ stable++
1370
+ if (visuallyStillSince === 0) visuallyStillSince = Date.now()
1371
+ // settle once the screen has held still long enough AND either the
1372
+ // network is genuinely quiet OR it has refused to quiet within the
1373
+ // grace (continuous background traffic must not block forever).
1374
+ if (
1375
+ stable >= requiredStablePolls &&
1376
+ (networkQuiet || Date.now() - visuallyStillSince >= networkQuietGraceMs)
1377
+ ) {
1378
+ return { settled: true, elapsed: Date.now() - start }
1379
+ }
1380
+ } else {
1381
+ stable = 0
1382
+ visuallyStillSince = 0
1485
1383
  }
1486
- ];
1487
- });
1488
- }
1489
- const res = await bridge.send({
1490
- type: "evaluate",
1491
- code: `(() => {
1492
- const obs = window.__sootsimObservability;
1493
- if (!obs) return { ok: false };
1494
- return { ok: true, entries: obs.logs.getSnapshot() };
1495
- })()`
1496
- });
1497
- if (!res || !res.ok) return [];
1498
- return res.entries ?? [];
1499
- }
1500
- async function clearLogs(bridge) {
1501
- await bridge.send({
1502
- type: "evaluate",
1503
- code: 'window.__sootsimObservability?.logs.clear(); "cleared"'
1504
- });
1505
- }
1506
- function isForwardedWorkerLogTwin(a, b) {
1507
- if (a.source === b.source) return false;
1508
- if (a.level !== b.level) return false;
1509
- if (Math.abs(a.ts - b.ts) > 1e3) return false;
1510
- if (a.args.length !== b.args.length) return false;
1511
- const sources = /* @__PURE__ */ new Set([a.source, b.source]);
1512
- if (!sources.has("sootsim-worker")) return false;
1513
- if (!sources.has("render-worker") && !sources.has("forwarded-render-worker")) {
1514
- return false;
1515
- }
1516
- return a.args.every((arg, index) => arg === b.args[index]);
1517
- }
1518
- function filterLogEntries(entries, opts = {}) {
1519
- let out = [];
1520
- for (const entry of entries) {
1521
- if (out.some((candidate) => isForwardedWorkerLogTwin(candidate, entry))) {
1522
- continue;
1384
+ lastLayout = snapshot.layout
1385
+ lastRequestTotal = snapshot.requestTotal
1386
+ await sleep(pollMs)
1387
+ }
1388
+ return { settled: false, elapsed: Date.now() - start }
1389
+ })()`
1390
+ },
1391
+ {
1392
+ timeoutMs: maxMs + 1e3
1523
1393
  }
1524
- out.push(entry);
1525
- }
1526
- if (!opts.showInternal) {
1527
- out = out.filter((e) => {
1528
- const first = e.args[0];
1529
- return !(typeof first === "string" && /^\[(?:rnx|sootsim)(?:\s[^\]]*)?\]/.test(first));
1530
- });
1394
+ );
1395
+ const { elapsed, settled } = result ?? {};
1396
+ return {
1397
+ elapsed: typeof elapsed === "number" ? elapsed : maxMs,
1398
+ settled: settled === true
1399
+ };
1400
+ }
1401
+
1402
+ // cli/commands/inspect/actions.ts
1403
+ function isScreenSize(value) {
1404
+ if (value === null || typeof value !== "object") return false;
1405
+ const width = Reflect.get(value, "width");
1406
+ const height = Reflect.get(value, "height");
1407
+ return typeof width === "number" && Number.isFinite(width) && width > 0 && typeof height === "number" && Number.isFinite(height) && height > 0;
1408
+ }
1409
+ function viewportFromCapture(value) {
1410
+ if (value === null || typeof value !== "object") return void 0;
1411
+ const tree = Reflect.get(value, "tree");
1412
+ return tree !== null && typeof tree === "object" ? Reflect.get(tree, "viewport") : void 0;
1413
+ }
1414
+ async function readScreen(bridge) {
1415
+ try {
1416
+ const viewport = viewportFromCapture(
1417
+ await bridge.send({ type: "capture", includeVisual: false })
1418
+ );
1419
+ return isScreenSize(viewport) ? { width: viewport.width, height: viewport.height } : null;
1420
+ } catch {
1421
+ return null;
1531
1422
  }
1532
- if (opts.level) out = out.filter((e) => opts.level.has(e.level));
1533
- if (opts.filter) {
1534
- const lf = opts.filter.toLowerCase();
1535
- out = out.filter((e) => e.args.join(" ").toLowerCase().includes(lf));
1423
+ }
1424
+ function offscreenPayload(cx, cy, screen) {
1425
+ if (!screen || cx >= 0 && cy >= 0 && cx <= screen.width && cy <= screen.height) {
1426
+ return {};
1536
1427
  }
1537
- return out;
1428
+ return { offscreen: true, screen };
1538
1429
  }
1539
- async function inspectTimelineSummary(bridge, query) {
1540
- return await bridge.send({
1541
- type: "call",
1542
- path: "SootSim.bridges.timeline.summary",
1543
- args: [query]
1544
- });
1430
+ function geometryContains(geometry, x, y) {
1431
+ return x >= geometry.x && y >= geometry.y && x <= geometry.x + geometry.width && y <= geometry.y + geometry.height;
1545
1432
  }
1546
- async function inspectTimelineRecent(bridge, query) {
1547
- return await bridge.send({
1548
- type: "call",
1549
- path: "SootSim.bridges.timeline.recent",
1550
- args: [query]
1551
- });
1433
+ function hitTestSemanticTree(nodes, x, y, ancestors = []) {
1434
+ for (let index = nodes.length - 1; index >= 0; index--) {
1435
+ const node = nodes[index];
1436
+ if (!node || !geometryContains(node.geometry, x, y)) continue;
1437
+ const childHit = node.children?.length ? hitTestSemanticTree(node.children, x, y, [...ancestors, node]) : null;
1438
+ return childHit ?? { node, ancestors };
1439
+ }
1440
+ return null;
1552
1441
  }
1553
- async function detectOpenNativeUI(bridge) {
1554
- let events;
1555
- try {
1556
- const res = await inspectTimelineRecent(bridge, {
1557
- kinds: ["alert", "actionsheet"],
1558
- limit: 80
1559
- });
1560
- events = res.events;
1561
- } catch {
1562
- return [];
1442
+ function nodeMatchesRequested(node, requested) {
1443
+ if (typeof requested.nodeId === "number" && node.nodeId === requested.nodeId) {
1444
+ return true;
1563
1445
  }
1564
- const latest = /* @__PURE__ */ new Map();
1565
- for (const ev of events) latest.set(ev.kind, ev);
1566
- const out = [];
1567
- const check = (kind, label) => {
1568
- const ev = latest.get(kind);
1569
- if (!ev || !ev.data || typeof ev.data !== "object") return;
1570
- const d = ev.data;
1571
- if (d.phase !== "show") return;
1572
- out.push({ label, title: typeof d.title === "string" ? d.title : null });
1446
+ if (requested.testID && node.testID === requested.testID) return true;
1447
+ if (requested.id && node.testID === requested.id) return true;
1448
+ return false;
1449
+ }
1450
+ function coveringNodeSummary(node) {
1451
+ return {
1452
+ nodeId: node.nodeId,
1453
+ testID: node.testID ?? null,
1454
+ text: node.text ?? node.label ?? null,
1455
+ type: node.type
1573
1456
  };
1574
- check("alert", "Alert");
1575
- check("actionsheet", "ActionSheet");
1576
- return out;
1577
1457
  }
1578
- async function inspectTimelineAdvanceCursor(bridge, cursorKey, watermark) {
1579
- await bridge.send({
1580
- type: "call",
1581
- path: "SootSim.bridges.timeline.cursorAdvance",
1582
- args: [cursorKey, watermark]
1583
- });
1458
+ var DEFAULT_AGENT_TIMING = {
1459
+ initialWaitMs: 3e3,
1460
+ deadlineMs: 5e3,
1461
+ retryWaitMs: 700
1462
+ };
1463
+ var DEFAULT_INTERACTIVE_TIMING = {
1464
+ initialWaitMs: 1200,
1465
+ deadlineMs: 2500,
1466
+ retryWaitMs: 700
1467
+ };
1468
+ function tapTiming(agent, opts = {}) {
1469
+ return {
1470
+ ...agent ? DEFAULT_AGENT_TIMING : DEFAULT_INTERACTIVE_TIMING,
1471
+ ...opts
1472
+ };
1584
1473
  }
1585
- async function inspectKeyboard(bridge) {
1586
- const result = await bridge.send({
1587
- type: "evaluate",
1588
- code: `(() => {
1589
- const kb = window.__sootsimKeyboard
1590
- if (!kb || typeof kb.getLayout !== 'function') {
1591
- return { error: 'keyboard bridge getLayout() not available' }
1592
- }
1593
- return kb.getLayout()
1594
- })()`
1595
- });
1596
- return result ?? { error: "keyboard bridge returned no result" };
1474
+ function isTapSuccess(result) {
1475
+ if (!result || result.hit === false || result.ok === false) return false;
1476
+ if (result.handled === false) return false;
1477
+ if (result.requestedTargetMatched === false) return false;
1478
+ if (result.pointerTapHandled === false && !result.keyboardOpened && !result.isTextInput) {
1479
+ return false;
1480
+ }
1481
+ return true;
1597
1482
  }
1598
- function isShellCommandUnavailable(error) {
1599
- const message = error instanceof Error ? error.message : String(error);
1600
- return message.includes("call target not found: SootSim.bridges.mainShell") || message.includes("test bridge unavailable before app-in-worker boot");
1483
+ function tapTargetFromPayload(payload, textFallback) {
1484
+ return {
1485
+ nodeId: payload.target?.nodeId ?? payload.match?.nodeId ?? payload.node?.nodeId ?? null,
1486
+ id: payload.target?.id ?? payload.match?.id ?? payload.node?.id ?? null,
1487
+ testID: payload.target?.testID ?? payload.match?.testID ?? payload.node?.testID ?? null,
1488
+ text: payload.target?.text ?? payload.target?.accessibilityLabel ?? payload.match?.text ?? payload.match?.accessibilityLabel ?? payload.node?.text ?? textFallback ?? null,
1489
+ type: payload.target?.type ?? payload.match?.type ?? payload.node?.type ?? null
1490
+ };
1601
1491
  }
1602
- async function getShellState(bridge, readyTimeoutMs = 0) {
1603
- const deadline = Date.now() + Math.max(0, readyTimeoutMs);
1604
- while (true) {
1492
+ async function tapResolvedTarget(bridge, args) {
1493
+ const timing = tapTiming(!!args.agent, args.timing);
1494
+ let attempts = 0;
1495
+ let lastPayload = null;
1496
+ let lastResult = null;
1497
+ const canWaitForIdle = bridge.plane !== "cloud" && timing.initialWaitMs > 0;
1498
+ if (canWaitForIdle) {
1605
1499
  try {
1606
- return await bridge.send({
1607
- type: "call",
1608
- path: "SootSim.bridges.mainShell.getState",
1609
- args: []
1500
+ await waitForSootsimIdle({
1501
+ bridge,
1502
+ maxMs: timing.initialWaitMs,
1503
+ pollMs: 32,
1504
+ stablePolls: 2
1610
1505
  });
1611
- } catch (error) {
1612
- if (!isShellCommandUnavailable(error) || Date.now() >= deadline) throw error;
1613
- await new Promise((r) => setTimeout(r, 50));
1506
+ } catch {
1507
+ }
1508
+ }
1509
+ const deadline = Date.now() + timing.deadlineMs;
1510
+ let lastOffscreenAt = null;
1511
+ const screen = await readScreen(bridge);
1512
+ while (Date.now() <= deadline || attempts === 0) {
1513
+ attempts++;
1514
+ const resolved = await args.resolve();
1515
+ const payload = resolved && typeof resolved.cx === "number" && typeof resolved.cy === "number" ? { ...resolved, ...offscreenPayload(resolved.cx, resolved.cy, screen) } : resolved;
1516
+ lastPayload = payload;
1517
+ if (payload?.error === "bridge-not-ready" || payload?.ambiguous || payload?.nthOutOfRange) {
1518
+ return { payload, result: null, attempts, failure: "special" };
1519
+ }
1520
+ if (payload && typeof payload.cx === "number" && typeof payload.cy === "number") {
1521
+ if (payload.offscreen) {
1522
+ lastResult = {
1523
+ hit: false,
1524
+ reason: "offscreen",
1525
+ x: payload.cx,
1526
+ y: payload.cy,
1527
+ screen: payload.screen
1528
+ };
1529
+ const at = `${Math.round(payload.cx)},${Math.round(payload.cy)}`;
1530
+ if (lastOffscreenAt === at) {
1531
+ return { payload, result: lastResult, attempts, failure: "missed" };
1532
+ }
1533
+ lastOffscreenAt = at;
1534
+ } else {
1535
+ lastOffscreenAt = null;
1536
+ const requestedTarget = tapTargetFromPayload(payload, args.textFallback);
1537
+ const { tree } = await inspectTree(bridge, 50);
1538
+ if (Array.isArray(tree) && tree.length > 0) {
1539
+ const hit = hitTestSemanticTree(tree, payload.cx, payload.cy);
1540
+ const matched = hit !== null && (nodeMatchesRequested(hit.node, requestedTarget) || hit.ancestors.some(
1541
+ (ancestor) => nodeMatchesRequested(ancestor, requestedTarget)
1542
+ ));
1543
+ if (hit && !matched) {
1544
+ lastResult = {
1545
+ hit: false,
1546
+ reason: "target-covered",
1547
+ requestedTarget,
1548
+ coordinateTarget: coveringNodeSummary(hit.node)
1549
+ };
1550
+ return { payload, result: lastResult, attempts, failure: "missed" };
1551
+ }
1552
+ }
1553
+ const result = await tapCoordinates(
1554
+ bridge,
1555
+ payload.cx,
1556
+ payload.cy,
1557
+ requestedTarget
1558
+ );
1559
+ lastResult = result;
1560
+ if (isTapSuccess(result)) return { payload, result, attempts };
1561
+ }
1562
+ }
1563
+ const remaining = deadline - Date.now();
1564
+ if (remaining <= 0) break;
1565
+ if (bridge.plane === "cloud" || timing.retryWaitMs <= 0) break;
1566
+ try {
1567
+ await waitForSootsimIdle({
1568
+ bridge,
1569
+ maxMs: Math.min(remaining, timing.retryWaitMs),
1570
+ pollMs: 32,
1571
+ stablePolls: 2
1572
+ });
1573
+ } catch {
1574
+ await new Promise((resolve) => setTimeout(resolve, Math.min(120, remaining)));
1614
1575
  }
1615
1576
  }
1616
- }
1617
- async function inspectScreens(bridge) {
1618
- const payload = await bridge.send({
1619
- type: "evaluate",
1620
- code: `(async () => {
1621
- const test = window.__sootsimTest
1622
- const kb = window.__sootsimKeyboard
1623
- const navSnap =
1624
- test && typeof test.getNavigationSnapshot === 'function'
1625
- ? await test.getNavigationSnapshot()
1626
- : null
1627
- const route =
1628
- test && typeof test.getRouteInfo === 'function'
1629
- ? await test.getRouteInfo()
1630
- : null
1631
- const keyboard =
1632
- kb && typeof kb.getLayout === 'function'
1633
- ? (() => {
1634
- const layout = kb.getLayout()
1635
- return layout ? {
1636
- visible: layout.visible,
1637
- mode: layout.mode,
1638
- spec: layout.spec
1639
- ? {
1640
- keyboardType: layout.spec.keyboardType,
1641
- returnKeyType: layout.spec.returnKeyType,
1642
- }
1643
- : null,
1644
- } : null
1645
- })()
1646
- : null
1647
- return { nav: navSnap, route, keyboard }
1648
- })()`
1649
- });
1650
- const shell = await getShellState(bridge, 500).catch(() => null);
1651
1577
  return {
1652
- shell,
1653
- nav: payload?.nav ?? null,
1654
- route: payload?.route ?? null,
1655
- keyboard: payload?.keyboard ?? null
1578
+ payload: lastPayload,
1579
+ result: lastResult,
1580
+ attempts,
1581
+ failure: lastPayload && typeof lastPayload.cx === "number" ? "missed" : "not-found"
1656
1582
  };
1657
1583
  }
1658
- var DEBUG_CHANNELS = [
1659
- "portals",
1660
- "sheets",
1661
- "layout",
1662
- "onlayout",
1663
- "animated",
1664
- "render",
1665
- "touch",
1666
- "yoga"
1667
- ];
1668
- async function inspectDebugStatus(bridge) {
1669
- return bridge.send({ type: "evaluate", code: "window.__sootsimDebug.status()" });
1670
- }
1671
- async function inspectDebugFlags(bridge) {
1672
- return bridge.send({ type: "evaluate", code: "window.__sootsimDebug.flags()" });
1673
- }
1674
- async function inspectDebugFind(bridge, target) {
1675
- return bridge.send({
1676
- type: "evaluate",
1677
- code: `window.__sootsimTest.debugFind(${JSON.stringify(target)})`
1678
- });
1679
- }
1680
- async function inspectDebugRecent(bridge, channel, limit = 50) {
1681
- const code = channel && channel !== "all" ? `window.__sootsimDebug.recent(${JSON.stringify(channel)}, ${limit})` : `window.__sootsimDebug.recent(undefined, ${limit})`;
1682
- return bridge.send({ type: "evaluate", code });
1683
- }
1684
- async function setDebugChannels(bridge, action, channels) {
1685
- const args = channels.length > 0 ? channels.map((c) => JSON.stringify(c)).join(", ") : action === "disable" ? "'all'" : "";
1584
+ async function tapCoordinates(bridge, x, y, target) {
1686
1585
  return bridge.send({
1687
- type: "evaluate",
1688
- code: `window.__sootsimDebug.${action}(${args})`
1586
+ type: "tap",
1587
+ x,
1588
+ y,
1589
+ ...target ? { target } : {}
1689
1590
  });
1690
1591
  }
1691
- async function inspectMemory(bridge) {
1692
- if (bridge.plane === "cloud") {
1693
- const value = await bridge.send({ type: "memory" });
1694
- const rawWasmHeapBytes = typeof value === "object" && value !== null ? Reflect.get(value, "wasmHeapBytes") : null;
1695
- const wasmHeapBytes = typeof rawWasmHeapBytes === "number" ? rawWasmHeapBytes : null;
1696
- return {
1697
- tenant: {
1698
- nodesRegistered: null,
1699
- nodesDetached: null,
1700
- detachedTypes: null,
1701
- objects: null,
1702
- activeNativeAnimations: null,
1703
- imageLoader: null,
1704
- workerHeap: null,
1705
- wasmHeapBytes
1706
- },
1707
- shell: null,
1708
- compositor: null,
1709
- hostHeap: null
1710
- };
1711
- }
1712
- const result = await bridge.send({
1713
- type: "evaluate",
1714
- code: `(async () => {
1715
- const host = window.__sootsimRenderHost
1716
- const stats = host?.queryStats ? await host.queryStats() : null
1717
- const shellRaw = window.__sootsimShellMemory
1718
- ? await window.__sootsimShellMemory()
1719
- : null
1720
- const shell = shellRaw && !shellRaw.error ? shellRaw : null
1721
- // the compositor paints home + app:one + app:two, so every app-surface
1722
- // picture and raster image is counted here and nowhere else.
1723
- let compositor = null
1724
- try {
1725
- const cs = await window.__sootsimCompositor?.getStats?.(false, true)
1726
- compositor = cs?.memory ?? null
1727
- } catch {}
1728
- const normalize = (m) =>
1729
- m
1730
- ? {
1731
- nodesRegistered: m.nodesRegistered ?? null,
1732
- nodesDetached: m.nodesDetached ?? null,
1733
- detachedTypes: m.detachedTypes ?? null,
1734
- objects: m.objects ?? null,
1735
- activeNativeAnimations: m.activeNativeAnimations ?? null,
1736
- imageLoader: m.imageLoader ?? null,
1737
- workerHeap: m.workerHeap ?? null,
1738
- wasmHeapBytes: m.wasmHeapBytes ?? null,
1739
- }
1740
- : null
1741
- const hostMem = performance.memory
1742
- ? {
1743
- usedJSHeapSize: performance.memory.usedJSHeapSize,
1744
- totalJSHeapSize: performance.memory.totalJSHeapSize,
1745
- jsHeapSizeLimit: performance.memory.jsHeapSizeLimit,
1746
- }
1747
- : null
1748
- return {
1749
- tenant: normalize(stats?.memory),
1750
- shell: normalize(shell),
1751
- compositor: normalize(compositor),
1752
- hostHeap: hostMem,
1753
- }
1754
- })()`
1755
- });
1756
- return result ?? { tenant: null, shell: null, compositor: null, hostHeap: null };
1592
+ function payloadFromResolved(resolved, match, extra = {}) {
1593
+ return {
1594
+ cx: resolved.x,
1595
+ cy: resolved.y,
1596
+ match: {
1597
+ nodeId: match.nodeId ?? null,
1598
+ id: match.testID ?? null,
1599
+ testID: match.testID ?? null,
1600
+ text: match.text ?? null,
1601
+ type: match.type ?? null
1602
+ },
1603
+ target: {
1604
+ nodeId: match.nodeId ?? null,
1605
+ id: resolved.testID ?? match.testID ?? null,
1606
+ testID: resolved.testID ?? match.testID ?? null,
1607
+ text: resolved.text ?? match.text ?? null,
1608
+ type: resolved.type ?? match.type ?? null
1609
+ },
1610
+ strategy: "matched-node",
1611
+ ...extra
1612
+ };
1757
1613
  }
1758
- function formatTimelineSummary(summary) {
1759
- if (summary.total === 0) return "nothing recorded";
1760
- const parts = [];
1761
- const ORDER = [
1762
- "error",
1763
- "warning",
1764
- "console",
1765
- "fetch",
1766
- "toast",
1767
- "alert",
1768
- "actionsheet",
1769
- "picker",
1770
- "notification",
1771
- "screen",
1772
- "route",
1773
- "keyboard",
1774
- "app-launch",
1775
- "shell",
1776
- "scroll",
1777
- "gesture",
1778
- "text-input",
1779
- "react-commit",
1780
- "animation",
1781
- "reanimated"
1782
- ];
1783
- const seen = /* @__PURE__ */ new Set();
1784
- for (const k of ORDER) {
1785
- const n = summary.byKind[k];
1786
- if (n) {
1787
- parts.push(`${n} ${k}${n === 1 ? "" : "s"}`);
1788
- seen.add(k);
1614
+ function collectTextCandidates(nodes, ancestors = []) {
1615
+ const out = [];
1616
+ for (const node of nodes) {
1617
+ out.push({ node, ancestorTestIDs: ancestors });
1618
+ if (node.children?.length) {
1619
+ const next = node.testID ? [...ancestors, node.testID] : ancestors;
1620
+ out.push(...collectTextCandidates(node.children, next));
1789
1621
  }
1790
1622
  }
1791
- for (const [k, n] of Object.entries(summary.byKind)) {
1792
- if (!seen.has(k) && n) parts.push(`${n} ${k}${n === 1 ? "" : "s"}`);
1793
- }
1794
- return parts.join(" \xB7 ");
1623
+ return out;
1795
1624
  }
1796
- var NOISY_TIMELINE_KINDS = /* @__PURE__ */ new Set([
1797
- "react-commit",
1798
- "layout",
1799
- "scroll"
1800
- ]);
1801
- function formatTimelineRelativeTime(t, anchor) {
1802
- if (anchor === null) return new Date(t).toLocaleTimeString();
1803
- const dt = (t - anchor) / 1e3;
1804
- const sign = dt >= 0 ? "+" : "";
1805
- return `${sign}${dt.toFixed(2)}s`;
1625
+ function nodeText(node) {
1626
+ return node.text ?? node.label ?? "";
1806
1627
  }
1807
- function formatTimelinePayload(kind, d) {
1808
- switch (kind) {
1809
- case "app-launch":
1810
- return d.phase === "launch" ? `launch ${d.appName ?? d.toAppId ?? ""}` : `dismiss ${d.appName ?? d.fromAppId ?? ""} \u2192 ${d.toAppId ?? ""}`;
1811
- case "toast":
1812
- return `"${d.text ?? ""}"${d.durationMs ? ` (${d.durationMs}ms)` : ""}`;
1813
- case "keyboard":
1814
- return `${d.phase ?? "?"}${d.heightPx ? ` h=${d.heightPx}` : ""}${d.mode ? ` ${d.mode}` : ""}`;
1815
- case "screen":
1816
- return `${d.phase ?? "?"} ${d.name ?? d.activeName ?? ""}`;
1817
- case "flow-step":
1818
- return `${d.status ?? "?"} ${d.stepName ?? "step"}${d.durationMs ? ` ${d.durationMs}ms` : ""}`;
1819
- case "route":
1820
- return `${d.phase ?? "?"} ${d.path ?? d.pathname ?? ""}`;
1821
- case "alert":
1822
- case "actionsheet":
1823
- case "picker":
1824
- return `${d.phase ?? "?"} ${d.title ?? d.message ?? ""}`;
1825
- case "notification":
1826
- return `${d.title ?? ""}${d.body ? ` \u2014 ${d.body}` : ""}`;
1827
- case "fetch":
1828
- return `${d.method ?? "GET"} ${d.url ?? ""}${d.status ? ` -> ${d.status}` : ""}`;
1829
- case "console":
1830
- case "console-log":
1831
- return `${d.level ?? "log"}: ${(d.message ?? "").toString().slice(0, 120)}`;
1832
- case "shell":
1833
- return `${d.event ?? d.type ?? d.phase ?? ""}`;
1834
- case "scroll":
1835
- return `${d.phase ?? "?"} ${d.target ?? ""}`;
1836
- case "gesture":
1837
- return `${d.phase ?? "?"} ${d.type ?? ""}`;
1838
- case "text-input":
1839
- return `${d.phase ?? "?"}${d.value !== void 0 ? ` "${String(d.value).slice(0, 40)}"` : ""}`;
1840
- case "layout":
1841
- return `${d.kind ?? "?"} ${d.testID ?? d.type ?? ""}${d.skipped ? ` skipped:${d.reason ?? "unknown"}` : ""}`;
1842
- case "react-commit": {
1843
- const slowest = d.slowest;
1844
- return `${d.fiberCount ?? "?"} fibers ${d.durationMs ?? "?"}ms${slowest?.displayName ? ` \xB7 ${slowest.displayName} ${slowest.durationMs ?? "?"}ms` : ""}`;
1628
+ async function tapById(bridge, query, opts = {}) {
1629
+ return tapResolvedTarget(bridge, {
1630
+ agent: opts.agent,
1631
+ timing: opts.timing,
1632
+ resolve: async () => {
1633
+ const resolved = await resolveTargetCoords(bridge, { mode: "testid", value: query });
1634
+ if (!resolved) return { cx: null };
1635
+ return payloadFromResolved(resolved, resolved);
1845
1636
  }
1846
- case "frame":
1847
- return `${d.totalMs ?? "?"}ms${d.renderMs ? ` \xB7 render ${d.renderMs}ms` : ""}${d.layoutMs ? ` \xB7 layout ${d.layoutMs}ms` : ""}${d.copyMs ? ` \xB7 copy ${d.copyMs}ms` : ""}`;
1848
- case "reanimated":
1849
- case "animation":
1850
- return `${d.kind ?? ""} ${d.target ?? ""}${d.durationMs ? ` ${d.durationMs}ms` : ""}`;
1851
- }
1852
- return "";
1853
- }
1854
- function formatTimelineEvent(event, anchor) {
1855
- const ts = formatTimelineRelativeTime(event.t, anchor).padStart(8);
1856
- const ctx = event.context.padEnd(6);
1857
- const kind = `[${event.kind}]`.padEnd(15);
1858
- const d = event.data;
1859
- const payload = d && typeof d === "object" ? formatTimelinePayload(event.kind, d) : "";
1860
- return ` ${ts} ${ctx} ${kind} ${payload}`;
1637
+ });
1861
1638
  }
1862
- function formatTimelineEvents(events) {
1863
- if (!events.length) return "";
1864
- const anchor = events[0]?.t ?? null;
1865
- return events.map((e) => formatTimelineEvent(e, anchor)).join("\n");
1639
+ async function tapByText(bridge, query, options = {}, opts = {}) {
1640
+ return tapResolvedTarget(bridge, {
1641
+ agent: opts.agent,
1642
+ timing: opts.timing,
1643
+ textFallback: query,
1644
+ resolve: async () => {
1645
+ const { tree } = await inspectTree(bridge, 50);
1646
+ if (!Array.isArray(tree)) return { matched: 0, total: 0 };
1647
+ const needle = options.exact ? query : query.toLowerCase();
1648
+ let candidates = collectTextCandidates(tree).filter(({ node: node2, ancestorTestIDs }) => {
1649
+ const text = nodeText(node2);
1650
+ if (!text) return false;
1651
+ if (options.exact ? text !== needle : !text.toLowerCase().includes(needle)) {
1652
+ return false;
1653
+ }
1654
+ if (options.role && node2.role !== options.role) return false;
1655
+ if (options.within && !ancestorTestIDs.includes(options.within)) return false;
1656
+ const ax = node2.geometry.x;
1657
+ const ay = node2.geometry.y;
1658
+ if (options.minX != null && !(ax >= options.minX)) return false;
1659
+ if (options.maxX != null && !(ax <= options.maxX)) return false;
1660
+ if (options.minY != null && !(ay >= options.minY)) return false;
1661
+ if (options.maxY != null && !(ay <= options.maxY)) return false;
1662
+ return true;
1663
+ });
1664
+ if (options.near) {
1665
+ const { x, y } = options.near;
1666
+ candidates.sort(
1667
+ (a, b) => Math.hypot(a.node.geometry.x - x, a.node.geometry.y - y) - Math.hypot(b.node.geometry.x - x, b.node.geometry.y - y)
1668
+ );
1669
+ } else {
1670
+ candidates.sort((a, b) => {
1671
+ if (Math.abs(a.node.geometry.y - b.node.geometry.y) > 2) {
1672
+ return a.node.geometry.y - b.node.geometry.y;
1673
+ }
1674
+ return a.node.geometry.x - b.node.geometry.x;
1675
+ });
1676
+ }
1677
+ const total = candidates.length;
1678
+ if (total === 0) return { matched: 0, total: 0 };
1679
+ let idx = 0;
1680
+ if (options.nth != null) {
1681
+ idx = options.nth < 0 ? total + options.nth : options.nth;
1682
+ if (idx < 0 || idx >= total) {
1683
+ return { matched: 0, total, nthOutOfRange: true, nth: options.nth };
1684
+ }
1685
+ } else if (total > 1 && !options.first && !options.near) {
1686
+ return {
1687
+ ambiguous: true,
1688
+ total,
1689
+ candidates: candidates.slice(0, 10).map((candidate, index) => ({
1690
+ idx: index,
1691
+ nodeId: candidate.node.nodeId,
1692
+ type: candidate.node.type,
1693
+ testID: candidate.node.testID ?? null,
1694
+ text: nodeText(candidate.node).slice(0, 60),
1695
+ abs: { x: candidate.node.geometry.x, y: candidate.node.geometry.y },
1696
+ layout: {
1697
+ width: Math.round(candidate.node.geometry.width),
1698
+ height: Math.round(candidate.node.geometry.height)
1699
+ },
1700
+ ancestorTestIDs: candidate.ancestorTestIDs.slice(0, 5)
1701
+ }))
1702
+ };
1703
+ }
1704
+ const picked = candidates[idx];
1705
+ if (!picked) return { matched: 0, total };
1706
+ const node = picked.node;
1707
+ let resolved = node.testID ? await resolveTargetCoords(bridge, { mode: "testid", value: node.testID }) : await resolveTargetCoords(bridge, {
1708
+ mode: "text",
1709
+ value: nodeText(node) || query
1710
+ });
1711
+ if (!resolved) {
1712
+ resolved = {
1713
+ x: node.geometry.x + node.geometry.width / 2,
1714
+ y: node.geometry.y + node.geometry.height / 2,
1715
+ testID: node.testID ?? null,
1716
+ text: nodeText(node) || query,
1717
+ type: node.type
1718
+ };
1719
+ }
1720
+ return payloadFromResolved(resolved, node, { total, idx });
1721
+ }
1722
+ });
1866
1723
  }
1867
-
1868
- // cli/commands/inspect/resolve-target.ts
1869
- async function resolveTargetCoords(bridge, target) {
1870
- const result = await bridge.send({
1871
- type: "resolve",
1872
- selector: target.mode === "testid" ? { testID: target.value } : { text: target.value }
1724
+ async function tapBest(bridge, query, opts = {}) {
1725
+ return tapResolvedTarget(bridge, {
1726
+ agent: opts.agent,
1727
+ timing: opts.timing,
1728
+ textFallback: query,
1729
+ resolve: async () => {
1730
+ const byId = await resolveTargetCoords(bridge, { mode: "testid", value: query });
1731
+ if (byId) {
1732
+ return {
1733
+ ...payloadFromResolved(byId, byId),
1734
+ strategy: "testid",
1735
+ node: {
1736
+ nodeId: null,
1737
+ id: byId.testID ?? query,
1738
+ testID: byId.testID ?? query,
1739
+ type: byId.type ?? null,
1740
+ text: byId.text ?? null
1741
+ }
1742
+ };
1743
+ }
1744
+ const byText = await resolveTargetCoords(bridge, { mode: "text", value: query });
1745
+ if (byText) {
1746
+ return {
1747
+ ...payloadFromResolved(byText, byText),
1748
+ strategy: "text",
1749
+ node: {
1750
+ nodeId: null,
1751
+ id: byText.testID ?? null,
1752
+ testID: byText.testID ?? null,
1753
+ type: byText.type ?? null,
1754
+ text: byText.text ?? query
1755
+ }
1756
+ };
1757
+ }
1758
+ return { strategy: "none" };
1759
+ }
1873
1760
  });
1874
- if (result === null) return null;
1875
- if (typeof result !== "object") throw new Error("resolve returned an invalid result");
1876
- const match = Reflect.get(result, "match");
1877
- const resolvedTarget = Reflect.get(result, "target");
1878
- const semanticNodes = [match, resolvedTarget];
1879
- const point = Reflect.get(result, "point");
1880
- const x = point !== null && typeof point === "object" ? Reflect.get(point, "x") : void 0;
1881
- const y = point !== null && typeof point === "object" ? Reflect.get(point, "y") : void 0;
1882
- if (!isSimSemanticTree(semanticNodes) || typeof x !== "number" || !Number.isFinite(x) || typeof y !== "number" || !Number.isFinite(y)) {
1883
- throw new Error("resolve returned an invalid result");
1884
- }
1885
- const semanticMatch = semanticNodes[0];
1886
- if (!semanticMatch) throw new Error("resolve returned an invalid result");
1887
- return {
1888
- x,
1889
- y,
1890
- testID: semanticMatch.testID ?? null,
1891
- text: semanticMatch.text ?? (target.mode === "text" ? target.value : semanticMatch.label),
1892
- type: semanticMatch.type
1893
- };
1894
1761
  }
1895
1762
 
1896
1763
  // src/app-url.ts