rnxsim 0.1.417 → 0.1.419

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/cli/cloud-client.ts +104 -32
  2. package/cli/cloud-dispatch.ts +38 -4
  3. package/cli/commands/inspect/actions.ts +313 -385
  4. package/cli/commands/inspect/core.ts +27 -12
  5. package/cli/commands/inspect/resolve-target.ts +2 -3
  6. package/cli/commands/inspect/settle.ts +10 -0
  7. package/cli/commands/inspect/wait-ready.ts +2 -0
  8. package/cli/commands/inspect.ts +31 -48
  9. package/cli/commands/platform.ts +41 -10
  10. package/cli/outbound-endpoints.ts +1 -1
  11. package/cli/self-invocation.ts +2 -2
  12. package/cli/shell-init.ts +1 -1
  13. package/dist-lib/agent-daemon-client.cjs +1 -1
  14. package/dist-lib/agent-events.cjs +1 -1
  15. package/dist-lib/agent-identity.cjs +1 -1
  16. package/dist-lib/agent-sessions.cjs +1 -1
  17. package/dist-lib/attached-projects.cjs +1 -1
  18. package/dist-lib/auth/shared-session.cjs +1 -1
  19. package/dist-lib/backend-origin.cjs +1 -1
  20. package/dist-lib/beta.cjs +1 -1
  21. package/dist-lib/beta.mjs +1 -1
  22. package/dist-lib/bridge-constants.cjs +1 -1
  23. package/dist-lib/bridge-contract-input.cjs +1 -1
  24. package/dist-lib/bridge-contract-input.mjs +1 -1
  25. package/dist-lib/bridge-contract.cjs +7 -1
  26. package/dist-lib/bridge-contract.mjs +5 -1
  27. package/dist-lib/capture-contract.cjs +1 -1
  28. package/dist-lib/capture-contract.mjs +1 -1
  29. package/dist-lib/cli-constants.cjs +1 -1
  30. package/dist-lib/cloud-contract.cjs +1 -1
  31. package/dist-lib/cloud-contract.mjs +1 -1
  32. package/dist-lib/config.cjs +1 -1
  33. package/dist-lib/detox/index.cjs +1 -1
  34. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  35. package/dist-lib/home-paths.cjs +1 -1
  36. package/dist-lib/host/bridge-host.cjs +1 -1
  37. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  38. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  39. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  40. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  41. package/dist-lib/host/websocket-proxy.cjs +1 -1
  42. package/dist-lib/index.cjs +631 -749
  43. package/dist-lib/jump-to-source-babel.cjs +1 -1
  44. package/dist-lib/menu.cjs +1 -1
  45. package/dist-lib/menu.mjs +1 -1
  46. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  47. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  48. package/dist-lib/metro-production-bundle.cjs +1 -1
  49. package/dist-lib/metro-production-bundle.mjs +1 -1
  50. package/dist-lib/metro.cjs +1 -1
  51. package/dist-lib/profiles.cjs +1 -1
  52. package/dist-lib/public-brand.cjs +1 -1
  53. package/dist-lib/react-native-host-modules.cjs +1 -1
  54. package/dist-lib/react-native-host-modules.mjs +1 -1
  55. package/dist-lib/render-mode.cjs +1 -1
  56. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  57. package/dist-lib/sdk.cjs +1209 -1342
  58. package/dist-lib/sdk.mjs +1209 -1342
  59. package/dist-lib/skills.cjs +63 -2
  60. package/dist-lib/vite.cjs +1 -1
  61. package/package.json +1 -1
  62. package/src/bridge-contract.ts +3 -0
  63. package/src/host/bridge-host.ts +1 -1
  64. package/src/host/plane-host.ts +1 -1
  65. package/src/native-dev-bundle-url.ts +1 -1
  66. package/src/vite-plugin.ts +1 -1
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.417 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.419 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __create = Object.create;
@@ -503,6 +503,13 @@ function isDevBridgeLockfileFresh(lock, now = Date.now()) {
503
503
  }
504
504
  }
505
505
 
506
+ // ../sootsim-engine/src/metro-fingerprint.ts
507
+ var import_metro_production_bundle = require("rnxsim/metro-production-bundle");
508
+ var import_react_native_host_modules = require("rnxsim/react-native-host-modules");
509
+
510
+ // ../sootsim-engine/src/metro-fingerprint-registry-client.ts
511
+ var import_metro_fingerprint_registry = require("rnxsim/metro-fingerprint-registry");
512
+
506
513
  // src/cloud-contract.ts
507
514
  var RNX_CLOUD_MAX_ARTIFACT_BYTES = 20 * 1024 * 1024;
508
515
  var RNX_CLOUD_DESIGN_MAX_STORAGE_BYTES = 1024 * 1024;
@@ -527,6 +534,31 @@ var RNX_CLOUD_COMMAND_TYPES = Object.freeze([
527
534
  ]);
528
535
  var RNX_CLOUD_COMMAND_TYPE_SET = new Set(RNX_CLOUD_COMMAND_TYPES);
529
536
 
537
+ // src/metro-production-bundle.ts
538
+ var RNXSIM_METRO_MODULE_PATHS_PREFIX = "globalThis.__sootsimModulePaths=";
539
+ var RNX_METRO_MODULE_IDENTITY_VERSION = 2;
540
+ var RNXSIM_METRO_SOURCE_MAP_COMMENT = "//# sourceMappingURL=";
541
+ function detachRNXMetroSourceMapUrl(bundleText) {
542
+ const trimmedEnd = bundleText.replace(/\s+$/, "");
543
+ const commentStart = trimmedEnd.lastIndexOf(`
544
+ ${RNXSIM_METRO_SOURCE_MAP_COMMENT}`);
545
+ if (commentStart < 0) return { source: bundleText, sourceMappingUrl: null };
546
+ const url = trimmedEnd.slice(commentStart + 1 + RNXSIM_METRO_SOURCE_MAP_COMMENT.length);
547
+ if (url.includes("\n")) return { source: bundleText, sourceMappingUrl: null };
548
+ return { source: trimmedEnd.slice(0, commentStart), sourceMappingUrl: url };
549
+ }
550
+ function createRNXMetroModuleIdentityFooter(identity) {
551
+ const payload = JSON.stringify(identity).replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029").replace(/<\//g, "<\\/");
552
+ return `
553
+ ${RNXSIM_METRO_MODULE_PATHS_PREFIX}${payload};`;
554
+ }
555
+ function appendRNXMetroModuleIdentityFooter(bundleText, identity) {
556
+ const footer = createRNXMetroModuleIdentityFooter(identity);
557
+ const detached = detachRNXMetroSourceMapUrl(bundleText);
558
+ return detached.sourceMappingUrl === null ? bundleText + footer : `${detached.source}${footer}
559
+ ${RNXSIM_METRO_SOURCE_MAP_COMMENT}${detached.sourceMappingUrl}`;
560
+ }
561
+
530
562
  // src/public-brand.ts
531
563
  var name = "rnx";
532
564
  var rnxPublicBrand = Object.freeze({
@@ -801,665 +833,27 @@ function createBridge(wsPort, opts = {}) {
801
833
  });
802
834
  return result;
803
835
  },
804
- close() {
805
- try {
806
- if (ws.readyState === import_ws.WebSocket.OPEN) {
807
- ws.send(JSON.stringify({ type: "bridge:bye", id: 0 }));
808
- }
809
- } catch {
810
- }
811
- ws.close();
812
- setTimeout(() => {
813
- if (ws.readyState !== import_ws.WebSocket.CLOSED) {
814
- ws.terminate();
815
- }
816
- }, 250).unref();
817
- }
818
- };
819
- }
820
-
821
- // cli/commands/inspect/settling.ts
822
- async function waitForSootsimIdle({
823
- bridge,
824
- simId,
825
- maxMs,
826
- pollMs = 50,
827
- stablePolls = 3,
828
- strict = false
829
- }) {
830
- const result = await bridge.send(
831
- {
832
- type: "evaluate",
833
- simId,
834
- code: `(async () => {
835
- const start = Date.now()
836
- const deadline = start + ${Math.max(0, Math.round(maxMs))}
837
- const pollMs = ${Math.max(1, Math.round(pollMs))}
838
- const requiredStablePolls = ${Math.max(1, Math.round(stablePolls))}
839
- const strict = ${strict ? "true" : "false"}
840
- const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
841
-
842
- // route-aware settle: a tap that pushes/pops a screen is async \u2014 the
843
- // old screen still renders for a moment, then the new one mounts and
844
- // its content loads. a pure layout-hash check reports "idle" on the
845
- // outgoing screen or on the incoming skeleton, so a driver re-taps
846
- // and stacks duplicate navigations. drain any in-flight screen
847
- // transition FIRST, bounded by the overall budget. when no
848
- // transition is happening this returns in ~80ms (startWindowMs), so
849
- // a plain button tap barely pays for it.
850
- try {
851
- const wfst = window.__sootsimTest?.waitForScreenTransitions
852
- if (typeof wfst === 'function') {
853
- const remaining = deadline - Date.now()
854
- if (remaining > 120) {
855
- await wfst({
856
- timeoutMs: Math.min(remaining - 80, 4000),
857
- settleMs: 64,
858
- startWindowMs: 80,
859
- })
860
- }
861
- }
862
- } catch {}
863
-
864
- const readSnapshot = async () => {
865
- let animating = false
866
- let layoutDirty = false
867
- let pendingFetches = 0
868
- let requestTotal = 0
869
- let requestInFlight = 0
870
- let renderStatsAvailable = false
871
- try {
872
- const stats = await window.__sootsimRenderHost?.queryStats?.()
873
- if (stats) {
874
- renderStatsAvailable = true
875
- animating =
876
- stats.hasActiveAnims === true ||
877
- stats.hasActiveNativeAnimations === true ||
878
- stats.hasPendingAnimationFrames === true
879
- layoutDirty =
880
- stats.layoutDirty === true && stats.renderRequested === true
881
- // a freshly-pushed screen showing a skeleton is layout-stable
882
- // but not actually settled \u2014 its data/images are still in
883
- // flight. treat bounded image-loader fetches as not-idle so
884
- // settle waits for real content, capped by the budget.
885
- const pf = stats.memory && stats.memory.imageLoader
886
- ? stats.memory.imageLoader.pendingFetches
887
- : 0
888
- pendingFetches = typeof pf === 'number' ? pf : 0
889
- }
890
- } catch {}
891
- try {
892
- const counts = await window.__sootsimTest?.getRequestCounts?.()
893
- requestTotal = typeof counts?.total === 'number' ? counts.total : 0
894
- requestInFlight = typeof counts?.inFlight === 'number' ? counts.inFlight : 0
895
- } catch {}
896
- const root = window.__sootsimRoot
897
- const nodes = []
898
- if (root) {
899
- const walk = (n) => {
900
- if (n.layout && n.layout.width > 0) {
901
- nodes.push(Math.round(n.layout.x) + ',' + Math.round(n.layout.y) + ',' + Math.round(n.layout.width))
902
- }
903
- for (const c of n.children || []) walk(c)
904
- }
905
- walk(root)
906
- }
907
- return { layout: nodes.join(';'), animating, layoutDirty, pendingFetches, renderStatsAvailable, requestTotal, requestInFlight }
908
- }
909
-
910
- // how long generic background network is allowed to keep us waiting AFTER
911
- // the screen is otherwise visually settled. apps with continuous traffic
912
- // (polling, realtime sockets, a tap that fires 20-50 fetches) never reach
913
- // requestInFlight===0 / a stable requestTotal, so requiring full network
914
- // quiet burns the ENTIRE budget on every command \u2014 the leading idle that
915
- // failed PR-preview recordings for network-heavy apps (3pc fight-pulse).
916
- // layout-stability + the image-loader already prove visible content
917
- // loaded (a data load changes layout; an image load shows in the loader),
918
- // so the generic request counters are a courtesy, not a gate: honor them
919
- // only within this grace once the screen is visually still.
920
- const networkQuietGraceMs = 1200
921
- let lastLayout = ''
922
- let lastRequestTotal = -1
923
- let stable = 0
924
- let visuallyStillSince = 0
925
- while (Date.now() < deadline) {
926
- const snapshot = await readSnapshot()
927
- const strictOk =
928
- !strict ||
929
- (snapshot.renderStatsAvailable && !snapshot.animating && !snapshot.layoutDirty)
930
- // visually settled: no animations, no image content loading, layout
931
- // unchanged since the last poll.
932
- const visuallyStill =
933
- strictOk && snapshot.pendingFetches === 0 && snapshot.layout === lastLayout
934
- // generic network quiet: nothing in flight and no new requests issued.
935
- const networkQuiet =
936
- snapshot.requestInFlight === 0 && snapshot.requestTotal === lastRequestTotal
937
- if (visuallyStill) {
938
- stable++
939
- if (visuallyStillSince === 0) visuallyStillSince = Date.now()
940
- // settle once the screen has held still long enough AND either the
941
- // network is genuinely quiet OR it has refused to quiet within the
942
- // grace (continuous background traffic must not block forever).
943
- if (
944
- stable >= requiredStablePolls &&
945
- (networkQuiet || Date.now() - visuallyStillSince >= networkQuietGraceMs)
946
- ) {
947
- return { settled: true, elapsed: Date.now() - start }
948
- }
949
- } else {
950
- stable = 0
951
- visuallyStillSince = 0
952
- }
953
- lastLayout = snapshot.layout
954
- lastRequestTotal = snapshot.requestTotal
955
- await sleep(pollMs)
956
- }
957
- return { settled: false, elapsed: Date.now() - start }
958
- })()`
959
- },
960
- {
961
- timeoutMs: maxMs + 1e3
962
- }
963
- );
964
- const { elapsed, settled } = result ?? {};
965
- return {
966
- elapsed: typeof elapsed === "number" ? elapsed : maxMs,
967
- settled: settled === true
968
- };
969
- }
970
-
971
- // cli/commands/inspect/actions.ts
972
- var SCREEN_EVAL = `(() => {
973
- const spec = window.SootSim?.state?.engineSnapshot?.windowState?.deviceSpec
974
- return spec && spec.width > 0 && spec.height > 0
975
- ? { width: spec.width, height: spec.height }
976
- : null
977
- })()`;
978
- var DEFAULT_AGENT_TIMING = {
979
- initialWaitMs: 3e3,
980
- deadlineMs: 5e3,
981
- retryWaitMs: 700
982
- };
983
- var DEFAULT_INTERACTIVE_TIMING = {
984
- initialWaitMs: 1200,
985
- deadlineMs: 2500,
986
- retryWaitMs: 700
987
- };
988
- function tapTiming(agent, opts = {}) {
989
- return {
990
- ...agent ? DEFAULT_AGENT_TIMING : DEFAULT_INTERACTIVE_TIMING,
991
- ...opts
992
- };
993
- }
994
- function isTapSuccess(result) {
995
- if (!result || result.hit === false || result.ok === false) return false;
996
- if (result.requestedTargetMatched === false) return false;
997
- if (result.pointerTapHandled === false && !result.keyboardOpened && !result.isTextInput) {
998
- return false;
999
- }
1000
- return true;
1001
- }
1002
- function tapTargetFromPayload(payload, textFallback) {
1003
- return {
1004
- nodeId: payload.target?.nodeId ?? payload.match?.nodeId ?? payload.node?.nodeId ?? null,
1005
- id: payload.target?.id ?? payload.match?.id ?? payload.node?.id ?? null,
1006
- testID: payload.target?.testID ?? payload.match?.testID ?? payload.node?.testID ?? null,
1007
- text: payload.target?.text ?? payload.target?.accessibilityLabel ?? payload.match?.text ?? payload.match?.accessibilityLabel ?? payload.node?.text ?? textFallback ?? null,
1008
- type: payload.target?.type ?? payload.match?.type ?? payload.node?.type ?? null
1009
- };
1010
- }
1011
- async function tapResolvedTarget(bridge, args) {
1012
- const timing = tapTiming(!!args.agent, args.timing);
1013
- let attempts = 0;
1014
- let lastPayload = null;
1015
- let lastResult = null;
1016
- try {
1017
- await waitForSootsimIdle({
1018
- bridge,
1019
- maxMs: timing.initialWaitMs,
1020
- pollMs: 32,
1021
- stablePolls: 2
1022
- });
1023
- } catch {
1024
- }
1025
- const deadline = Date.now() + timing.deadlineMs;
1026
- let lastOffscreenAt = null;
1027
- while (Date.now() <= deadline || attempts === 0) {
1028
- attempts++;
1029
- const payload = await args.resolve();
1030
- lastPayload = payload;
1031
- if (payload?.error === "bridge-not-ready" || payload?.ambiguous || payload?.nthOutOfRange) {
1032
- return { payload, result: null, attempts, failure: "special" };
1033
- }
1034
- if (payload && typeof payload.cx === "number" && typeof payload.cy === "number") {
1035
- if (payload.offscreen) {
1036
- lastResult = {
1037
- hit: false,
1038
- reason: "offscreen",
1039
- x: payload.cx,
1040
- y: payload.cy,
1041
- screen: payload.screen
1042
- };
1043
- const at = `${Math.round(payload.cx)},${Math.round(payload.cy)}`;
1044
- if (lastOffscreenAt === at) {
1045
- return { payload, result: lastResult, attempts, failure: "missed" };
1046
- }
1047
- lastOffscreenAt = at;
1048
- } else {
1049
- lastOffscreenAt = null;
1050
- const requestedTarget = tapTargetFromPayload(payload, args.textFallback);
1051
- const verification = await bridge.send({
1052
- type: "evaluate",
1053
- code: `(async () => {
1054
- const t = window.__sootsimTest
1055
- if (
1056
- !t ||
1057
- typeof t.inspectAt !== 'function' ||
1058
- typeof t.resolveTapTarget !== 'function'
1059
- ) {
1060
- return { error: 'tap-target-inspection-unavailable' }
1061
- }
1062
- const requestedTarget = ${JSON.stringify(requestedTarget)}
1063
- const inspected = await t.inspectAt(${payload.cx}, ${payload.cy})
1064
- const resolved =
1065
- inspected && typeof inspected.nodeId === 'number'
1066
- ? await t.resolveTapTarget(inspected.nodeId)
1067
- : null
1068
- const node = (resolved && resolved.target) || inspected
1069
- const coordinateTarget = node
1070
- ? {
1071
- nodeId: node.nodeId ?? null,
1072
- id: node.id ?? null,
1073
- testID: node.testID ?? null,
1074
- text: node.text ?? node.accessibilityLabel ?? null,
1075
- type: node.type ?? null,
1076
- }
1077
- : null
1078
- const coordinateAncestors = Array.isArray(inspected?.ancestors)
1079
- ? inspected.ancestors
1080
- : []
1081
- const ancestorMatchesRequested = coordinateAncestors.some(
1082
- (ancestor) =>
1083
- (typeof requestedTarget.nodeId === 'number' &&
1084
- ancestor?.nodeId === requestedTarget.nodeId) ||
1085
- (requestedTarget.testID &&
1086
- ancestor?.testID === requestedTarget.testID) ||
1087
- (requestedTarget.id && ancestor?.id === requestedTarget.id),
1088
- )
1089
- const requestedTargetMatched =
1090
- coordinateTarget === null
1091
- ? false
1092
- : typeof requestedTarget.nodeId === 'number' &&
1093
- typeof coordinateTarget.nodeId === 'number'
1094
- ? requestedTarget.nodeId === coordinateTarget.nodeId ||
1095
- ancestorMatchesRequested
1096
- : requestedTarget.testID && coordinateTarget.testID
1097
- ? requestedTarget.testID === coordinateTarget.testID
1098
- || ancestorMatchesRequested
1099
- : requestedTarget.id && coordinateTarget.id
1100
- ? requestedTarget.id === coordinateTarget.id
1101
- || ancestorMatchesRequested
1102
- : ancestorMatchesRequested
1103
- if (!requestedTargetMatched) {
1104
- return {
1105
- requestedTargetMatched,
1106
- requestedTarget,
1107
- coordinateTarget,
1108
- }
1109
- }
1110
- if (typeof t.activatePressAt !== 'function') {
1111
- return { error: 'tap-target-activation-unavailable' }
1112
- }
1113
- const activation = await t.activatePressAt(
1114
- ${payload.cx},
1115
- ${payload.cy},
1116
- requestedTarget.nodeId,
1117
- )
1118
- if (activation?.ok) {
1119
- window.dispatchEvent(
1120
- new CustomEvent('sootsim:agentAction', {
1121
- detail: {
1122
- type: 'tap',
1123
- x: ${payload.cx},
1124
- y: ${payload.cy},
1125
- target: requestedTarget,
1126
- },
1127
- }),
1128
- )
1129
- }
1130
- return {
1131
- activation,
1132
- requestedTargetMatched,
1133
- requestedTarget,
1134
- coordinateTarget,
1135
- }
1136
- })()`
1137
- });
1138
- if (verification?.error) {
1139
- return {
1140
- payload,
1141
- result: {
1142
- hit: false,
1143
- reason: verification.error,
1144
- requestedTarget
1145
- },
1146
- attempts,
1147
- failure: "special"
1148
- };
1149
- }
1150
- if (verification?.requestedTargetMatched === false) {
1151
- return {
1152
- payload,
1153
- result: {
1154
- hit: false,
1155
- reason: "target-covered",
1156
- ...verification
1157
- },
1158
- attempts,
1159
- failure: "missed"
1160
- };
1161
- }
1162
- const dispatched = verification?.activation?.tap ?? verification?.activation;
1163
- const result = dispatched && typeof dispatched === "object" ? { ...dispatched, ...verification?.activation, ...verification } : { hit: !!dispatched, ...verification };
1164
- lastResult = result;
1165
- if (isTapSuccess(result)) return { payload, result, attempts };
1166
- }
1167
- }
1168
- const remaining = deadline - Date.now();
1169
- if (remaining <= 0) break;
1170
- try {
1171
- await waitForSootsimIdle({
1172
- bridge,
1173
- maxMs: Math.min(remaining, timing.retryWaitMs),
1174
- pollMs: 32,
1175
- stablePolls: 2
1176
- });
1177
- } catch {
1178
- await new Promise((resolve2) => setTimeout(resolve2, Math.min(120, remaining)));
1179
- }
1180
- }
1181
- return {
1182
- payload: lastPayload,
1183
- result: lastResult,
1184
- attempts,
1185
- failure: lastPayload && typeof lastPayload.cx === "number" ? "missed" : "not-found"
1186
- };
1187
- }
1188
- async function tapById(bridge, query, opts = {}) {
1189
- const arg = JSON.stringify(query);
1190
- return tapResolvedTarget(bridge, {
1191
- agent: opts.agent,
1192
- timing: opts.timing,
1193
- resolve: () => bridge.send({
1194
- type: "evaluate",
1195
- code: `(async () => {
1196
- const t = window.__sootsimTest
1197
- if (!t) return null
1198
- const n = (await t.findByTestId(${arg})) || (await t.findById(${arg}))
1199
- if (!n || !n.absolutePosition || !n.layout) return { cx: null }
1200
- const resolved =
1201
- typeof n.nodeId === 'number' && typeof t.resolveTapTarget === 'function'
1202
- ? await t.resolveTapTarget(n.nodeId)
1203
- : null
1204
- const target = (resolved && resolved.target) || n
1205
- const cx =
1206
- resolved && typeof resolved.cx === 'number'
1207
- ? resolved.cx
1208
- : n.absolutePosition.x + (n.layout.width || 0) / 2
1209
- const cy =
1210
- resolved && typeof resolved.cy === 'number'
1211
- ? resolved.cy
1212
- : n.absolutePosition.y + (n.layout.height || 0) / 2
1213
- const scr = ${SCREEN_EVAL}
1214
- const offscreen =
1215
- !!scr && (cx < 0 || cy < 0 || cx > scr.width || cy > scr.height)
1216
- return {
1217
- cx,
1218
- cy,
1219
- ...(offscreen ? { offscreen: true, screen: scr } : {}),
1220
- match: {
1221
- nodeId: n.nodeId ?? null,
1222
- id: n.id,
1223
- testID: n.testID,
1224
- text: n.text ?? n.accessibilityLabel ?? null,
1225
- type: n.type,
1226
- },
1227
- target: {
1228
- nodeId: target.nodeId ?? null,
1229
- id: target.id,
1230
- testID: target.testID,
1231
- text:
1232
- target.text ??
1233
- target.accessibilityLabel ??
1234
- n.text ??
1235
- n.accessibilityLabel ??
1236
- null,
1237
- type: target.type,
1238
- },
1239
- strategy: (resolved && resolved.strategy) || 'matched-node',
1240
- }
1241
- })()`
1242
- })
1243
- });
1244
- }
1245
- async function tapByText(bridge, query, options = {}, opts = {}) {
1246
- const filterArg = JSON.stringify({
1247
- query,
1248
- exact: !!options.exact,
1249
- role: options.role ?? null,
1250
- within: options.within ?? null,
1251
- minX: options.minX ?? null,
1252
- maxX: options.maxX ?? null,
1253
- minY: options.minY ?? null,
1254
- maxY: options.maxY ?? null,
1255
- near: options.near ?? null,
1256
- nth: options.nth ?? null,
1257
- first: !!options.first
1258
- });
1259
- return tapResolvedTarget(bridge, {
1260
- agent: opts.agent,
1261
- timing: opts.timing,
1262
- textFallback: query,
1263
- resolve: () => bridge.send({
1264
- type: "evaluate",
1265
- code: `(async () => {
1266
- const t = window.__sootsimTest
1267
- if (!t) return { error: 'bridge-not-ready' }
1268
- const F = ${filterArg}
1269
-
1270
- const res = await t.queryTextCandidates({
1271
- query: F.query,
1272
- exact: !!F.exact,
1273
- ...(F.role ? { role: F.role } : {}),
1274
- })
1275
-
1276
- let candidates = res.candidates || []
1277
-
1278
- candidates = candidates.filter((c) => {
1279
- const ax = c.info.absolutePosition && c.info.absolutePosition.x
1280
- const ay = c.info.absolutePosition && c.info.absolutePosition.y
1281
- if (F.minX !== null && !(ax >= F.minX)) return false
1282
- if (F.maxX !== null && !(ax <= F.maxX)) return false
1283
- if (F.minY !== null && !(ay >= F.minY)) return false
1284
- if (F.maxY !== null && !(ay <= F.maxY)) return false
1285
- if (F.within && !c.ancestorTestIDs.includes(F.within)) return false
1286
- return true
1287
- })
1288
-
1289
- if (F.near) {
1290
- candidates.sort((a, b) => {
1291
- const ax = (a.info.absolutePosition && a.info.absolutePosition.x) || 0
1292
- const ay = (a.info.absolutePosition && a.info.absolutePosition.y) || 0
1293
- const bx = (b.info.absolutePosition && b.info.absolutePosition.x) || 0
1294
- const by = (b.info.absolutePosition && b.info.absolutePosition.y) || 0
1295
- return (
1296
- Math.hypot(ax - F.near.x, ay - F.near.y) -
1297
- Math.hypot(bx - F.near.x, by - F.near.y)
1298
- )
1299
- })
1300
- } else {
1301
- candidates.sort((a, b) => {
1302
- const ay = (a.info.absolutePosition && a.info.absolutePosition.y) || 0
1303
- const by = (b.info.absolutePosition && b.info.absolutePosition.y) || 0
1304
- if (Math.abs(ay - by) > 2) return ay - by
1305
- const ax = (a.info.absolutePosition && a.info.absolutePosition.x) || 0
1306
- const bx = (b.info.absolutePosition && b.info.absolutePosition.x) || 0
1307
- return ax - bx
1308
- })
1309
- }
1310
-
1311
- const total = candidates.length
1312
- if (total === 0) return { matched: 0, total: 0 }
1313
-
1314
- let idx = 0
1315
- if (F.nth !== null) {
1316
- idx = F.nth < 0 ? total + F.nth : F.nth
1317
- if (idx < 0 || idx >= total) {
1318
- return { matched: 0, total, nthOutOfRange: true, nth: F.nth }
1319
- }
1320
- } else if (total > 1 && !F.first && !F.near) {
1321
- return {
1322
- ambiguous: true,
1323
- total,
1324
- candidates: candidates.slice(0, 10).map((c, i) => ({
1325
- idx: i,
1326
- nodeId: c.info.nodeId,
1327
- type: c.info.type,
1328
- testID: c.info.testID,
1329
- text: (c.info.text || '').slice(0, 60),
1330
- abs: c.info.absolutePosition,
1331
- layout: c.info.layout
1332
- ? {
1333
- width: Math.round(c.info.layout.width || 0),
1334
- height: Math.round(c.info.layout.height || 0),
1335
- }
1336
- : null,
1337
- ancestorTestIDs: (c.ancestorTestIDs || []).slice(0, 5),
1338
- })),
1339
- }
1340
- }
1341
-
1342
- const picked = candidates[idx]
1343
- const n = picked.info
1344
- if (!n.absolutePosition || !n.layout) return { matched: 0, total }
1345
-
1346
- const resolved =
1347
- typeof n.nodeId === 'number' &&
1348
- typeof t.resolveTapTarget === 'function'
1349
- ? await t.resolveTapTarget(n.nodeId)
1350
- : null
1351
- const target = (resolved && resolved.target) || n
1352
- const cx =
1353
- resolved && typeof resolved.cx === 'number'
1354
- ? resolved.cx
1355
- : n.absolutePosition.x + (n.layout.width || 0) / 2
1356
- const cy =
1357
- resolved && typeof resolved.cy === 'number'
1358
- ? resolved.cy
1359
- : n.absolutePosition.y + (n.layout.height || 0) / 2
1360
- const scr = ${SCREEN_EVAL}
1361
- const offscreen =
1362
- !!scr && (cx < 0 || cy < 0 || cx > scr.width || cy > scr.height)
1363
- return {
1364
- cx,
1365
- cy,
1366
- ...(offscreen ? { offscreen: true, screen: scr } : {}),
1367
- match: {
1368
- nodeId: n.nodeId ?? null,
1369
- id: n.id,
1370
- testID: n.testID,
1371
- type: n.type,
1372
- },
1373
- target: {
1374
- nodeId: target.nodeId ?? null,
1375
- id: target.id,
1376
- testID: target.testID,
1377
- text:
1378
- target.text ??
1379
- target.accessibilityLabel ??
1380
- n.text ??
1381
- n.accessibilityLabel ??
1382
- null,
1383
- type: target.type,
1384
- },
1385
- strategy: (resolved && resolved.strategy) || 'matched-node',
1386
- total,
1387
- idx,
1388
- }
1389
- })()`
1390
- })
1391
- });
1392
- }
1393
- async function tapBest(bridge, query, opts = {}) {
1394
- const arg = JSON.stringify(query);
1395
- return tapResolvedTarget(bridge, {
1396
- agent: opts.agent,
1397
- timing: opts.timing,
1398
- textFallback: query,
1399
- resolve: async () => {
1400
- const payload = await bridge.send({
1401
- type: "evaluate",
1402
- code: `(async () => {
1403
- const t = window.__sootsimTest
1404
- if (!t) return { error: 'bridge-not-ready' }
1405
- const byTestId =
1406
- (await t.findByTestId(${arg})) || (await t.findById(${arg}))
1407
- if (byTestId && byTestId.absolutePosition && byTestId.layout) {
1408
- return {
1409
- strategy: 'testid',
1410
- node: {
1411
- nodeId: byTestId.nodeId ?? null,
1412
- id: byTestId.id,
1413
- testID: byTestId.testID,
1414
- type: byTestId.type,
1415
- text: byTestId.text,
1416
- absolutePosition: byTestId.absolutePosition,
1417
- layout: byTestId.layout,
1418
- },
1419
- screen: ${SCREEN_EVAL},
1420
- }
1421
- }
1422
- const byText = await t.findByText(${arg})
1423
- if (byText && byText.absolutePosition && byText.layout) {
1424
- return {
1425
- strategy: 'text',
1426
- node: {
1427
- nodeId: byText.nodeId ?? null,
1428
- id: byText.id,
1429
- testID: byText.testID,
1430
- type: byText.type,
1431
- text: byText.text,
1432
- absolutePosition: byText.absolutePosition,
1433
- layout: byText.layout,
1434
- },
1435
- screen: ${SCREEN_EVAL},
1436
- }
1437
- }
1438
- return { strategy: 'none' }
1439
- })()`
1440
- });
1441
- if (!payload || !("node" in payload)) return payload;
1442
- const node = payload.node;
1443
- const cx = node.absolutePosition.x + node.layout.width / 2;
1444
- const cy = node.absolutePosition.y + node.layout.height / 2;
1445
- const screen = payload.screen ?? null;
1446
- const offscreen = !!screen && (cx < 0 || cy < 0 || cx > screen.width || cy > screen.height);
1447
- return {
1448
- ...payload,
1449
- cx,
1450
- cy,
1451
- ...offscreen ? { offscreen: true, screen } : {},
1452
- target: {
1453
- id: node.id,
1454
- testID: node.testID,
1455
- text: payload.strategy === "text" ? query : node.text,
1456
- type: node.type
836
+ close() {
837
+ try {
838
+ if (ws.readyState === import_ws.WebSocket.OPEN) {
839
+ ws.send(JSON.stringify({ type: "bridge:bye", id: 0 }));
1457
840
  }
1458
- };
841
+ } catch {
842
+ }
843
+ ws.close();
844
+ setTimeout(() => {
845
+ if (ws.readyState !== import_ws.WebSocket.CLOSED) {
846
+ ws.terminate();
847
+ }
848
+ }, 250).unref();
1459
849
  }
1460
- });
850
+ };
1461
851
  }
1462
852
 
853
+ // src/bridge-contract.ts
854
+ var PUBLIC_NO_NETWORK_MARKER = "public RNX Cloud simulators have no network";
855
+ var HEADLESS_TENANT_FETCH_HUNG_MARKER = "headless tenant fetch hung";
856
+
1463
857
  // cli/commands/inspect/core.ts
1464
858
  async function inspectTree(bridge, depth = 5) {
1465
859
  const result = await bridge.send({
@@ -1925,9 +1319,13 @@ async function inspectFind(bridge, q) {
1925
1319
  }
1926
1320
  function waitReadyReason(status) {
1927
1321
  if (status.bridgeError) {
1322
+ if (status.bridgeError.includes(HEADLESS_TENANT_FETCH_HUNG_MARKER) || status.bridgeError.includes(PUBLIC_NO_NETWORK_MARKER)) {
1323
+ return status.bridgeError;
1324
+ }
1928
1325
  return `sim unreachable: ${status.bridgeError} \u2014 the target tab is closed or no sim is connected (check \`rnx list\`, reopen with \`rnx open\`)`;
1929
1326
  }
1930
- 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";
1327
+ const waitingOnGuestFetch = status.externalReady === false && /https?:\/\//.test(status.externalStatus);
1328
+ const base = status.externalError ? `guest app errored: ${status.externalError}` : waitingOnGuestFetch ? `waiting on a guest fetch: ${status.externalStatus}` : status.loadingText ? `still showing "${status.loadingText}"` : status.externalReady === false ? "guest app is still loading" : status.flag !== true && status.targets > 0 ? "native content is rendered but the ready signal has not settled" : status.flag !== true ? "guest app has not emitted sootsim:externalAppReady" : status.targets <= 0 ? "ready flag emitted but no visible app content is inspectable yet" : "node tree is still changing";
1931
1329
  return !status.externalError && status.suppressedEntryError ? `${base} (suppressed entry error: ${status.suppressedEntryError})` : base;
1932
1330
  }
1933
1331
  async function inspectWaitReady(bridge, timeoutMs = 2e4, options = {}) {
@@ -2072,87 +1470,598 @@ function isForwardedWorkerLogTwin(a, b) {
2072
1470
  }
2073
1471
  return a.args.every((arg, index) => arg === b.args[index]);
2074
1472
  }
2075
- function filterLogEntries(entries, opts = {}) {
2076
- let out = [];
2077
- for (const entry of entries) {
2078
- if (out.some((candidate) => isForwardedWorkerLogTwin(candidate, entry))) {
2079
- continue;
2080
- }
2081
- out.push(entry);
1473
+ function filterLogEntries(entries, opts = {}) {
1474
+ let out = [];
1475
+ for (const entry of entries) {
1476
+ if (out.some((candidate) => isForwardedWorkerLogTwin(candidate, entry))) {
1477
+ continue;
1478
+ }
1479
+ out.push(entry);
1480
+ }
1481
+ if (!opts.showInternal) {
1482
+ out = out.filter((e) => {
1483
+ const first = e.args[0];
1484
+ return !(typeof first === "string" && /^\[(?:rnx|sootsim)(?:\s[^\]]*)?\]/.test(first));
1485
+ });
1486
+ }
1487
+ if (opts.level) out = out.filter((e) => opts.level.has(e.level));
1488
+ if (opts.filter) {
1489
+ const lf = opts.filter.toLowerCase();
1490
+ out = out.filter((e) => e.args.join(" ").toLowerCase().includes(lf));
1491
+ }
1492
+ return out;
1493
+ }
1494
+ async function inspectKeyboard(bridge) {
1495
+ const result = await bridge.send({
1496
+ type: "evaluate",
1497
+ code: `(() => {
1498
+ const kb = window.__sootsimKeyboard
1499
+ if (!kb || typeof kb.getLayout !== 'function') {
1500
+ return { error: 'keyboard bridge getLayout() not available' }
1501
+ }
1502
+ return kb.getLayout()
1503
+ })()`
1504
+ });
1505
+ return result ?? { error: "keyboard bridge returned no result" };
1506
+ }
1507
+ function isShellCommandUnavailable(error) {
1508
+ const message = error instanceof Error ? error.message : String(error);
1509
+ return message.includes("call target not found: SootSim.bridges.mainShell") || message.includes("test bridge unavailable before app-in-worker boot");
1510
+ }
1511
+ async function getShellState(bridge, readyTimeoutMs = 0) {
1512
+ const deadline = Date.now() + Math.max(0, readyTimeoutMs);
1513
+ while (true) {
1514
+ try {
1515
+ return await bridge.send({
1516
+ type: "call",
1517
+ path: "SootSim.bridges.mainShell.getState",
1518
+ args: []
1519
+ });
1520
+ } catch (error) {
1521
+ if (!isShellCommandUnavailable(error) || Date.now() >= deadline) throw error;
1522
+ await new Promise((r) => setTimeout(r, 50));
1523
+ }
1524
+ }
1525
+ }
1526
+
1527
+ // cli/commands/inspect/resolve-target.ts
1528
+ async function resolveTargetCoords(bridge, target) {
1529
+ const result = await bridge.send({
1530
+ type: "resolve",
1531
+ selector: target.mode === "testid" ? { testID: target.value } : { text: target.value }
1532
+ });
1533
+ if (result === null) return null;
1534
+ if (typeof result !== "object") throw new Error("resolve returned an invalid result");
1535
+ const match = Reflect.get(result, "match");
1536
+ const resolvedTarget = Reflect.get(result, "target");
1537
+ const semanticNodes = [match, resolvedTarget];
1538
+ const point = Reflect.get(result, "point");
1539
+ const x = point !== null && typeof point === "object" ? Reflect.get(point, "x") : void 0;
1540
+ const y = point !== null && typeof point === "object" ? Reflect.get(point, "y") : void 0;
1541
+ if (!isSimSemanticTree(semanticNodes) || typeof x !== "number" || !Number.isFinite(x) || typeof y !== "number" || !Number.isFinite(y)) {
1542
+ throw new Error("resolve returned an invalid result");
1543
+ }
1544
+ const semanticMatch = semanticNodes[0];
1545
+ if (!semanticMatch) throw new Error("resolve returned an invalid result");
1546
+ return {
1547
+ x,
1548
+ y,
1549
+ testID: semanticMatch.testID ?? null,
1550
+ text: semanticMatch.text ?? (target.mode === "text" ? target.value : semanticMatch.label),
1551
+ type: semanticMatch.type
1552
+ };
1553
+ }
1554
+
1555
+ // cli/commands/inspect/settling.ts
1556
+ async function waitForSootsimIdle({
1557
+ bridge,
1558
+ simId,
1559
+ maxMs,
1560
+ pollMs = 50,
1561
+ stablePolls = 3,
1562
+ strict = false
1563
+ }) {
1564
+ const result = await bridge.send(
1565
+ {
1566
+ type: "evaluate",
1567
+ simId,
1568
+ code: `(async () => {
1569
+ const start = Date.now()
1570
+ const deadline = start + ${Math.max(0, Math.round(maxMs))}
1571
+ const pollMs = ${Math.max(1, Math.round(pollMs))}
1572
+ const requiredStablePolls = ${Math.max(1, Math.round(stablePolls))}
1573
+ const strict = ${strict ? "true" : "false"}
1574
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
1575
+
1576
+ // route-aware settle: a tap that pushes/pops a screen is async \u2014 the
1577
+ // old screen still renders for a moment, then the new one mounts and
1578
+ // its content loads. a pure layout-hash check reports "idle" on the
1579
+ // outgoing screen or on the incoming skeleton, so a driver re-taps
1580
+ // and stacks duplicate navigations. drain any in-flight screen
1581
+ // transition FIRST, bounded by the overall budget. when no
1582
+ // transition is happening this returns in ~80ms (startWindowMs), so
1583
+ // a plain button tap barely pays for it.
1584
+ try {
1585
+ const wfst = window.__sootsimTest?.waitForScreenTransitions
1586
+ if (typeof wfst === 'function') {
1587
+ const remaining = deadline - Date.now()
1588
+ if (remaining > 120) {
1589
+ await wfst({
1590
+ timeoutMs: Math.min(remaining - 80, 4000),
1591
+ settleMs: 64,
1592
+ startWindowMs: 80,
1593
+ })
1594
+ }
1595
+ }
1596
+ } catch {}
1597
+
1598
+ const readSnapshot = async () => {
1599
+ let animating = false
1600
+ let layoutDirty = false
1601
+ let pendingFetches = 0
1602
+ let requestTotal = 0
1603
+ let requestInFlight = 0
1604
+ let renderStatsAvailable = false
1605
+ try {
1606
+ const stats = await window.__sootsimRenderHost?.queryStats?.()
1607
+ if (stats) {
1608
+ renderStatsAvailable = true
1609
+ animating =
1610
+ stats.hasActiveAnims === true ||
1611
+ stats.hasActiveNativeAnimations === true ||
1612
+ stats.hasPendingAnimationFrames === true
1613
+ layoutDirty =
1614
+ stats.layoutDirty === true && stats.renderRequested === true
1615
+ // a freshly-pushed screen showing a skeleton is layout-stable
1616
+ // but not actually settled \u2014 its data/images are still in
1617
+ // flight. treat bounded image-loader fetches as not-idle so
1618
+ // settle waits for real content, capped by the budget.
1619
+ const pf = stats.memory && stats.memory.imageLoader
1620
+ ? stats.memory.imageLoader.pendingFetches
1621
+ : 0
1622
+ pendingFetches = typeof pf === 'number' ? pf : 0
1623
+ }
1624
+ } catch {}
1625
+ try {
1626
+ const counts = await window.__sootsimTest?.getRequestCounts?.()
1627
+ requestTotal = typeof counts?.total === 'number' ? counts.total : 0
1628
+ requestInFlight = typeof counts?.inFlight === 'number' ? counts.inFlight : 0
1629
+ } catch {}
1630
+ const root = window.__sootsimRoot
1631
+ const nodes = []
1632
+ if (root) {
1633
+ const walk = (n) => {
1634
+ if (n.layout && n.layout.width > 0) {
1635
+ nodes.push(Math.round(n.layout.x) + ',' + Math.round(n.layout.y) + ',' + Math.round(n.layout.width))
1636
+ }
1637
+ for (const c of n.children || []) walk(c)
1638
+ }
1639
+ walk(root)
1640
+ }
1641
+ return { layout: nodes.join(';'), animating, layoutDirty, pendingFetches, renderStatsAvailable, requestTotal, requestInFlight }
1642
+ }
1643
+
1644
+ // how long generic background network is allowed to keep us waiting AFTER
1645
+ // the screen is otherwise visually settled. apps with continuous traffic
1646
+ // (polling, realtime sockets, a tap that fires 20-50 fetches) never reach
1647
+ // requestInFlight===0 / a stable requestTotal, so requiring full network
1648
+ // quiet burns the ENTIRE budget on every command \u2014 the leading idle that
1649
+ // failed PR-preview recordings for network-heavy apps (3pc fight-pulse).
1650
+ // layout-stability + the image-loader already prove visible content
1651
+ // loaded (a data load changes layout; an image load shows in the loader),
1652
+ // so the generic request counters are a courtesy, not a gate: honor them
1653
+ // only within this grace once the screen is visually still.
1654
+ const networkQuietGraceMs = 1200
1655
+ let lastLayout = ''
1656
+ let lastRequestTotal = -1
1657
+ let stable = 0
1658
+ let visuallyStillSince = 0
1659
+ while (Date.now() < deadline) {
1660
+ const snapshot = await readSnapshot()
1661
+ const strictOk =
1662
+ !strict ||
1663
+ (snapshot.renderStatsAvailable && !snapshot.animating && !snapshot.layoutDirty)
1664
+ // visually settled: no animations, no image content loading, layout
1665
+ // unchanged since the last poll.
1666
+ const visuallyStill =
1667
+ strictOk && snapshot.pendingFetches === 0 && snapshot.layout === lastLayout
1668
+ // generic network quiet: nothing in flight and no new requests issued.
1669
+ const networkQuiet =
1670
+ snapshot.requestInFlight === 0 && snapshot.requestTotal === lastRequestTotal
1671
+ if (visuallyStill) {
1672
+ stable++
1673
+ if (visuallyStillSince === 0) visuallyStillSince = Date.now()
1674
+ // settle once the screen has held still long enough AND either the
1675
+ // network is genuinely quiet OR it has refused to quiet within the
1676
+ // grace (continuous background traffic must not block forever).
1677
+ if (
1678
+ stable >= requiredStablePolls &&
1679
+ (networkQuiet || Date.now() - visuallyStillSince >= networkQuietGraceMs)
1680
+ ) {
1681
+ return { settled: true, elapsed: Date.now() - start }
1682
+ }
1683
+ } else {
1684
+ stable = 0
1685
+ visuallyStillSince = 0
1686
+ }
1687
+ lastLayout = snapshot.layout
1688
+ lastRequestTotal = snapshot.requestTotal
1689
+ await sleep(pollMs)
1690
+ }
1691
+ return { settled: false, elapsed: Date.now() - start }
1692
+ })()`
1693
+ },
1694
+ {
1695
+ timeoutMs: maxMs + 1e3
1696
+ }
1697
+ );
1698
+ const { elapsed, settled } = result ?? {};
1699
+ return {
1700
+ elapsed: typeof elapsed === "number" ? elapsed : maxMs,
1701
+ settled: settled === true
1702
+ };
1703
+ }
1704
+
1705
+ // cli/commands/inspect/actions.ts
1706
+ function isScreenSize(value) {
1707
+ if (value === null || typeof value !== "object") return false;
1708
+ const width = Reflect.get(value, "width");
1709
+ const height = Reflect.get(value, "height");
1710
+ return typeof width === "number" && Number.isFinite(width) && width > 0 && typeof height === "number" && Number.isFinite(height) && height > 0;
1711
+ }
1712
+ function viewportFromCapture(value) {
1713
+ if (value === null || typeof value !== "object") return void 0;
1714
+ const tree = Reflect.get(value, "tree");
1715
+ return tree !== null && typeof tree === "object" ? Reflect.get(tree, "viewport") : void 0;
1716
+ }
1717
+ async function readScreen(bridge) {
1718
+ try {
1719
+ const viewport = viewportFromCapture(
1720
+ await bridge.send({ type: "capture", includeVisual: false })
1721
+ );
1722
+ return isScreenSize(viewport) ? { width: viewport.width, height: viewport.height } : null;
1723
+ } catch {
1724
+ return null;
1725
+ }
1726
+ }
1727
+ function offscreenPayload(cx, cy, screen) {
1728
+ if (!screen || cx >= 0 && cy >= 0 && cx <= screen.width && cy <= screen.height) {
1729
+ return {};
2082
1730
  }
2083
- if (!opts.showInternal) {
2084
- out = out.filter((e) => {
2085
- const first = e.args[0];
2086
- return !(typeof first === "string" && /^\[(?:rnx|sootsim)(?:\s[^\]]*)?\]/.test(first));
2087
- });
1731
+ return { offscreen: true, screen };
1732
+ }
1733
+ function geometryContains(geometry, x, y) {
1734
+ return x >= geometry.x && y >= geometry.y && x <= geometry.x + geometry.width && y <= geometry.y + geometry.height;
1735
+ }
1736
+ function hitTestSemanticTree(nodes, x, y, ancestors = []) {
1737
+ for (let index = nodes.length - 1; index >= 0; index--) {
1738
+ const node = nodes[index];
1739
+ if (!node || !geometryContains(node.geometry, x, y)) continue;
1740
+ const childHit = node.children?.length ? hitTestSemanticTree(node.children, x, y, [...ancestors, node]) : null;
1741
+ return childHit ?? { node, ancestors };
2088
1742
  }
2089
- if (opts.level) out = out.filter((e) => opts.level.has(e.level));
2090
- if (opts.filter) {
2091
- const lf = opts.filter.toLowerCase();
2092
- out = out.filter((e) => e.args.join(" ").toLowerCase().includes(lf));
1743
+ return null;
1744
+ }
1745
+ function nodeMatchesRequested(node, requested) {
1746
+ if (typeof requested.nodeId === "number" && node.nodeId === requested.nodeId) {
1747
+ return true;
2093
1748
  }
2094
- return out;
1749
+ if (requested.testID && node.testID === requested.testID) return true;
1750
+ if (requested.id && node.testID === requested.id) return true;
1751
+ return false;
2095
1752
  }
2096
- async function inspectKeyboard(bridge) {
2097
- const result = await bridge.send({
2098
- type: "evaluate",
2099
- code: `(() => {
2100
- const kb = window.__sootsimKeyboard
2101
- if (!kb || typeof kb.getLayout !== 'function') {
2102
- return { error: 'keyboard bridge getLayout() not available' }
2103
- }
2104
- return kb.getLayout()
2105
- })()`
2106
- });
2107
- return result ?? { error: "keyboard bridge returned no result" };
1753
+ function coveringNodeSummary(node) {
1754
+ return {
1755
+ nodeId: node.nodeId,
1756
+ testID: node.testID ?? null,
1757
+ text: node.text ?? node.label ?? null,
1758
+ type: node.type
1759
+ };
2108
1760
  }
2109
- function isShellCommandUnavailable(error) {
2110
- const message = error instanceof Error ? error.message : String(error);
2111
- return message.includes("call target not found: SootSim.bridges.mainShell") || message.includes("test bridge unavailable before app-in-worker boot");
1761
+ var DEFAULT_AGENT_TIMING = {
1762
+ initialWaitMs: 3e3,
1763
+ deadlineMs: 5e3,
1764
+ retryWaitMs: 700
1765
+ };
1766
+ var DEFAULT_INTERACTIVE_TIMING = {
1767
+ initialWaitMs: 1200,
1768
+ deadlineMs: 2500,
1769
+ retryWaitMs: 700
1770
+ };
1771
+ function tapTiming(agent, opts = {}) {
1772
+ return {
1773
+ ...agent ? DEFAULT_AGENT_TIMING : DEFAULT_INTERACTIVE_TIMING,
1774
+ ...opts
1775
+ };
2112
1776
  }
2113
- async function getShellState(bridge, readyTimeoutMs = 0) {
2114
- const deadline = Date.now() + Math.max(0, readyTimeoutMs);
2115
- while (true) {
1777
+ function isTapSuccess(result) {
1778
+ if (!result || result.hit === false || result.ok === false) return false;
1779
+ if (result.handled === false) return false;
1780
+ if (result.requestedTargetMatched === false) return false;
1781
+ if (result.pointerTapHandled === false && !result.keyboardOpened && !result.isTextInput) {
1782
+ return false;
1783
+ }
1784
+ return true;
1785
+ }
1786
+ function tapTargetFromPayload(payload, textFallback) {
1787
+ return {
1788
+ nodeId: payload.target?.nodeId ?? payload.match?.nodeId ?? payload.node?.nodeId ?? null,
1789
+ id: payload.target?.id ?? payload.match?.id ?? payload.node?.id ?? null,
1790
+ testID: payload.target?.testID ?? payload.match?.testID ?? payload.node?.testID ?? null,
1791
+ text: payload.target?.text ?? payload.target?.accessibilityLabel ?? payload.match?.text ?? payload.match?.accessibilityLabel ?? payload.node?.text ?? textFallback ?? null,
1792
+ type: payload.target?.type ?? payload.match?.type ?? payload.node?.type ?? null
1793
+ };
1794
+ }
1795
+ async function tapResolvedTarget(bridge, args) {
1796
+ const timing = tapTiming(!!args.agent, args.timing);
1797
+ let attempts = 0;
1798
+ let lastPayload = null;
1799
+ let lastResult = null;
1800
+ const canWaitForIdle = bridge.plane !== "cloud" && timing.initialWaitMs > 0;
1801
+ if (canWaitForIdle) {
2116
1802
  try {
2117
- return await bridge.send({
2118
- type: "call",
2119
- path: "SootSim.bridges.mainShell.getState",
2120
- args: []
1803
+ await waitForSootsimIdle({
1804
+ bridge,
1805
+ maxMs: timing.initialWaitMs,
1806
+ pollMs: 32,
1807
+ stablePolls: 2
2121
1808
  });
2122
- } catch (error) {
2123
- if (!isShellCommandUnavailable(error) || Date.now() >= deadline) throw error;
2124
- await new Promise((r) => setTimeout(r, 50));
1809
+ } catch {
2125
1810
  }
2126
1811
  }
2127
- }
2128
-
2129
- // cli/commands/inspect/resolve-target.ts
2130
- async function resolveTargetCoords(bridge, target) {
2131
- const result = await bridge.send({
2132
- type: "resolve",
2133
- selector: target.mode === "testid" ? { testID: target.value } : { text: target.value }
2134
- });
2135
- if (result === null) return null;
2136
- if (typeof result !== "object") throw new Error("resolve returned an invalid result");
2137
- const match = Reflect.get(result, "match");
2138
- const resolvedTarget = Reflect.get(result, "target");
2139
- const semanticNodes = [match, resolvedTarget];
2140
- const point = Reflect.get(result, "point");
2141
- const x = point !== null && typeof point === "object" ? Reflect.get(point, "x") : void 0;
2142
- const y = point !== null && typeof point === "object" ? Reflect.get(point, "y") : void 0;
2143
- if (!isSimSemanticTree(semanticNodes) || typeof x !== "number" || !Number.isFinite(x) || typeof y !== "number" || !Number.isFinite(y)) {
2144
- throw new Error("resolve returned an invalid result");
1812
+ const deadline = Date.now() + timing.deadlineMs;
1813
+ let lastOffscreenAt = null;
1814
+ const screen = await readScreen(bridge);
1815
+ while (Date.now() <= deadline || attempts === 0) {
1816
+ attempts++;
1817
+ const resolved = await args.resolve();
1818
+ const payload = resolved && typeof resolved.cx === "number" && typeof resolved.cy === "number" ? { ...resolved, ...offscreenPayload(resolved.cx, resolved.cy, screen) } : resolved;
1819
+ lastPayload = payload;
1820
+ if (payload?.error === "bridge-not-ready" || payload?.ambiguous || payload?.nthOutOfRange) {
1821
+ return { payload, result: null, attempts, failure: "special" };
1822
+ }
1823
+ if (payload && typeof payload.cx === "number" && typeof payload.cy === "number") {
1824
+ if (payload.offscreen) {
1825
+ lastResult = {
1826
+ hit: false,
1827
+ reason: "offscreen",
1828
+ x: payload.cx,
1829
+ y: payload.cy,
1830
+ screen: payload.screen
1831
+ };
1832
+ const at = `${Math.round(payload.cx)},${Math.round(payload.cy)}`;
1833
+ if (lastOffscreenAt === at) {
1834
+ return { payload, result: lastResult, attempts, failure: "missed" };
1835
+ }
1836
+ lastOffscreenAt = at;
1837
+ } else {
1838
+ lastOffscreenAt = null;
1839
+ const requestedTarget = tapTargetFromPayload(payload, args.textFallback);
1840
+ const { tree } = await inspectTree(bridge, 50);
1841
+ if (Array.isArray(tree) && tree.length > 0) {
1842
+ const hit = hitTestSemanticTree(tree, payload.cx, payload.cy);
1843
+ const matched = hit !== null && (nodeMatchesRequested(hit.node, requestedTarget) || hit.ancestors.some(
1844
+ (ancestor) => nodeMatchesRequested(ancestor, requestedTarget)
1845
+ ));
1846
+ if (hit && !matched) {
1847
+ lastResult = {
1848
+ hit: false,
1849
+ reason: "target-covered",
1850
+ requestedTarget,
1851
+ coordinateTarget: coveringNodeSummary(hit.node)
1852
+ };
1853
+ return { payload, result: lastResult, attempts, failure: "missed" };
1854
+ }
1855
+ }
1856
+ const result = await tapCoordinates(
1857
+ bridge,
1858
+ payload.cx,
1859
+ payload.cy,
1860
+ requestedTarget
1861
+ );
1862
+ lastResult = result;
1863
+ if (isTapSuccess(result)) return { payload, result, attempts };
1864
+ }
1865
+ }
1866
+ const remaining = deadline - Date.now();
1867
+ if (remaining <= 0) break;
1868
+ if (bridge.plane === "cloud" || timing.retryWaitMs <= 0) break;
1869
+ try {
1870
+ await waitForSootsimIdle({
1871
+ bridge,
1872
+ maxMs: Math.min(remaining, timing.retryWaitMs),
1873
+ pollMs: 32,
1874
+ stablePolls: 2
1875
+ });
1876
+ } catch {
1877
+ await new Promise((resolve2) => setTimeout(resolve2, Math.min(120, remaining)));
1878
+ }
2145
1879
  }
2146
- const semanticMatch = semanticNodes[0];
2147
- if (!semanticMatch) throw new Error("resolve returned an invalid result");
2148
1880
  return {
1881
+ payload: lastPayload,
1882
+ result: lastResult,
1883
+ attempts,
1884
+ failure: lastPayload && typeof lastPayload.cx === "number" ? "missed" : "not-found"
1885
+ };
1886
+ }
1887
+ async function tapCoordinates(bridge, x, y, target) {
1888
+ return bridge.send({
1889
+ type: "tap",
2149
1890
  x,
2150
1891
  y,
2151
- testID: semanticMatch.testID ?? null,
2152
- text: semanticMatch.text ?? (target.mode === "text" ? target.value : semanticMatch.label),
2153
- type: semanticMatch.type
1892
+ ...target ? { target } : {}
1893
+ });
1894
+ }
1895
+ function payloadFromResolved(resolved, match, extra = {}) {
1896
+ return {
1897
+ cx: resolved.x,
1898
+ cy: resolved.y,
1899
+ match: {
1900
+ nodeId: match.nodeId ?? null,
1901
+ id: match.testID ?? null,
1902
+ testID: match.testID ?? null,
1903
+ text: match.text ?? null,
1904
+ type: match.type ?? null
1905
+ },
1906
+ target: {
1907
+ nodeId: match.nodeId ?? null,
1908
+ id: resolved.testID ?? match.testID ?? null,
1909
+ testID: resolved.testID ?? match.testID ?? null,
1910
+ text: resolved.text ?? match.text ?? null,
1911
+ type: resolved.type ?? match.type ?? null
1912
+ },
1913
+ strategy: "matched-node",
1914
+ ...extra
2154
1915
  };
2155
1916
  }
1917
+ function collectTextCandidates(nodes, ancestors = []) {
1918
+ const out = [];
1919
+ for (const node of nodes) {
1920
+ out.push({ node, ancestorTestIDs: ancestors });
1921
+ if (node.children?.length) {
1922
+ const next = node.testID ? [...ancestors, node.testID] : ancestors;
1923
+ out.push(...collectTextCandidates(node.children, next));
1924
+ }
1925
+ }
1926
+ return out;
1927
+ }
1928
+ function nodeText(node) {
1929
+ return node.text ?? node.label ?? "";
1930
+ }
1931
+ async function tapById(bridge, query, opts = {}) {
1932
+ return tapResolvedTarget(bridge, {
1933
+ agent: opts.agent,
1934
+ timing: opts.timing,
1935
+ resolve: async () => {
1936
+ const resolved = await resolveTargetCoords(bridge, { mode: "testid", value: query });
1937
+ if (!resolved) return { cx: null };
1938
+ return payloadFromResolved(resolved, resolved);
1939
+ }
1940
+ });
1941
+ }
1942
+ async function tapByText(bridge, query, options = {}, opts = {}) {
1943
+ return tapResolvedTarget(bridge, {
1944
+ agent: opts.agent,
1945
+ timing: opts.timing,
1946
+ textFallback: query,
1947
+ resolve: async () => {
1948
+ const { tree } = await inspectTree(bridge, 50);
1949
+ if (!Array.isArray(tree)) return { matched: 0, total: 0 };
1950
+ const needle = options.exact ? query : query.toLowerCase();
1951
+ let candidates = collectTextCandidates(tree).filter(({ node: node2, ancestorTestIDs }) => {
1952
+ const text = nodeText(node2);
1953
+ if (!text) return false;
1954
+ if (options.exact ? text !== needle : !text.toLowerCase().includes(needle)) {
1955
+ return false;
1956
+ }
1957
+ if (options.role && node2.role !== options.role) return false;
1958
+ if (options.within && !ancestorTestIDs.includes(options.within)) return false;
1959
+ const ax = node2.geometry.x;
1960
+ const ay = node2.geometry.y;
1961
+ if (options.minX != null && !(ax >= options.minX)) return false;
1962
+ if (options.maxX != null && !(ax <= options.maxX)) return false;
1963
+ if (options.minY != null && !(ay >= options.minY)) return false;
1964
+ if (options.maxY != null && !(ay <= options.maxY)) return false;
1965
+ return true;
1966
+ });
1967
+ if (options.near) {
1968
+ const { x, y } = options.near;
1969
+ candidates.sort(
1970
+ (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)
1971
+ );
1972
+ } else {
1973
+ candidates.sort((a, b) => {
1974
+ if (Math.abs(a.node.geometry.y - b.node.geometry.y) > 2) {
1975
+ return a.node.geometry.y - b.node.geometry.y;
1976
+ }
1977
+ return a.node.geometry.x - b.node.geometry.x;
1978
+ });
1979
+ }
1980
+ const total = candidates.length;
1981
+ if (total === 0) return { matched: 0, total: 0 };
1982
+ let idx = 0;
1983
+ if (options.nth != null) {
1984
+ idx = options.nth < 0 ? total + options.nth : options.nth;
1985
+ if (idx < 0 || idx >= total) {
1986
+ return { matched: 0, total, nthOutOfRange: true, nth: options.nth };
1987
+ }
1988
+ } else if (total > 1 && !options.first && !options.near) {
1989
+ return {
1990
+ ambiguous: true,
1991
+ total,
1992
+ candidates: candidates.slice(0, 10).map((candidate, index) => ({
1993
+ idx: index,
1994
+ nodeId: candidate.node.nodeId,
1995
+ type: candidate.node.type,
1996
+ testID: candidate.node.testID ?? null,
1997
+ text: nodeText(candidate.node).slice(0, 60),
1998
+ abs: { x: candidate.node.geometry.x, y: candidate.node.geometry.y },
1999
+ layout: {
2000
+ width: Math.round(candidate.node.geometry.width),
2001
+ height: Math.round(candidate.node.geometry.height)
2002
+ },
2003
+ ancestorTestIDs: candidate.ancestorTestIDs.slice(0, 5)
2004
+ }))
2005
+ };
2006
+ }
2007
+ const picked = candidates[idx];
2008
+ if (!picked) return { matched: 0, total };
2009
+ const node = picked.node;
2010
+ let resolved = node.testID ? await resolveTargetCoords(bridge, { mode: "testid", value: node.testID }) : await resolveTargetCoords(bridge, {
2011
+ mode: "text",
2012
+ value: nodeText(node) || query
2013
+ });
2014
+ if (!resolved) {
2015
+ resolved = {
2016
+ x: node.geometry.x + node.geometry.width / 2,
2017
+ y: node.geometry.y + node.geometry.height / 2,
2018
+ testID: node.testID ?? null,
2019
+ text: nodeText(node) || query,
2020
+ type: node.type
2021
+ };
2022
+ }
2023
+ return payloadFromResolved(resolved, node, { total, idx });
2024
+ }
2025
+ });
2026
+ }
2027
+ async function tapBest(bridge, query, opts = {}) {
2028
+ return tapResolvedTarget(bridge, {
2029
+ agent: opts.agent,
2030
+ timing: opts.timing,
2031
+ textFallback: query,
2032
+ resolve: async () => {
2033
+ const byId = await resolveTargetCoords(bridge, { mode: "testid", value: query });
2034
+ if (byId) {
2035
+ return {
2036
+ ...payloadFromResolved(byId, byId),
2037
+ strategy: "testid",
2038
+ node: {
2039
+ nodeId: null,
2040
+ id: byId.testID ?? query,
2041
+ testID: byId.testID ?? query,
2042
+ type: byId.type ?? null,
2043
+ text: byId.text ?? null
2044
+ }
2045
+ };
2046
+ }
2047
+ const byText = await resolveTargetCoords(bridge, { mode: "text", value: query });
2048
+ if (byText) {
2049
+ return {
2050
+ ...payloadFromResolved(byText, byText),
2051
+ strategy: "text",
2052
+ node: {
2053
+ nodeId: null,
2054
+ id: byText.testID ?? null,
2055
+ testID: byText.testID ?? null,
2056
+ type: byText.type ?? null,
2057
+ text: byText.text ?? query
2058
+ }
2059
+ };
2060
+ }
2061
+ return { strategy: "none" };
2062
+ }
2063
+ });
2064
+ }
2156
2065
 
2157
2066
  // src/app-url.ts
2158
2067
  var APP_URL_SCHEME_RE = /^[A-Za-z][A-Za-z0-9+.-]*:/;
@@ -3498,33 +3407,6 @@ var import_fs5 = __toESM(require("fs"), 1);
3498
3407
  var import_module2 = require("module");
3499
3408
  var import_node_crypto4 = require("node:crypto");
3500
3409
  var import_path4 = __toESM(require("path"), 1);
3501
-
3502
- // src/metro-production-bundle.ts
3503
- var RNXSIM_METRO_MODULE_PATHS_PREFIX = "globalThis.__sootsimModulePaths=";
3504
- var RNX_METRO_MODULE_IDENTITY_VERSION = 2;
3505
- var RNXSIM_METRO_SOURCE_MAP_COMMENT = "//# sourceMappingURL=";
3506
- function detachRNXMetroSourceMapUrl(bundleText) {
3507
- const trimmedEnd = bundleText.replace(/\s+$/, "");
3508
- const commentStart = trimmedEnd.lastIndexOf(`
3509
- ${RNXSIM_METRO_SOURCE_MAP_COMMENT}`);
3510
- if (commentStart < 0) return { source: bundleText, sourceMappingUrl: null };
3511
- const url = trimmedEnd.slice(commentStart + 1 + RNXSIM_METRO_SOURCE_MAP_COMMENT.length);
3512
- if (url.includes("\n")) return { source: bundleText, sourceMappingUrl: null };
3513
- return { source: trimmedEnd.slice(0, commentStart), sourceMappingUrl: url };
3514
- }
3515
- function createRNXMetroModuleIdentityFooter(identity) {
3516
- const payload = JSON.stringify(identity).replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029").replace(/<\//g, "<\\/");
3517
- return `
3518
- ${RNXSIM_METRO_MODULE_PATHS_PREFIX}${payload};`;
3519
- }
3520
- function appendRNXMetroModuleIdentityFooter(bundleText, identity) {
3521
- const footer = createRNXMetroModuleIdentityFooter(identity);
3522
- const detached = detachRNXMetroSourceMapUrl(bundleText);
3523
- return detached.sourceMappingUrl === null ? bundleText + footer : `${detached.source}${footer}
3524
- ${RNXSIM_METRO_SOURCE_MAP_COMMENT}${detached.sourceMappingUrl}`;
3525
- }
3526
-
3527
- // src/metro-plugin.ts
3528
3410
  var prefix = "/__soot";
3529
3411
  function readCallableMetroExport(moduleValue, label) {
3530
3412
  const callable = typeof moduleValue === "function" ? moduleValue : typeof moduleValue === "object" && moduleValue !== null ? Reflect.get(moduleValue, "default") : void 0;