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