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
@@ -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 __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({
@@ -800,664 +832,22 @@ function createBridge(wsPort, opts = {}) {
800
832
  force: claimOpts.force === true
801
833
  });
802
834
  return result;
803
- },
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
835
+ },
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
 
1463
853
  // cli/commands/inspect/core.ts
@@ -1925,6 +1315,9 @@ async function inspectFind(bridge, q) {
1925
1315
  }
1926
1316
  function waitReadyReason(status) {
1927
1317
  if (status.bridgeError) {
1318
+ if (status.bridgeError.includes("headless tenant fetch hung") || status.bridgeError.includes("public RNX Cloud simulators have no network")) {
1319
+ return status.bridgeError;
1320
+ }
1928
1321
  return `sim unreachable: ${status.bridgeError} \u2014 the target tab is closed or no sim is connected (check \`rnx list\`, reopen with \`rnx open\`)`;
1929
1322
  }
1930
1323
  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";
@@ -2072,87 +1465,598 @@ function isForwardedWorkerLogTwin(a, b) {
2072
1465
  }
2073
1466
  return a.args.every((arg, index) => arg === b.args[index]);
2074
1467
  }
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);
1468
+ function filterLogEntries(entries, opts = {}) {
1469
+ let out = [];
1470
+ for (const entry of entries) {
1471
+ if (out.some((candidate) => isForwardedWorkerLogTwin(candidate, entry))) {
1472
+ continue;
1473
+ }
1474
+ out.push(entry);
1475
+ }
1476
+ if (!opts.showInternal) {
1477
+ out = out.filter((e) => {
1478
+ const first = e.args[0];
1479
+ return !(typeof first === "string" && /^\[(?:rnx|sootsim)(?:\s[^\]]*)?\]/.test(first));
1480
+ });
1481
+ }
1482
+ if (opts.level) out = out.filter((e) => opts.level.has(e.level));
1483
+ if (opts.filter) {
1484
+ const lf = opts.filter.toLowerCase();
1485
+ out = out.filter((e) => e.args.join(" ").toLowerCase().includes(lf));
1486
+ }
1487
+ return out;
1488
+ }
1489
+ async function inspectKeyboard(bridge) {
1490
+ const result = await bridge.send({
1491
+ type: "evaluate",
1492
+ code: `(() => {
1493
+ const kb = window.__sootsimKeyboard
1494
+ if (!kb || typeof kb.getLayout !== 'function') {
1495
+ return { error: 'keyboard bridge getLayout() not available' }
1496
+ }
1497
+ return kb.getLayout()
1498
+ })()`
1499
+ });
1500
+ return result ?? { error: "keyboard bridge returned no result" };
1501
+ }
1502
+ function isShellCommandUnavailable(error) {
1503
+ const message = error instanceof Error ? error.message : String(error);
1504
+ return message.includes("call target not found: SootSim.bridges.mainShell") || message.includes("test bridge unavailable before app-in-worker boot");
1505
+ }
1506
+ async function getShellState(bridge, readyTimeoutMs = 0) {
1507
+ const deadline = Date.now() + Math.max(0, readyTimeoutMs);
1508
+ while (true) {
1509
+ try {
1510
+ return await bridge.send({
1511
+ type: "call",
1512
+ path: "SootSim.bridges.mainShell.getState",
1513
+ args: []
1514
+ });
1515
+ } catch (error) {
1516
+ if (!isShellCommandUnavailable(error) || Date.now() >= deadline) throw error;
1517
+ await new Promise((r) => setTimeout(r, 50));
1518
+ }
1519
+ }
1520
+ }
1521
+
1522
+ // cli/commands/inspect/resolve-target.ts
1523
+ async function resolveTargetCoords(bridge, target) {
1524
+ const result = await bridge.send({
1525
+ type: "resolve",
1526
+ selector: target.mode === "testid" ? { testID: target.value } : { text: target.value }
1527
+ });
1528
+ if (result === null) return null;
1529
+ if (typeof result !== "object") throw new Error("resolve returned an invalid result");
1530
+ const match = Reflect.get(result, "match");
1531
+ const resolvedTarget = Reflect.get(result, "target");
1532
+ const semanticNodes = [match, resolvedTarget];
1533
+ const point = Reflect.get(result, "point");
1534
+ const x = point !== null && typeof point === "object" ? Reflect.get(point, "x") : void 0;
1535
+ const y = point !== null && typeof point === "object" ? Reflect.get(point, "y") : void 0;
1536
+ if (!isSimSemanticTree(semanticNodes) || typeof x !== "number" || !Number.isFinite(x) || typeof y !== "number" || !Number.isFinite(y)) {
1537
+ throw new Error("resolve returned an invalid result");
1538
+ }
1539
+ const semanticMatch = semanticNodes[0];
1540
+ if (!semanticMatch) throw new Error("resolve returned an invalid result");
1541
+ return {
1542
+ x,
1543
+ y,
1544
+ testID: semanticMatch.testID ?? null,
1545
+ text: semanticMatch.text ?? (target.mode === "text" ? target.value : semanticMatch.label),
1546
+ type: semanticMatch.type
1547
+ };
1548
+ }
1549
+
1550
+ // cli/commands/inspect/settling.ts
1551
+ async function waitForSootsimIdle({
1552
+ bridge,
1553
+ simId,
1554
+ maxMs,
1555
+ pollMs = 50,
1556
+ stablePolls = 3,
1557
+ strict = false
1558
+ }) {
1559
+ const result = await bridge.send(
1560
+ {
1561
+ type: "evaluate",
1562
+ simId,
1563
+ code: `(async () => {
1564
+ const start = Date.now()
1565
+ const deadline = start + ${Math.max(0, Math.round(maxMs))}
1566
+ const pollMs = ${Math.max(1, Math.round(pollMs))}
1567
+ const requiredStablePolls = ${Math.max(1, Math.round(stablePolls))}
1568
+ const strict = ${strict ? "true" : "false"}
1569
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
1570
+
1571
+ // route-aware settle: a tap that pushes/pops a screen is async \u2014 the
1572
+ // old screen still renders for a moment, then the new one mounts and
1573
+ // its content loads. a pure layout-hash check reports "idle" on the
1574
+ // outgoing screen or on the incoming skeleton, so a driver re-taps
1575
+ // and stacks duplicate navigations. drain any in-flight screen
1576
+ // transition FIRST, bounded by the overall budget. when no
1577
+ // transition is happening this returns in ~80ms (startWindowMs), so
1578
+ // a plain button tap barely pays for it.
1579
+ try {
1580
+ const wfst = window.__sootsimTest?.waitForScreenTransitions
1581
+ if (typeof wfst === 'function') {
1582
+ const remaining = deadline - Date.now()
1583
+ if (remaining > 120) {
1584
+ await wfst({
1585
+ timeoutMs: Math.min(remaining - 80, 4000),
1586
+ settleMs: 64,
1587
+ startWindowMs: 80,
1588
+ })
1589
+ }
1590
+ }
1591
+ } catch {}
1592
+
1593
+ const readSnapshot = async () => {
1594
+ let animating = false
1595
+ let layoutDirty = false
1596
+ let pendingFetches = 0
1597
+ let requestTotal = 0
1598
+ let requestInFlight = 0
1599
+ let renderStatsAvailable = false
1600
+ try {
1601
+ const stats = await window.__sootsimRenderHost?.queryStats?.()
1602
+ if (stats) {
1603
+ renderStatsAvailable = true
1604
+ animating =
1605
+ stats.hasActiveAnims === true ||
1606
+ stats.hasActiveNativeAnimations === true ||
1607
+ stats.hasPendingAnimationFrames === true
1608
+ layoutDirty =
1609
+ stats.layoutDirty === true && stats.renderRequested === true
1610
+ // a freshly-pushed screen showing a skeleton is layout-stable
1611
+ // but not actually settled \u2014 its data/images are still in
1612
+ // flight. treat bounded image-loader fetches as not-idle so
1613
+ // settle waits for real content, capped by the budget.
1614
+ const pf = stats.memory && stats.memory.imageLoader
1615
+ ? stats.memory.imageLoader.pendingFetches
1616
+ : 0
1617
+ pendingFetches = typeof pf === 'number' ? pf : 0
1618
+ }
1619
+ } catch {}
1620
+ try {
1621
+ const counts = await window.__sootsimTest?.getRequestCounts?.()
1622
+ requestTotal = typeof counts?.total === 'number' ? counts.total : 0
1623
+ requestInFlight = typeof counts?.inFlight === 'number' ? counts.inFlight : 0
1624
+ } catch {}
1625
+ const root = window.__sootsimRoot
1626
+ const nodes = []
1627
+ if (root) {
1628
+ const walk = (n) => {
1629
+ if (n.layout && n.layout.width > 0) {
1630
+ nodes.push(Math.round(n.layout.x) + ',' + Math.round(n.layout.y) + ',' + Math.round(n.layout.width))
1631
+ }
1632
+ for (const c of n.children || []) walk(c)
1633
+ }
1634
+ walk(root)
1635
+ }
1636
+ return { layout: nodes.join(';'), animating, layoutDirty, pendingFetches, renderStatsAvailable, requestTotal, requestInFlight }
1637
+ }
1638
+
1639
+ // how long generic background network is allowed to keep us waiting AFTER
1640
+ // the screen is otherwise visually settled. apps with continuous traffic
1641
+ // (polling, realtime sockets, a tap that fires 20-50 fetches) never reach
1642
+ // requestInFlight===0 / a stable requestTotal, so requiring full network
1643
+ // quiet burns the ENTIRE budget on every command \u2014 the leading idle that
1644
+ // failed PR-preview recordings for network-heavy apps (3pc fight-pulse).
1645
+ // layout-stability + the image-loader already prove visible content
1646
+ // loaded (a data load changes layout; an image load shows in the loader),
1647
+ // so the generic request counters are a courtesy, not a gate: honor them
1648
+ // only within this grace once the screen is visually still.
1649
+ const networkQuietGraceMs = 1200
1650
+ let lastLayout = ''
1651
+ let lastRequestTotal = -1
1652
+ let stable = 0
1653
+ let visuallyStillSince = 0
1654
+ while (Date.now() < deadline) {
1655
+ const snapshot = await readSnapshot()
1656
+ const strictOk =
1657
+ !strict ||
1658
+ (snapshot.renderStatsAvailable && !snapshot.animating && !snapshot.layoutDirty)
1659
+ // visually settled: no animations, no image content loading, layout
1660
+ // unchanged since the last poll.
1661
+ const visuallyStill =
1662
+ strictOk && snapshot.pendingFetches === 0 && snapshot.layout === lastLayout
1663
+ // generic network quiet: nothing in flight and no new requests issued.
1664
+ const networkQuiet =
1665
+ snapshot.requestInFlight === 0 && snapshot.requestTotal === lastRequestTotal
1666
+ if (visuallyStill) {
1667
+ stable++
1668
+ if (visuallyStillSince === 0) visuallyStillSince = Date.now()
1669
+ // settle once the screen has held still long enough AND either the
1670
+ // network is genuinely quiet OR it has refused to quiet within the
1671
+ // grace (continuous background traffic must not block forever).
1672
+ if (
1673
+ stable >= requiredStablePolls &&
1674
+ (networkQuiet || Date.now() - visuallyStillSince >= networkQuietGraceMs)
1675
+ ) {
1676
+ return { settled: true, elapsed: Date.now() - start }
1677
+ }
1678
+ } else {
1679
+ stable = 0
1680
+ visuallyStillSince = 0
1681
+ }
1682
+ lastLayout = snapshot.layout
1683
+ lastRequestTotal = snapshot.requestTotal
1684
+ await sleep(pollMs)
1685
+ }
1686
+ return { settled: false, elapsed: Date.now() - start }
1687
+ })()`
1688
+ },
1689
+ {
1690
+ timeoutMs: maxMs + 1e3
1691
+ }
1692
+ );
1693
+ const { elapsed, settled } = result ?? {};
1694
+ return {
1695
+ elapsed: typeof elapsed === "number" ? elapsed : maxMs,
1696
+ settled: settled === true
1697
+ };
1698
+ }
1699
+
1700
+ // cli/commands/inspect/actions.ts
1701
+ function isScreenSize(value) {
1702
+ if (value === null || typeof value !== "object") return false;
1703
+ const width = Reflect.get(value, "width");
1704
+ const height = Reflect.get(value, "height");
1705
+ return typeof width === "number" && Number.isFinite(width) && width > 0 && typeof height === "number" && Number.isFinite(height) && height > 0;
1706
+ }
1707
+ function viewportFromCapture(value) {
1708
+ if (value === null || typeof value !== "object") return void 0;
1709
+ const tree = Reflect.get(value, "tree");
1710
+ return tree !== null && typeof tree === "object" ? Reflect.get(tree, "viewport") : void 0;
1711
+ }
1712
+ async function readScreen(bridge) {
1713
+ try {
1714
+ const viewport = viewportFromCapture(
1715
+ await bridge.send({ type: "capture", includeVisual: false })
1716
+ );
1717
+ return isScreenSize(viewport) ? { width: viewport.width, height: viewport.height } : null;
1718
+ } catch {
1719
+ return null;
1720
+ }
1721
+ }
1722
+ function offscreenPayload(cx, cy, screen) {
1723
+ if (!screen || cx >= 0 && cy >= 0 && cx <= screen.width && cy <= screen.height) {
1724
+ return {};
2082
1725
  }
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
- });
1726
+ return { offscreen: true, screen };
1727
+ }
1728
+ function geometryContains(geometry, x, y) {
1729
+ return x >= geometry.x && y >= geometry.y && x <= geometry.x + geometry.width && y <= geometry.y + geometry.height;
1730
+ }
1731
+ function hitTestSemanticTree(nodes, x, y, ancestors = []) {
1732
+ for (let index = nodes.length - 1; index >= 0; index--) {
1733
+ const node = nodes[index];
1734
+ if (!node || !geometryContains(node.geometry, x, y)) continue;
1735
+ const childHit = node.children?.length ? hitTestSemanticTree(node.children, x, y, [...ancestors, node]) : null;
1736
+ return childHit ?? { node, ancestors };
2088
1737
  }
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));
1738
+ return null;
1739
+ }
1740
+ function nodeMatchesRequested(node, requested) {
1741
+ if (typeof requested.nodeId === "number" && node.nodeId === requested.nodeId) {
1742
+ return true;
2093
1743
  }
2094
- return out;
1744
+ if (requested.testID && node.testID === requested.testID) return true;
1745
+ if (requested.id && node.testID === requested.id) return true;
1746
+ return false;
2095
1747
  }
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" };
1748
+ function coveringNodeSummary(node) {
1749
+ return {
1750
+ nodeId: node.nodeId,
1751
+ testID: node.testID ?? null,
1752
+ text: node.text ?? node.label ?? null,
1753
+ type: node.type
1754
+ };
2108
1755
  }
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");
1756
+ var DEFAULT_AGENT_TIMING = {
1757
+ initialWaitMs: 3e3,
1758
+ deadlineMs: 5e3,
1759
+ retryWaitMs: 700
1760
+ };
1761
+ var DEFAULT_INTERACTIVE_TIMING = {
1762
+ initialWaitMs: 1200,
1763
+ deadlineMs: 2500,
1764
+ retryWaitMs: 700
1765
+ };
1766
+ function tapTiming(agent, opts = {}) {
1767
+ return {
1768
+ ...agent ? DEFAULT_AGENT_TIMING : DEFAULT_INTERACTIVE_TIMING,
1769
+ ...opts
1770
+ };
2112
1771
  }
2113
- async function getShellState(bridge, readyTimeoutMs = 0) {
2114
- const deadline = Date.now() + Math.max(0, readyTimeoutMs);
2115
- while (true) {
1772
+ function isTapSuccess(result) {
1773
+ if (!result || result.hit === false || result.ok === false) return false;
1774
+ if (result.handled === false) return false;
1775
+ if (result.requestedTargetMatched === false) return false;
1776
+ if (result.pointerTapHandled === false && !result.keyboardOpened && !result.isTextInput) {
1777
+ return false;
1778
+ }
1779
+ return true;
1780
+ }
1781
+ function tapTargetFromPayload(payload, textFallback) {
1782
+ return {
1783
+ nodeId: payload.target?.nodeId ?? payload.match?.nodeId ?? payload.node?.nodeId ?? null,
1784
+ id: payload.target?.id ?? payload.match?.id ?? payload.node?.id ?? null,
1785
+ testID: payload.target?.testID ?? payload.match?.testID ?? payload.node?.testID ?? null,
1786
+ text: payload.target?.text ?? payload.target?.accessibilityLabel ?? payload.match?.text ?? payload.match?.accessibilityLabel ?? payload.node?.text ?? textFallback ?? null,
1787
+ type: payload.target?.type ?? payload.match?.type ?? payload.node?.type ?? null
1788
+ };
1789
+ }
1790
+ async function tapResolvedTarget(bridge, args) {
1791
+ const timing = tapTiming(!!args.agent, args.timing);
1792
+ let attempts = 0;
1793
+ let lastPayload = null;
1794
+ let lastResult = null;
1795
+ const canWaitForIdle = bridge.plane !== "cloud" && timing.initialWaitMs > 0;
1796
+ if (canWaitForIdle) {
2116
1797
  try {
2117
- return await bridge.send({
2118
- type: "call",
2119
- path: "SootSim.bridges.mainShell.getState",
2120
- args: []
1798
+ await waitForSootsimIdle({
1799
+ bridge,
1800
+ maxMs: timing.initialWaitMs,
1801
+ pollMs: 32,
1802
+ stablePolls: 2
2121
1803
  });
2122
- } catch (error) {
2123
- if (!isShellCommandUnavailable(error) || Date.now() >= deadline) throw error;
2124
- await new Promise((r) => setTimeout(r, 50));
1804
+ } catch {
2125
1805
  }
2126
1806
  }
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");
1807
+ const deadline = Date.now() + timing.deadlineMs;
1808
+ let lastOffscreenAt = null;
1809
+ const screen = await readScreen(bridge);
1810
+ while (Date.now() <= deadline || attempts === 0) {
1811
+ attempts++;
1812
+ const resolved = await args.resolve();
1813
+ const payload = resolved && typeof resolved.cx === "number" && typeof resolved.cy === "number" ? { ...resolved, ...offscreenPayload(resolved.cx, resolved.cy, screen) } : resolved;
1814
+ lastPayload = payload;
1815
+ if (payload?.error === "bridge-not-ready" || payload?.ambiguous || payload?.nthOutOfRange) {
1816
+ return { payload, result: null, attempts, failure: "special" };
1817
+ }
1818
+ if (payload && typeof payload.cx === "number" && typeof payload.cy === "number") {
1819
+ if (payload.offscreen) {
1820
+ lastResult = {
1821
+ hit: false,
1822
+ reason: "offscreen",
1823
+ x: payload.cx,
1824
+ y: payload.cy,
1825
+ screen: payload.screen
1826
+ };
1827
+ const at = `${Math.round(payload.cx)},${Math.round(payload.cy)}`;
1828
+ if (lastOffscreenAt === at) {
1829
+ return { payload, result: lastResult, attempts, failure: "missed" };
1830
+ }
1831
+ lastOffscreenAt = at;
1832
+ } else {
1833
+ lastOffscreenAt = null;
1834
+ const requestedTarget = tapTargetFromPayload(payload, args.textFallback);
1835
+ const { tree } = await inspectTree(bridge, 50);
1836
+ if (Array.isArray(tree) && tree.length > 0) {
1837
+ const hit = hitTestSemanticTree(tree, payload.cx, payload.cy);
1838
+ const matched = hit !== null && (nodeMatchesRequested(hit.node, requestedTarget) || hit.ancestors.some(
1839
+ (ancestor) => nodeMatchesRequested(ancestor, requestedTarget)
1840
+ ));
1841
+ if (hit && !matched) {
1842
+ lastResult = {
1843
+ hit: false,
1844
+ reason: "target-covered",
1845
+ requestedTarget,
1846
+ coordinateTarget: coveringNodeSummary(hit.node)
1847
+ };
1848
+ return { payload, result: lastResult, attempts, failure: "missed" };
1849
+ }
1850
+ }
1851
+ const result = await tapCoordinates(
1852
+ bridge,
1853
+ payload.cx,
1854
+ payload.cy,
1855
+ requestedTarget
1856
+ );
1857
+ lastResult = result;
1858
+ if (isTapSuccess(result)) return { payload, result, attempts };
1859
+ }
1860
+ }
1861
+ const remaining = deadline - Date.now();
1862
+ if (remaining <= 0) break;
1863
+ if (bridge.plane === "cloud" || timing.retryWaitMs <= 0) break;
1864
+ try {
1865
+ await waitForSootsimIdle({
1866
+ bridge,
1867
+ maxMs: Math.min(remaining, timing.retryWaitMs),
1868
+ pollMs: 32,
1869
+ stablePolls: 2
1870
+ });
1871
+ } catch {
1872
+ await new Promise((resolve2) => setTimeout(resolve2, Math.min(120, remaining)));
1873
+ }
2145
1874
  }
2146
- const semanticMatch = semanticNodes[0];
2147
- if (!semanticMatch) throw new Error("resolve returned an invalid result");
2148
1875
  return {
1876
+ payload: lastPayload,
1877
+ result: lastResult,
1878
+ attempts,
1879
+ failure: lastPayload && typeof lastPayload.cx === "number" ? "missed" : "not-found"
1880
+ };
1881
+ }
1882
+ async function tapCoordinates(bridge, x, y, target) {
1883
+ return bridge.send({
1884
+ type: "tap",
2149
1885
  x,
2150
1886
  y,
2151
- testID: semanticMatch.testID ?? null,
2152
- text: semanticMatch.text ?? (target.mode === "text" ? target.value : semanticMatch.label),
2153
- type: semanticMatch.type
1887
+ ...target ? { target } : {}
1888
+ });
1889
+ }
1890
+ function payloadFromResolved(resolved, match, extra = {}) {
1891
+ return {
1892
+ cx: resolved.x,
1893
+ cy: resolved.y,
1894
+ match: {
1895
+ nodeId: match.nodeId ?? null,
1896
+ id: match.testID ?? null,
1897
+ testID: match.testID ?? null,
1898
+ text: match.text ?? null,
1899
+ type: match.type ?? null
1900
+ },
1901
+ target: {
1902
+ nodeId: match.nodeId ?? null,
1903
+ id: resolved.testID ?? match.testID ?? null,
1904
+ testID: resolved.testID ?? match.testID ?? null,
1905
+ text: resolved.text ?? match.text ?? null,
1906
+ type: resolved.type ?? match.type ?? null
1907
+ },
1908
+ strategy: "matched-node",
1909
+ ...extra
2154
1910
  };
2155
1911
  }
1912
+ function collectTextCandidates(nodes, ancestors = []) {
1913
+ const out = [];
1914
+ for (const node of nodes) {
1915
+ out.push({ node, ancestorTestIDs: ancestors });
1916
+ if (node.children?.length) {
1917
+ const next = node.testID ? [...ancestors, node.testID] : ancestors;
1918
+ out.push(...collectTextCandidates(node.children, next));
1919
+ }
1920
+ }
1921
+ return out;
1922
+ }
1923
+ function nodeText(node) {
1924
+ return node.text ?? node.label ?? "";
1925
+ }
1926
+ async function tapById(bridge, query, opts = {}) {
1927
+ return tapResolvedTarget(bridge, {
1928
+ agent: opts.agent,
1929
+ timing: opts.timing,
1930
+ resolve: async () => {
1931
+ const resolved = await resolveTargetCoords(bridge, { mode: "testid", value: query });
1932
+ if (!resolved) return { cx: null };
1933
+ return payloadFromResolved(resolved, resolved);
1934
+ }
1935
+ });
1936
+ }
1937
+ async function tapByText(bridge, query, options = {}, opts = {}) {
1938
+ return tapResolvedTarget(bridge, {
1939
+ agent: opts.agent,
1940
+ timing: opts.timing,
1941
+ textFallback: query,
1942
+ resolve: async () => {
1943
+ const { tree } = await inspectTree(bridge, 50);
1944
+ if (!Array.isArray(tree)) return { matched: 0, total: 0 };
1945
+ const needle = options.exact ? query : query.toLowerCase();
1946
+ let candidates = collectTextCandidates(tree).filter(({ node: node2, ancestorTestIDs }) => {
1947
+ const text = nodeText(node2);
1948
+ if (!text) return false;
1949
+ if (options.exact ? text !== needle : !text.toLowerCase().includes(needle)) {
1950
+ return false;
1951
+ }
1952
+ if (options.role && node2.role !== options.role) return false;
1953
+ if (options.within && !ancestorTestIDs.includes(options.within)) return false;
1954
+ const ax = node2.geometry.x;
1955
+ const ay = node2.geometry.y;
1956
+ if (options.minX != null && !(ax >= options.minX)) return false;
1957
+ if (options.maxX != null && !(ax <= options.maxX)) return false;
1958
+ if (options.minY != null && !(ay >= options.minY)) return false;
1959
+ if (options.maxY != null && !(ay <= options.maxY)) return false;
1960
+ return true;
1961
+ });
1962
+ if (options.near) {
1963
+ const { x, y } = options.near;
1964
+ candidates.sort(
1965
+ (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)
1966
+ );
1967
+ } else {
1968
+ candidates.sort((a, b) => {
1969
+ if (Math.abs(a.node.geometry.y - b.node.geometry.y) > 2) {
1970
+ return a.node.geometry.y - b.node.geometry.y;
1971
+ }
1972
+ return a.node.geometry.x - b.node.geometry.x;
1973
+ });
1974
+ }
1975
+ const total = candidates.length;
1976
+ if (total === 0) return { matched: 0, total: 0 };
1977
+ let idx = 0;
1978
+ if (options.nth != null) {
1979
+ idx = options.nth < 0 ? total + options.nth : options.nth;
1980
+ if (idx < 0 || idx >= total) {
1981
+ return { matched: 0, total, nthOutOfRange: true, nth: options.nth };
1982
+ }
1983
+ } else if (total > 1 && !options.first && !options.near) {
1984
+ return {
1985
+ ambiguous: true,
1986
+ total,
1987
+ candidates: candidates.slice(0, 10).map((candidate, index) => ({
1988
+ idx: index,
1989
+ nodeId: candidate.node.nodeId,
1990
+ type: candidate.node.type,
1991
+ testID: candidate.node.testID ?? null,
1992
+ text: nodeText(candidate.node).slice(0, 60),
1993
+ abs: { x: candidate.node.geometry.x, y: candidate.node.geometry.y },
1994
+ layout: {
1995
+ width: Math.round(candidate.node.geometry.width),
1996
+ height: Math.round(candidate.node.geometry.height)
1997
+ },
1998
+ ancestorTestIDs: candidate.ancestorTestIDs.slice(0, 5)
1999
+ }))
2000
+ };
2001
+ }
2002
+ const picked = candidates[idx];
2003
+ if (!picked) return { matched: 0, total };
2004
+ const node = picked.node;
2005
+ let resolved = node.testID ? await resolveTargetCoords(bridge, { mode: "testid", value: node.testID }) : await resolveTargetCoords(bridge, {
2006
+ mode: "text",
2007
+ value: nodeText(node) || query
2008
+ });
2009
+ if (!resolved) {
2010
+ resolved = {
2011
+ x: node.geometry.x + node.geometry.width / 2,
2012
+ y: node.geometry.y + node.geometry.height / 2,
2013
+ testID: node.testID ?? null,
2014
+ text: nodeText(node) || query,
2015
+ type: node.type
2016
+ };
2017
+ }
2018
+ return payloadFromResolved(resolved, node, { total, idx });
2019
+ }
2020
+ });
2021
+ }
2022
+ async function tapBest(bridge, query, opts = {}) {
2023
+ return tapResolvedTarget(bridge, {
2024
+ agent: opts.agent,
2025
+ timing: opts.timing,
2026
+ textFallback: query,
2027
+ resolve: async () => {
2028
+ const byId = await resolveTargetCoords(bridge, { mode: "testid", value: query });
2029
+ if (byId) {
2030
+ return {
2031
+ ...payloadFromResolved(byId, byId),
2032
+ strategy: "testid",
2033
+ node: {
2034
+ nodeId: null,
2035
+ id: byId.testID ?? query,
2036
+ testID: byId.testID ?? query,
2037
+ type: byId.type ?? null,
2038
+ text: byId.text ?? null
2039
+ }
2040
+ };
2041
+ }
2042
+ const byText = await resolveTargetCoords(bridge, { mode: "text", value: query });
2043
+ if (byText) {
2044
+ return {
2045
+ ...payloadFromResolved(byText, byText),
2046
+ strategy: "text",
2047
+ node: {
2048
+ nodeId: null,
2049
+ id: byText.testID ?? null,
2050
+ testID: byText.testID ?? null,
2051
+ type: byText.type ?? null,
2052
+ text: byText.text ?? query
2053
+ }
2054
+ };
2055
+ }
2056
+ return { strategy: "none" };
2057
+ }
2058
+ });
2059
+ }
2156
2060
 
2157
2061
  // src/app-url.ts
2158
2062
  var APP_URL_SCHEME_RE = /^[A-Za-z][A-Za-z0-9+.-]*:/;
@@ -3498,33 +3402,6 @@ var import_fs5 = __toESM(require("fs"), 1);
3498
3402
  var import_module2 = require("module");
3499
3403
  var import_node_crypto4 = require("node:crypto");
3500
3404
  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
3405
  var prefix = "/__soot";
3529
3406
  function readCallableMetroExport(moduleValue, label) {
3530
3407
  const callable = typeof moduleValue === "function" ? moduleValue : typeof moduleValue === "object" && moduleValue !== null ? Reflect.get(moduleValue, "default") : void 0;