rnxsim 0.1.416 → 0.1.418

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