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.
- package/cli/cloud-client.ts +104 -32
- package/cli/cloud-dispatch.ts +38 -4
- package/cli/commands/inspect/actions.ts +313 -385
- package/cli/commands/inspect/core.ts +6 -0
- package/cli/commands/inspect/resolve-target.ts +2 -3
- package/cli/commands/inspect/settle.ts +10 -0
- package/cli/commands/inspect.ts +31 -48
- package/cli/commands/platform.ts +41 -10
- package/cli/outbound-endpoints.ts +1 -1
- package/cli/shell-init.ts +1 -1
- package/dist-lib/agent-daemon-client.cjs +1 -1
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/agent-identity.cjs +1 -1
- package/dist-lib/agent-sessions.cjs +1 -1
- package/dist-lib/attached-projects.cjs +1 -1
- package/dist-lib/auth/shared-session.cjs +1 -1
- package/dist-lib/backend-origin.cjs +1 -1
- package/dist-lib/beta.cjs +1 -1
- package/dist-lib/beta.mjs +1 -1
- package/dist-lib/bridge-constants.cjs +1 -1
- package/dist-lib/bridge-contract-input.cjs +1 -1
- package/dist-lib/bridge-contract-input.mjs +1 -1
- package/dist-lib/bridge-contract.cjs +1 -1
- package/dist-lib/bridge-contract.mjs +1 -1
- package/dist-lib/capture-contract.cjs +1 -1
- package/dist-lib/capture-contract.mjs +1 -1
- package/dist-lib/cli-constants.cjs +1 -1
- package/dist-lib/cloud-contract.cjs +1 -1
- package/dist-lib/cloud-contract.mjs +1 -1
- package/dist-lib/config.cjs +1 -1
- package/dist-lib/detox/index.cjs +1 -1
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +1 -1
- package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
- package/dist-lib/host/replacement-module-handler.cjs +1 -1
- package/dist-lib/host/websocket-proxy.cjs +1 -1
- package/dist-lib/index.cjs +626 -749
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/menu.cjs +1 -1
- package/dist-lib/menu.mjs +1 -1
- package/dist-lib/metro-fingerprint-registry.cjs +1 -1
- package/dist-lib/metro-fingerprint-registry.mjs +1 -1
- package/dist-lib/metro-production-bundle.cjs +1 -1
- package/dist-lib/metro-production-bundle.mjs +1 -1
- package/dist-lib/metro.cjs +1 -1
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/public-brand.cjs +1 -1
- package/dist-lib/react-native-host-modules.cjs +1 -1
- package/dist-lib/react-native-host-modules.mjs +1 -1
- package/dist-lib/render-mode.cjs +1 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
- package/dist-lib/sdk.cjs +1206 -1342
- package/dist-lib/sdk.mjs +1206 -1342
- package/dist-lib/skills.cjs +57 -1
- package/dist-lib/vite.cjs +1 -1
- package/package.json +1 -1
package/dist-lib/index.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
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
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
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
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
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
|
|
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
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
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
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
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
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
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
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
1798
|
+
await waitForSootsimIdle({
|
|
1799
|
+
bridge,
|
|
1800
|
+
maxMs: timing.initialWaitMs,
|
|
1801
|
+
pollMs: 32,
|
|
1802
|
+
stablePolls: 2
|
|
2121
1803
|
});
|
|
2122
|
-
} catch
|
|
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
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
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
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
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;
|