peachsim 0.1.527
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/LICENSE +22 -0
- package/README.md +640 -0
- package/cli/app-config.ts +82 -0
- package/cli/app-fonts.ts +388 -0
- package/cli/app-project.ts +59 -0
- package/cli/app-splash.ts +171 -0
- package/cli/app-state-reset.ts +26 -0
- package/cli/auth.ts +184 -0
- package/cli/bin.ts +59 -0
- package/cli/bridge-diagnostics.ts +226 -0
- package/cli/bridge-flow-runner.ts +2806 -0
- package/cli/browser-backend.ts +44 -0
- package/cli/browser-evals.ts +96 -0
- package/cli/cli-update.ts +261 -0
- package/cli/cloud-client.ts +1530 -0
- package/cli/cloud-dispatch.ts +206 -0
- package/cli/cloud-session.ts +227 -0
- package/cli/command-contract.ts +221 -0
- package/cli/command-registry.ts +304 -0
- package/cli/commands/agent-wrapper.ts +1048 -0
- package/cli/commands/agent.ts +439 -0
- package/cli/commands/app-fonts.ts +99 -0
- package/cli/commands/assert.ts +542 -0
- package/cli/commands/auth.ts +71 -0
- package/cli/commands/box/bundle-plane.ts +71 -0
- package/cli/commands/box/checkout-plane.ts +256 -0
- package/cli/commands/box/plane-share.ts +208 -0
- package/cli/commands/box/rnx-command.ts +60 -0
- package/cli/commands/box/search-command.ts +157 -0
- package/cli/commands/box.ts +925 -0
- package/cli/commands/camera.ts +266 -0
- package/cli/commands/cleanup.ts +169 -0
- package/cli/commands/compat.ts +176 -0
- package/cli/commands/config.ts +34 -0
- package/cli/commands/control.ts +2154 -0
- package/cli/commands/cpu-profile.ts +941 -0
- package/cli/commands/daemon-mac-app.ts +165 -0
- package/cli/commands/daemon.ts +800 -0
- package/cli/commands/debug.ts +751 -0
- package/cli/commands/desktop.ts +39 -0
- package/cli/commands/detect.ts +201 -0
- package/cli/commands/detox.ts +481 -0
- package/cli/commands/device.ts +134 -0
- package/cli/commands/diagnose.ts +589 -0
- package/cli/commands/do-chain.ts +408 -0
- package/cli/commands/electron.ts +94 -0
- package/cli/commands/film.ts +379 -0
- package/cli/commands/flow.ts +1240 -0
- package/cli/commands/inspect/actions.ts +516 -0
- package/cli/commands/inspect/core.ts +2086 -0
- package/cli/commands/inspect/count.ts +17 -0
- package/cli/commands/inspect/describe.ts +119 -0
- package/cli/commands/inspect/env.ts +25 -0
- package/cli/commands/inspect/find.ts +169 -0
- package/cli/commands/inspect/get-layout.ts +53 -0
- package/cli/commands/inspect/keyboard.ts +54 -0
- package/cli/commands/inspect/list.ts +175 -0
- package/cli/commands/inspect/memory.ts +215 -0
- package/cli/commands/inspect/redaction.ts +39 -0
- package/cli/commands/inspect/resolve-target.ts +77 -0
- package/cli/commands/inspect/screens.ts +78 -0
- package/cli/commands/inspect/settle.ts +32 -0
- package/cli/commands/inspect/settling.ts +197 -0
- package/cli/commands/inspect/shared.ts +259 -0
- package/cli/commands/inspect/sleep.ts +15 -0
- package/cli/commands/inspect/tree.ts +23 -0
- package/cli/commands/inspect/url.ts +17 -0
- package/cli/commands/inspect/wait-event.ts +211 -0
- package/cli/commands/inspect/wait-idle.ts +25 -0
- package/cli/commands/inspect/wait-ready.ts +87 -0
- package/cli/commands/inspect/wait-selector.ts +59 -0
- package/cli/commands/inspect/wait.ts +32 -0
- package/cli/commands/inspect.ts +4738 -0
- package/cli/commands/install-cli.ts +275 -0
- package/cli/commands/install-desktop.ts +385 -0
- package/cli/commands/login.ts +357 -0
- package/cli/commands/logout.ts +24 -0
- package/cli/commands/maestro-generate.ts +375 -0
- package/cli/commands/maestro.ts +536 -0
- package/cli/commands/mode.ts +58 -0
- package/cli/commands/no-bridge-hint.ts +80 -0
- package/cli/commands/perf.ts +67 -0
- package/cli/commands/permissions.ts +203 -0
- package/cli/commands/platform.ts +410 -0
- package/cli/commands/preview.ts +103 -0
- package/cli/commands/profile.ts +106 -0
- package/cli/commands/react.ts +353 -0
- package/cli/commands/record.ts +1435 -0
- package/cli/commands/report-issue.ts +305 -0
- package/cli/commands/reset.ts +89 -0
- package/cli/commands/runtime.ts +348 -0
- package/cli/commands/screenshot-command.ts +106 -0
- package/cli/commands/screenshot-layers.ts +144 -0
- package/cli/commands/screenshot-mode.ts +38 -0
- package/cli/commands/screenshot.ts +492 -0
- package/cli/commands/screenshots-capture.ts +608 -0
- package/cli/commands/screenshots.ts +127 -0
- package/cli/commands/serve.ts +139 -0
- package/cli/commands/setup.ts +416 -0
- package/cli/commands/shell-boolean-mode.ts +82 -0
- package/cli/commands/skills.ts +468 -0
- package/cli/commands/slides.ts +357 -0
- package/cli/commands/state.ts +94 -0
- package/cli/commands/storage.ts +58 -0
- package/cli/commands/telemetry.ts +55 -0
- package/cli/commands/test.ts +535 -0
- package/cli/commands/three-mode.ts +771 -0
- package/cli/commands/timeline.ts +121 -0
- package/cli/commands/upgrade.ts +167 -0
- package/cli/commands/upload.ts +1203 -0
- package/cli/commands/version.ts +63 -0
- package/cli/commands/welcome.ts +17 -0
- package/cli/commands/what-happened.ts +328 -0
- package/cli/current-sim.ts +217 -0
- package/cli/desktop-companion.ts +315 -0
- package/cli/drivers/electron.ts +70 -0
- package/cli/drivers/index.ts +20 -0
- package/cli/drivers/playwright-provisioning.ts +184 -0
- package/cli/drivers/playwright-sim-host.ts +537 -0
- package/cli/drivers/playwright.ts +209 -0
- package/cli/drivers/registry.ts +65 -0
- package/cli/drivers/types.ts +102 -0
- package/cli/flow-export.ts +55 -0
- package/cli/flow-file.ts +142 -0
- package/cli/flow-live-status.ts +126 -0
- package/cli/flow-session.ts +140 -0
- package/cli/help-core.ts +143 -0
- package/cli/help.ts +44 -0
- package/cli/hidden-runtime-alias.ts +19 -0
- package/cli/hints.ts +214 -0
- package/cli/inspect-notice-state.ts +114 -0
- package/cli/internal-child.ts +176 -0
- package/cli/maestro-js.ts +323 -0
- package/cli/main.ts +704 -0
- package/cli/open-url.ts +8 -0
- package/cli/outbound-endpoints.ts +830 -0
- package/cli/parent-pid.ts +204 -0
- package/cli/parse-args.ts +189 -0
- package/cli/privacy.ts +53 -0
- package/cli/prompt.ts +56 -0
- package/cli/recording-access.ts +108 -0
- package/cli/registry.ts +2 -0
- package/cli/resolve-assets.ts +63 -0
- package/cli/rnx-in-process.ts +98 -0
- package/cli/run-registry.ts +226 -0
- package/cli/run-rnx-core.ts +148 -0
- package/cli/run-rnx.ts +137 -0
- package/cli/runtime-notes.ts +66 -0
- package/cli/runtime-summary.ts +25 -0
- package/cli/secure-file.ts +31 -0
- package/cli/self-invocation.ts +58 -0
- package/cli/send-to-box.ts +109 -0
- package/cli/setup-repository.ts +191 -0
- package/cli/shell-init.ts +98 -0
- package/cli/standalone.ts +4 -0
- package/cli/telemetry.ts +84 -0
- package/cli/ui.ts +68 -0
- package/cli/ws-bridge.ts +1020 -0
- package/detox/attempt-events.cjs +23 -0
- package/detox/colors.ts +54 -0
- package/detox/config-loader.ts +135 -0
- package/detox/element-types.ts +48 -0
- package/detox/expectations.ts +534 -0
- package/detox/gestures.ts +501 -0
- package/detox/index.ts +2694 -0
- package/detox/jest-preset.cjs +29 -0
- package/detox/jest-setup-after-env.cjs +22 -0
- package/detox/matchers.ts +30 -0
- package/detox/motion-change.cjs +109 -0
- package/detox/motion-change.d.cts +35 -0
- package/detox/navigation-lifecycle.ts +290 -0
- package/detox/navigation.ts +43 -0
- package/detox/proof-frame.cjs +114 -0
- package/detox/record-block-execution.cjs +78 -0
- package/detox/screenshots/animated-color-test-rest-norngh.png +0 -0
- package/detox/screenshots/color-test-after-drag-norngh.png +0 -0
- package/detox/screenshots/color-test-rest-norngh.png +0 -0
- package/detox/screenshots/theme-blue-toggle.png +0 -0
- package/detox/screenshots/theme-blue.png +0 -0
- package/detox/screenshots/theme-red-toggle.png +0 -0
- package/detox/screenshots/theme-red.png +0 -0
- package/dist-lib/agent-daemon-client.cjs +502 -0
- package/dist-lib/agent-events.cjs +48 -0
- package/dist-lib/agent-identity.cjs +53 -0
- package/dist-lib/agent-sessions.cjs +759 -0
- package/dist-lib/attached-projects.cjs +526 -0
- package/dist-lib/auth/shared-session.cjs +208 -0
- package/dist-lib/backend-origin.cjs +88 -0
- package/dist-lib/beta.cjs +41 -0
- package/dist-lib/beta.mjs +13 -0
- package/dist-lib/bridge-constants.cjs +161 -0
- package/dist-lib/bridge-contract-input.cjs +451 -0
- package/dist-lib/bridge-contract-input.mjs +414 -0
- package/dist-lib/bridge-contract.cjs +130 -0
- package/dist-lib/bridge-contract.mjs +94 -0
- package/dist-lib/capture-contract.cjs +220 -0
- package/dist-lib/capture-contract.mjs +190 -0
- package/dist-lib/cli-constants.cjs +32 -0
- package/dist-lib/cloud-contract.cjs +194 -0
- package/dist-lib/cloud-contract.mjs +145 -0
- package/dist-lib/cloud.cjs +670 -0
- package/dist-lib/cloud.mjs +639 -0
- package/dist-lib/config.cjs +96 -0
- package/dist-lib/detox/index.cjs +3789 -0
- package/dist-lib/detox/jest-preset.cjs +29 -0
- package/dist-lib/dev-bundle-resolution.cjs +290 -0
- package/dist-lib/home-paths.cjs +638 -0
- package/dist-lib/host/bridge-host.cjs +10903 -0
- package/dist-lib/host/fetch-proxy-handler.cjs +421 -0
- package/dist-lib/host/fetch-proxy-overrides.cjs +63 -0
- package/dist-lib/host/fetch-proxy-overrides.mjs +37 -0
- package/dist-lib/host/replacement-module-handler.cjs +469 -0
- package/dist-lib/host/websocket-proxy.cjs +298 -0
- package/dist-lib/index.cjs +4268 -0
- package/dist-lib/jump-to-source-babel.cjs +93 -0
- package/dist-lib/jump-to-source-native.cjs +112 -0
- package/dist-lib/menu.cjs +646 -0
- package/dist-lib/menu.mjs +604 -0
- package/dist-lib/metro-fingerprint-registry.cjs +186 -0
- package/dist-lib/metro-fingerprint-registry.mjs +156 -0
- package/dist-lib/metro-production-bundle.cjs +329 -0
- package/dist-lib/metro-production-bundle.mjs +295 -0
- package/dist-lib/metro.cjs +904 -0
- package/dist-lib/profiles.cjs +246 -0
- package/dist-lib/public-brand.cjs +39 -0
- package/dist-lib/react-native-host-modules.cjs +51 -0
- package/dist-lib/react-native-host-modules.mjs +25 -0
- package/dist-lib/render-mode.cjs +54 -0
- package/dist-lib/scripts/dev-server-scanner.cjs +1232 -0
- package/dist-lib/sdk.cjs +2433 -0
- package/dist-lib/sdk.mjs +2336 -0
- package/dist-lib/skills.cjs +39132 -0
- package/dist-lib/swift-submit.cjs +92 -0
- package/dist-lib/swift-submit.mjs +65 -0
- package/dist-lib/swift.cjs +1646 -0
- package/dist-lib/vite.cjs +1591 -0
- package/package.json +290 -0
- package/scripts/dev-server-scanner.ts +1291 -0
- package/scripts/optional-demo-registry.ts +113 -0
- package/skills/contrast/SKILL.md +165 -0
- package/skills/rnx-debug/SKILL.md +433 -0
- package/skills/rnx-perf/SKILL.md +399 -0
- package/skills/rnx-setup/SKILL.md +172 -0
- package/skills/rnx-test/SKILL.md +321 -0
- package/skills/rnx-visual/SKILL.md +126 -0
- package/src/agent-daemon-client.ts +410 -0
- package/src/agent-events.ts +71 -0
- package/src/agent-identity.ts +23 -0
- package/src/agent-prompt.ts +71 -0
- package/src/agent-sessions.ts +580 -0
- package/src/app-url.ts +39 -0
- package/src/attached-projects.ts +539 -0
- package/src/auth/shared-session.ts +235 -0
- package/src/backend-origin.ts +70 -0
- package/src/beta.ts +19 -0
- package/src/bridge-constants.ts +175 -0
- package/src/bridge-contract-input.ts +502 -0
- package/src/bridge-contract.ts +558 -0
- package/src/browser-cache.ts +16 -0
- package/src/capture-contract.ts +587 -0
- package/src/cli-constants.ts +1 -0
- package/src/cli-version.ts +40 -0
- package/src/cloud-contract.ts +274 -0
- package/src/cloud.ts +762 -0
- package/src/codex-client.ts +234 -0
- package/src/config.ts +132 -0
- package/src/connect.ts +101 -0
- package/src/dev-bundle-resolution.ts +346 -0
- package/src/dev-server-open.ts +198 -0
- package/src/disk-cleanup.ts +540 -0
- package/src/flow-timeout-scale.ts +15 -0
- package/src/home-paths.ts +806 -0
- package/src/host/agent-host.ts +666 -0
- package/src/host/bridge-host.ts +2788 -0
- package/src/host/fetch-proxy-handler.ts +449 -0
- package/src/host/fetch-proxy-overrides.ts +51 -0
- package/src/host/open-url.ts +89 -0
- package/src/host/plane-host.ts +203 -0
- package/src/host/replacement-module-handler.ts +158 -0
- package/src/host/websocket-proxy.ts +315 -0
- package/src/index.ts +71 -0
- package/src/jump-to-source-babel.ts +135 -0
- package/src/jump-to-source-native.ts +102 -0
- package/src/menu.ts +777 -0
- package/src/metro-fingerprint-registry.ts +302 -0
- package/src/metro-plugin.ts +341 -0
- package/src/metro-production-bundle.ts +471 -0
- package/src/native-dev-bundle-url.ts +118 -0
- package/src/native-seam-manifest.ts +341 -0
- package/src/playwright-host-log.ts +16 -0
- package/src/plist.ts +27 -0
- package/src/poll-until.ts +123 -0
- package/src/profiles.ts +179 -0
- package/src/public-brand.ts +9 -0
- package/src/react-native-host-modules.ts +26 -0
- package/src/render-mode.ts +27 -0
- package/src/rolldown-plugin-swift.ts +240 -0
- package/src/runtime-assets.ts +233 -0
- package/src/runtime-delivery.ts +54 -0
- package/src/screenshots/compose.ts +422 -0
- package/src/screenshots/frame-compose.ts +514 -0
- package/src/screenshots/orchestrate.ts +256 -0
- package/src/screenshots/org-deck.ts +91 -0
- package/src/screenshots/registry.ts +58 -0
- package/src/screenshots/schema.ts +364 -0
- package/src/sdk.ts +15 -0
- package/src/settings-bundle.ts +418 -0
- package/src/shell-bridge-identity.ts +15 -0
- package/src/sim-client.ts +697 -0
- package/src/skills/builtin/a11y-review.ts +132 -0
- package/src/skills/builtin/compat-check.ts +71 -0
- package/src/skills/builtin/maestro-test.ts +121 -0
- package/src/skills/builtin/perf-profile.ts +90 -0
- package/src/skills/builtin/screenshot-all.ts +52 -0
- package/src/skills/builtin/visual-diff.ts +100 -0
- package/src/skills/registry.ts +107 -0
- package/src/skills/types.ts +41 -0
- package/src/swift-submit.ts +185 -0
- package/src/swift.ts +7 -0
- package/src/team-machine-prompt.ts +73 -0
- package/src/vite-plugin-one.ts +264 -0
- package/src/vite-plugin-swift.ts +620 -0
- package/src/vite-plugin.ts +1946 -0
- package/src/worklets-babel.ts +250 -0
package/dist-lib/sdk.mjs
ADDED
|
@@ -0,0 +1,2336 @@
|
|
|
1
|
+
/*! rnx v0.1.527 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
4
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
5
|
+
|
|
6
|
+
// src/bridge-contract.ts
|
|
7
|
+
var SEMANTIC_READY_CONTENT_NODE_FLOOR = 100;
|
|
8
|
+
var PUBLIC_NO_NETWORK_MARKER = "public RNX Cloud simulators have no network";
|
|
9
|
+
var HEADLESS_TENANT_FETCH_HUNG_MARKER = "headless tenant fetch hung";
|
|
10
|
+
function readyProbeHasContent(probe) {
|
|
11
|
+
return probe.liveFrameActive || probe.targets > 0 || probe.nodes >= SEMANTIC_READY_CONTENT_NODE_FLOOR;
|
|
12
|
+
}
|
|
13
|
+
function readyProbeHasTargetContent(probe) {
|
|
14
|
+
return probe.liveFrameActive || probe.targets > 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// cli/commands/inspect/core.ts
|
|
18
|
+
function resolveMaxMsFlag(args, fallbackMs) {
|
|
19
|
+
const aliases = ["--max-ms", "--maxMs", "--maxms", "--max_ms"];
|
|
20
|
+
for (const flag of aliases) {
|
|
21
|
+
const i = args.indexOf(flag);
|
|
22
|
+
if (i >= 0 && args[i + 1]) {
|
|
23
|
+
const n = Number(args[i + 1]);
|
|
24
|
+
if (Number.isFinite(n)) return Math.max(100, n);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return fallbackMs;
|
|
28
|
+
}
|
|
29
|
+
var INSPECT_PICK_COMMANDS = /* @__PURE__ */ new Set([
|
|
30
|
+
"tap",
|
|
31
|
+
"double-tap",
|
|
32
|
+
"tap-text",
|
|
33
|
+
"tap-id",
|
|
34
|
+
"long-press",
|
|
35
|
+
"touch"
|
|
36
|
+
]);
|
|
37
|
+
function isInspectPickCommand(subcommand) {
|
|
38
|
+
return typeof subcommand === "string" && INSPECT_PICK_COMMANDS.has(subcommand);
|
|
39
|
+
}
|
|
40
|
+
async function isInspectModeActive(bridge) {
|
|
41
|
+
const active = await bridge.send({
|
|
42
|
+
type: "evaluate",
|
|
43
|
+
code: "window.__sootsimEngineState?.inspectActive === true"
|
|
44
|
+
});
|
|
45
|
+
return active === true;
|
|
46
|
+
}
|
|
47
|
+
async function shouldSkipAutoSettleForInspectPick(bridge, subcommand) {
|
|
48
|
+
if (!isInspectPickCommand(subcommand)) return false;
|
|
49
|
+
try {
|
|
50
|
+
return await isInspectModeActive(bridge);
|
|
51
|
+
} catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async function inspectNodeCount(bridge) {
|
|
56
|
+
const result = await bridge.send({
|
|
57
|
+
type: "query",
|
|
58
|
+
query: { kind: "count" }
|
|
59
|
+
});
|
|
60
|
+
return { nodes: isSemanticQueryResult(result) ? result.total : 0 };
|
|
61
|
+
}
|
|
62
|
+
async function inspectTree(bridge, depth = 5) {
|
|
63
|
+
const result = await bridge.send({
|
|
64
|
+
type: "query",
|
|
65
|
+
query: { kind: "tree", depth }
|
|
66
|
+
});
|
|
67
|
+
const tree = isSemanticQueryResult(result) ? result.nodes : [];
|
|
68
|
+
return { depth, tree };
|
|
69
|
+
}
|
|
70
|
+
async function inspectUrl(bridge) {
|
|
71
|
+
const url = await bridge.send({ type: "evaluate", code: "window.location.href" });
|
|
72
|
+
return { url: typeof url === "string" ? url : "" };
|
|
73
|
+
}
|
|
74
|
+
function isSemanticGeometry(value) {
|
|
75
|
+
return value !== null && typeof value === "object" && [
|
|
76
|
+
Reflect.get(value, "x"),
|
|
77
|
+
Reflect.get(value, "y"),
|
|
78
|
+
Reflect.get(value, "width"),
|
|
79
|
+
Reflect.get(value, "height")
|
|
80
|
+
].every(
|
|
81
|
+
(coordinate) => typeof coordinate === "number" ? Number.isFinite(coordinate) : false
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
function isSimSemanticTree(value) {
|
|
85
|
+
if (!Array.isArray(value)) return false;
|
|
86
|
+
return value.every((node) => {
|
|
87
|
+
if (node === null || typeof node !== "object" || !Number.isInteger(Reflect.get(node, "nodeId")) || typeof Reflect.get(node, "type") !== "string" || !isSemanticGeometry(Reflect.get(node, "geometry"))) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
for (const key of ["testID", "text", "role", "label"]) {
|
|
91
|
+
const value2 = Reflect.get(node, key);
|
|
92
|
+
if (value2 !== void 0 && typeof value2 !== "string") return false;
|
|
93
|
+
}
|
|
94
|
+
const pressable = Reflect.get(node, "pressable");
|
|
95
|
+
if (pressable !== void 0 && pressable !== true) return false;
|
|
96
|
+
const children = Reflect.get(node, "children");
|
|
97
|
+
return children === void 0 || isSimSemanticTree(children);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
function isSemanticQueryResult(value) {
|
|
101
|
+
const total = value !== null && typeof value === "object" ? Reflect.get(value, "total") : void 0;
|
|
102
|
+
return value !== null && typeof value === "object" && typeof total === "number" && Number.isInteger(total) && total >= 0 && isSimSemanticTree(Reflect.get(value, "nodes"));
|
|
103
|
+
}
|
|
104
|
+
function globRegex(glob) {
|
|
105
|
+
const escaped = glob.replace(/[.+^$(){}|[\]\\]/g, "\\$&");
|
|
106
|
+
return new RegExp(`^${escaped.replace(/\*/g, ".*").replace(/\?/g, ".")}$`, "i");
|
|
107
|
+
}
|
|
108
|
+
function formatSemanticTree(nodes, opts = {}) {
|
|
109
|
+
const filter = opts.filter?.toLowerCase() ?? "";
|
|
110
|
+
const testIDRegex = opts.testIdLike ? globRegex(opts.testIdLike) : null;
|
|
111
|
+
const onlyRegex = opts.onlyGlob ? globRegex(opts.onlyGlob) : null;
|
|
112
|
+
const lines = [];
|
|
113
|
+
const visit = (node, indent) => {
|
|
114
|
+
const joined = [node.role, node.label, node.text, node.testID].filter((value) => typeof value === "string").join(" ");
|
|
115
|
+
const matches = (!filter || joined.toLowerCase().includes(filter)) && (!testIDRegex || testIDRegex.test(node.testID ?? "")) && (!onlyRegex || onlyRegex.test(joined));
|
|
116
|
+
if (matches) {
|
|
117
|
+
const parts = [
|
|
118
|
+
node.role ? `[${node.role}]` : node.pressable ? "[tap]" : `<${node.type}>`
|
|
119
|
+
];
|
|
120
|
+
if (node.text) {
|
|
121
|
+
const text = opts.fullText || node.text.length <= 50 ? node.text : `${node.text.slice(0, 49).trimEnd()}\u2026`;
|
|
122
|
+
parts.push(JSON.stringify(text));
|
|
123
|
+
}
|
|
124
|
+
if (node.testID) parts.push(`#${node.testID}`);
|
|
125
|
+
if (node.label && node.label !== node.text) {
|
|
126
|
+
parts.push(`label=${JSON.stringify(node.label)}`);
|
|
127
|
+
}
|
|
128
|
+
if (!opts.hideXy) {
|
|
129
|
+
const geometry = node.geometry;
|
|
130
|
+
parts.push(
|
|
131
|
+
`@(${Math.round(geometry.x)},${Math.round(geometry.y)}) ${Math.round(geometry.width)}x${Math.round(geometry.height)}`
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
if (node.pressable) parts.push("(tap)");
|
|
135
|
+
lines.push(`${" ".repeat(indent + 1)}${parts.join(" ")}`);
|
|
136
|
+
}
|
|
137
|
+
for (const child of node.children ?? []) visit(child, indent + 1);
|
|
138
|
+
};
|
|
139
|
+
let roots = nodes;
|
|
140
|
+
if (opts.subtreeRoot) {
|
|
141
|
+
const pending = [...nodes];
|
|
142
|
+
let found;
|
|
143
|
+
while (pending.length > 0 && !found) {
|
|
144
|
+
const candidate = pending.shift();
|
|
145
|
+
if (!candidate) continue;
|
|
146
|
+
if (candidate.testID === opts.subtreeRoot) found = candidate;
|
|
147
|
+
else pending.push(...candidate.children ?? []);
|
|
148
|
+
}
|
|
149
|
+
if (!found) return `__SUBTREE_NOT_FOUND__:${opts.subtreeRoot}`;
|
|
150
|
+
roots = [found];
|
|
151
|
+
}
|
|
152
|
+
for (const root of roots) visit(root, 0);
|
|
153
|
+
return lines.join("\n");
|
|
154
|
+
}
|
|
155
|
+
async function inspectDescribe(bridge, dumpOpts) {
|
|
156
|
+
const dumpDepth = dumpOpts.maxDepth ?? 12;
|
|
157
|
+
const result = await bridge.send({
|
|
158
|
+
type: "query",
|
|
159
|
+
query: { kind: "tree", depth: dumpDepth }
|
|
160
|
+
});
|
|
161
|
+
if (!isSemanticQueryResult(result)) return {};
|
|
162
|
+
return {
|
|
163
|
+
tree: formatSemanticTree(result.nodes, dumpOpts),
|
|
164
|
+
nodeCount: result.total
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function scoreLayoutTextContrast(input) {
|
|
168
|
+
const required = input.fontSize >= 18 || input.fontSize >= 14 && (input.fontWeight ?? 400) >= 700 ? 3 : 4.5;
|
|
169
|
+
const { normal, background } = input;
|
|
170
|
+
if (normal.width <= 0 || normal.height <= 0 || normal.width !== background.width || normal.height !== background.height) {
|
|
171
|
+
return { ratio: null, required, reason: "capture dimensions do not match" };
|
|
172
|
+
}
|
|
173
|
+
const left = Math.max(0, Math.floor(input.box.x));
|
|
174
|
+
const top = Math.max(0, Math.floor(input.box.y));
|
|
175
|
+
const right = Math.min(normal.width, Math.ceil(input.box.x + input.box.w));
|
|
176
|
+
const bottom = Math.min(normal.height, Math.ceil(input.box.y + input.box.h));
|
|
177
|
+
if (right <= left || bottom <= top) {
|
|
178
|
+
return { ratio: null, required, reason: "text is outside the captured frame" };
|
|
179
|
+
}
|
|
180
|
+
const linear = (value) => {
|
|
181
|
+
const channel = value / 255;
|
|
182
|
+
return channel <= 0.04045 ? channel / 12.92 : Math.pow((channel + 0.055) / 1.055, 2.4);
|
|
183
|
+
};
|
|
184
|
+
const ratioOf = (foreground, behind) => {
|
|
185
|
+
const foregroundLuminance = 0.2126 * linear(foreground[0]) + 0.7152 * linear(foreground[1]) + 0.0722 * linear(foreground[2]);
|
|
186
|
+
const backgroundLuminance = 0.2126 * linear(behind[0]) + 0.7152 * linear(behind[1]) + 0.0722 * linear(behind[2]);
|
|
187
|
+
return (Math.max(foregroundLuminance, backgroundLuminance) + 0.05) / (Math.min(foregroundLuminance, backgroundLuminance) + 0.05);
|
|
188
|
+
};
|
|
189
|
+
const hex = (color) => `#${color.slice(0, 3).map((value) => Math.round(value).toString(16).padStart(2, "0")).join("")}`;
|
|
190
|
+
const area = (right - left) * (bottom - top);
|
|
191
|
+
const step = Math.max(1, Math.floor(Math.sqrt(area / 12e3)));
|
|
192
|
+
const offsetAt = (x, y) => (y * normal.width + x) * 4;
|
|
193
|
+
const deltaAt = (offset) => Math.max(
|
|
194
|
+
Math.abs(normal.data[offset] - background.data[offset]),
|
|
195
|
+
Math.abs(normal.data[offset + 1] - background.data[offset + 1]),
|
|
196
|
+
Math.abs(normal.data[offset + 2] - background.data[offset + 2])
|
|
197
|
+
);
|
|
198
|
+
let strongestDelta = 0;
|
|
199
|
+
let strongestForeground = null;
|
|
200
|
+
let opaqueBackground = null;
|
|
201
|
+
for (let y = top; y < bottom; y += step) {
|
|
202
|
+
for (let x = left; x < right; x += step) {
|
|
203
|
+
const offset = offsetAt(x, y);
|
|
204
|
+
if (background.data[offset + 3] < 250) continue;
|
|
205
|
+
const behind = [
|
|
206
|
+
background.data[offset],
|
|
207
|
+
background.data[offset + 1],
|
|
208
|
+
background.data[offset + 2]
|
|
209
|
+
];
|
|
210
|
+
opaqueBackground ?? (opaqueBackground = behind);
|
|
211
|
+
const delta = deltaAt(offset);
|
|
212
|
+
if (delta <= strongestDelta) continue;
|
|
213
|
+
strongestDelta = delta;
|
|
214
|
+
strongestForeground = [
|
|
215
|
+
normal.data[offset],
|
|
216
|
+
normal.data[offset + 1],
|
|
217
|
+
normal.data[offset + 2]
|
|
218
|
+
];
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
let bestRatio = Infinity;
|
|
222
|
+
let bestForeground = null;
|
|
223
|
+
let bestBackground = null;
|
|
224
|
+
const consider = (foreground, behind) => {
|
|
225
|
+
const ratio = ratioOf(foreground, behind);
|
|
226
|
+
if (ratio >= bestRatio) return;
|
|
227
|
+
bestRatio = ratio;
|
|
228
|
+
bestForeground = foreground;
|
|
229
|
+
bestBackground = behind;
|
|
230
|
+
};
|
|
231
|
+
if (strongestDelta >= 2 && strongestForeground) {
|
|
232
|
+
for (let y = top; y < bottom; y += step) {
|
|
233
|
+
for (let x = left; x < right; x += step) {
|
|
234
|
+
const offset = offsetAt(x, y);
|
|
235
|
+
if (background.data[offset + 3] < 250 || deltaAt(offset) < 2) {
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
consider(strongestForeground, [
|
|
239
|
+
background.data[offset],
|
|
240
|
+
background.data[offset + 1],
|
|
241
|
+
background.data[offset + 2]
|
|
242
|
+
]);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
} else if (opaqueBackground) {
|
|
246
|
+
consider(opaqueBackground, opaqueBackground);
|
|
247
|
+
}
|
|
248
|
+
if (!bestForeground || !bestBackground || !Number.isFinite(bestRatio)) {
|
|
249
|
+
return {
|
|
250
|
+
ratio: null,
|
|
251
|
+
required,
|
|
252
|
+
reason: "could not isolate opaque rendered text pixels"
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
return {
|
|
256
|
+
ratio: Math.round(bestRatio * 100) / 100,
|
|
257
|
+
required,
|
|
258
|
+
foreground: hex(bestForeground),
|
|
259
|
+
background: hex(bestBackground)
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
function layoutGetNativeEval(opts = {}) {
|
|
263
|
+
const includeStyle = opts.styling === true;
|
|
264
|
+
const filter = opts.filter?.trim().toLowerCase() ?? "";
|
|
265
|
+
return `(async () => {
|
|
266
|
+
const t = window.__sootsimTest
|
|
267
|
+
if (!t || typeof t.listInspectable !== 'function') return []
|
|
268
|
+
const list = await t.listInspectable({})
|
|
269
|
+
if (!Array.isArray(list)) return []
|
|
270
|
+
const includeStyle = ${JSON.stringify(includeStyle)}
|
|
271
|
+
const scoreContrast = ${scoreLayoutTextContrast.toString()}
|
|
272
|
+
// narrows by ELEMENT, not by style field: fewer rows, full detail on each.
|
|
273
|
+
const filter = ${JSON.stringify(filter)}
|
|
274
|
+
const matchesFilter = (n, text) => {
|
|
275
|
+
if (!filter) return true
|
|
276
|
+
const fields = [text, n.testID || n.testId, n.accessibilityRole || n.role, n.type]
|
|
277
|
+
return fields.some((f) => typeof f === 'string' && f.toLowerCase().includes(filter))
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : 0)
|
|
281
|
+
const round = (v) => Math.round(num(v))
|
|
282
|
+
|
|
283
|
+
// borderRadius can be authored as a string ("12px") or split per-corner;
|
|
284
|
+
// prefer the uniform borderRadius, else the largest corner radius present.
|
|
285
|
+
const radiusOf = (style) => {
|
|
286
|
+
if (!style) return 0
|
|
287
|
+
const keys = ['borderRadius','borderTopLeftRadius','borderTopRightRadius','borderBottomLeftRadius','borderBottomRightRadius']
|
|
288
|
+
let max = 0
|
|
289
|
+
for (const k of keys) {
|
|
290
|
+
const raw = style[k]
|
|
291
|
+
const n = typeof raw === 'number' ? raw : (typeof raw === 'string' ? parseFloat(raw) : NaN)
|
|
292
|
+
if (Number.isFinite(n)) max = Math.max(max, n)
|
|
293
|
+
}
|
|
294
|
+
return Math.round(max)
|
|
295
|
+
}
|
|
296
|
+
const fontSizeOf = (style) => {
|
|
297
|
+
if (!style) return 0
|
|
298
|
+
const raw = style.fontSize
|
|
299
|
+
const n = typeof raw === 'number' ? raw : (typeof raw === 'string' ? parseFloat(raw) : NaN)
|
|
300
|
+
return Number.isFinite(n) ? Math.round(n) : 0
|
|
301
|
+
}
|
|
302
|
+
// styling extractors \u2014 omit boring defaults so the agent only sees notable
|
|
303
|
+
// styling (low opacity, struck text, clipping, a real border/color).
|
|
304
|
+
const colorStr = (v) => {
|
|
305
|
+
if (v == null) return undefined
|
|
306
|
+
const s = String(v).split(' ').join('')
|
|
307
|
+
return s && s !== 'transparent' && s !== 'rgba(0,0,0,0)' ? s : undefined
|
|
308
|
+
}
|
|
309
|
+
const opacityOf = (style) => {
|
|
310
|
+
const o = style && style.opacity
|
|
311
|
+
const n = typeof o === 'number' ? o : (typeof o === 'string' ? parseFloat(o) : NaN)
|
|
312
|
+
return Number.isFinite(n) && n < 1 ? Math.round(n * 100) / 100 : undefined
|
|
313
|
+
}
|
|
314
|
+
const weightOf = (style) => {
|
|
315
|
+
const w = style && style.fontWeight
|
|
316
|
+
const n = w === 'bold' ? 700 : (typeof w === 'number' ? w : (typeof w === 'string' ? parseInt(w, 10) : NaN))
|
|
317
|
+
return Number.isFinite(n) && n >= 600 ? n : undefined
|
|
318
|
+
}
|
|
319
|
+
const decoOf = (style) => {
|
|
320
|
+
const d = style && (style.textDecorationLine || style.textDecoration)
|
|
321
|
+
return d && d !== 'none' && d !== 'normal' ? String(d).split(' ')[0] : undefined
|
|
322
|
+
}
|
|
323
|
+
const overflowOf = (style) => (style && style.overflow === 'hidden' ? 'hidden' : undefined)
|
|
324
|
+
const borderWOf = (style) => {
|
|
325
|
+
if (!style) return undefined
|
|
326
|
+
const keys = ['borderWidth','borderTopWidth','borderBottomWidth','borderLeftWidth','borderRightWidth']
|
|
327
|
+
let max = 0
|
|
328
|
+
for (const k of keys) {
|
|
329
|
+
const r = style[k]
|
|
330
|
+
const n = typeof r === 'number' ? r : (typeof r === 'string' ? parseFloat(r) : NaN)
|
|
331
|
+
if (Number.isFinite(n)) max = Math.max(max, n)
|
|
332
|
+
}
|
|
333
|
+
return max > 0 ? Math.round(max) : undefined
|
|
334
|
+
}
|
|
335
|
+
// only meaningful alongside a real border width, so it is resolved by the
|
|
336
|
+
// caller below rather than on its own.
|
|
337
|
+
const borderColorOf = (style) => {
|
|
338
|
+
if (!style) return undefined
|
|
339
|
+
const keys = ['borderColor','borderTopColor','borderBottomColor','borderLeftColor','borderRightColor']
|
|
340
|
+
for (const k of keys) {
|
|
341
|
+
const c = colorStr(style[k])
|
|
342
|
+
if (c) return c
|
|
343
|
+
}
|
|
344
|
+
return undefined
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const isVisible = (n) => {
|
|
348
|
+
const layout = n && n.layout
|
|
349
|
+
if (!layout || layout.width <= 0 || layout.height <= 0) return false
|
|
350
|
+
const abs = (n && (n.absolute || n.absolutePosition)) || null
|
|
351
|
+
if (!abs) return true
|
|
352
|
+
const device = (n && n.device) || {}
|
|
353
|
+
const screenW = Number(device.width) || window.innerWidth || 0
|
|
354
|
+
const screenH = Number(device.height) || window.innerHeight || 0
|
|
355
|
+
if (!screenW || !screenH) return true
|
|
356
|
+
return abs.x + layout.width > 0 && abs.y + layout.height > 0 && abs.x < screenW && abs.y < screenH
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const elements = list.filter(isVisible).filter((n) => {
|
|
360
|
+
const t = typeof n.text === 'string' && n.text.trim() ? n.text.trim() : undefined
|
|
361
|
+
return matchesFilter(n, t)
|
|
362
|
+
}).map((n) => {
|
|
363
|
+
const layout = n.layout || { x: 0, y: 0, width: 0, height: 0 }
|
|
364
|
+
const abs = n.absolute || n.absolutePosition || { x: layout.x, y: layout.y }
|
|
365
|
+
const text = typeof n.text === 'string' && n.text.trim() ? n.text.trim() : undefined
|
|
366
|
+
const base = {
|
|
367
|
+
...(typeof n.nodeId === 'number'
|
|
368
|
+
? {
|
|
369
|
+
nodeId: n.nodeId,
|
|
370
|
+
ancestorIds: Array.isArray(n.ancestors)
|
|
371
|
+
? n.ancestors.map((ancestor) => ancestor && ancestor.nodeId).filter((id) => typeof id === 'number')
|
|
372
|
+
: [],
|
|
373
|
+
}
|
|
374
|
+
: {}),
|
|
375
|
+
testID: n.testID || n.testId || undefined,
|
|
376
|
+
role: n.accessibilityRole || n.role || undefined,
|
|
377
|
+
type: typeof n.type === 'string' ? n.type : 'node',
|
|
378
|
+
text,
|
|
379
|
+
box: { x: round(abs.x), y: round(abs.y), w: round(layout.width), h: round(layout.height) },
|
|
380
|
+
}
|
|
381
|
+
if (!includeStyle) return base
|
|
382
|
+
// computedStyle has the resolved values; style is the curated subset \u2014
|
|
383
|
+
// fall back across both so radius/fontSize survive either shape.
|
|
384
|
+
const style = Object.assign({}, n.style || {}, n.computedStyle || {})
|
|
385
|
+
const pad = (n.boxModel && n.boxModel.padding) || {}
|
|
386
|
+
const borderWidth = borderWOf(style)
|
|
387
|
+
return {
|
|
388
|
+
...base,
|
|
389
|
+
borderRadius: radiusOf(style),
|
|
390
|
+
padding: { t: round(pad.top), r: round(pad.right), b: round(pad.bottom), l: round(pad.left) },
|
|
391
|
+
fontSize: text ? (fontSizeOf(style) || 14) : 0,
|
|
392
|
+
color: text ? colorStr(style.color) : undefined,
|
|
393
|
+
bg: colorStr(style.backgroundColor),
|
|
394
|
+
opacity: opacityOf(style),
|
|
395
|
+
fontWeight: weightOf(style),
|
|
396
|
+
textDecoration: decoOf(style),
|
|
397
|
+
overflow: overflowOf(style),
|
|
398
|
+
borderWidth,
|
|
399
|
+
borderColor: borderWidth ? borderColorOf(style) : undefined,
|
|
400
|
+
}
|
|
401
|
+
})
|
|
402
|
+
const textElements = elements.filter((element) => element.text && element.fontSize > 0)
|
|
403
|
+
if (!includeStyle || textElements.length === 0) return elements
|
|
404
|
+
|
|
405
|
+
const screenshot = window.SootSim && window.SootSim.bridges && window.SootSim.bridges.screenshot
|
|
406
|
+
if (typeof screenshot !== 'function') {
|
|
407
|
+
throw new Error('styled native layout requires the rnx capture bridge')
|
|
408
|
+
}
|
|
409
|
+
const device = list.find((node) => node && node.device)?.device || {}
|
|
410
|
+
const width = Math.round(Number(device.width) || window.innerWidth || 0)
|
|
411
|
+
const height = Math.round(Number(device.height) || window.innerHeight || 0)
|
|
412
|
+
if (!width || !height) throw new Error('native layout has no capture dimensions')
|
|
413
|
+
const capture = async (suppressText) => {
|
|
414
|
+
const dataUrl = await screenshot({
|
|
415
|
+
format: 'png',
|
|
416
|
+
outputWidth: width,
|
|
417
|
+
outputHeight: height,
|
|
418
|
+
...(suppressText ? { suppressText: true } : {}),
|
|
419
|
+
})
|
|
420
|
+
const image = await new Promise((resolve, reject) => {
|
|
421
|
+
const next = new Image()
|
|
422
|
+
next.onload = () => resolve(next)
|
|
423
|
+
next.onerror = () => reject(new Error('could not decode native layout pixels'))
|
|
424
|
+
next.src = dataUrl
|
|
425
|
+
})
|
|
426
|
+
const canvas = document.createElement('canvas')
|
|
427
|
+
canvas.width = image.naturalWidth
|
|
428
|
+
canvas.height = image.naturalHeight
|
|
429
|
+
const context = canvas.getContext('2d', { willReadFrequently: true })
|
|
430
|
+
if (!context) throw new Error('could not read native layout pixels')
|
|
431
|
+
context.drawImage(image, 0, 0)
|
|
432
|
+
return context.getImageData(0, 0, canvas.width, canvas.height)
|
|
433
|
+
}
|
|
434
|
+
const normal = await capture(false)
|
|
435
|
+
const background = await capture(true)
|
|
436
|
+
for (const element of textElements) {
|
|
437
|
+
element.contrast = scoreContrast({
|
|
438
|
+
normal,
|
|
439
|
+
background,
|
|
440
|
+
box: element.box,
|
|
441
|
+
fontSize: element.fontSize,
|
|
442
|
+
fontWeight: element.fontWeight,
|
|
443
|
+
})
|
|
444
|
+
}
|
|
445
|
+
return elements
|
|
446
|
+
})()`;
|
|
447
|
+
}
|
|
448
|
+
var LAYOUT_GET_NATIVE_EVAL = layoutGetNativeEval();
|
|
449
|
+
async function inspectGetLayout(bridge, opts = {}) {
|
|
450
|
+
if (!opts.styling) {
|
|
451
|
+
const result = await bridge.send({
|
|
452
|
+
type: "query",
|
|
453
|
+
query: { kind: "find", selector: { visible: true } }
|
|
454
|
+
});
|
|
455
|
+
if (!isSemanticQueryResult(result)) return [];
|
|
456
|
+
const filter = opts.filter?.trim().toLowerCase() ?? "";
|
|
457
|
+
return result.nodes.filter((node) => {
|
|
458
|
+
if (!filter) return true;
|
|
459
|
+
return [node.text, node.testID, node.role, node.type].some(
|
|
460
|
+
(value) => typeof value === "string" && value.toLowerCase().includes(filter)
|
|
461
|
+
);
|
|
462
|
+
}).map((node) => ({
|
|
463
|
+
nodeId: node.nodeId,
|
|
464
|
+
...node.testID ? { testID: node.testID } : {},
|
|
465
|
+
...node.role ? { role: node.role } : {},
|
|
466
|
+
type: node.type,
|
|
467
|
+
...node.text ? { text: node.text } : {},
|
|
468
|
+
box: {
|
|
469
|
+
x: Math.round(node.geometry.x),
|
|
470
|
+
y: Math.round(node.geometry.y),
|
|
471
|
+
w: Math.round(node.geometry.width),
|
|
472
|
+
h: Math.round(node.geometry.height)
|
|
473
|
+
}
|
|
474
|
+
}));
|
|
475
|
+
}
|
|
476
|
+
const raw = await bridge.send({
|
|
477
|
+
type: "evaluate",
|
|
478
|
+
code: layoutGetNativeEval(opts)
|
|
479
|
+
});
|
|
480
|
+
return Array.isArray(raw) ? raw : [];
|
|
481
|
+
}
|
|
482
|
+
function formatLayoutElements(elements, opts) {
|
|
483
|
+
if (elements.length === 0) return " no visible elements found";
|
|
484
|
+
const lines = elements.map((el) => {
|
|
485
|
+
const ident = el.testID ? `#${el.testID}` : el.selector ? el.selector : el.role ? `[${el.role}]` : `<${el.type}>`;
|
|
486
|
+
const parts = [ident, `@(${el.box.x},${el.box.y})`, `${el.box.w}x${el.box.h}`];
|
|
487
|
+
if (el.borderRadius !== void 0 && el.borderRadius > 0) {
|
|
488
|
+
parts.push(`radius:${el.borderRadius}`);
|
|
489
|
+
}
|
|
490
|
+
const p = el.padding;
|
|
491
|
+
if (p && (p.t || p.r || p.b || p.l)) {
|
|
492
|
+
const uniform = p.t === p.r && p.r === p.b && p.b === p.l;
|
|
493
|
+
parts.push(uniform ? `pad:${p.t}` : `pad:${p.t},${p.r},${p.b},${p.l}`);
|
|
494
|
+
}
|
|
495
|
+
if (el.fontSize !== void 0 && el.fontSize > 0) {
|
|
496
|
+
parts.push(`font:${el.fontSize}`);
|
|
497
|
+
}
|
|
498
|
+
if (opts?.styling) {
|
|
499
|
+
if (el.fontWeight) parts.push(`weight:${el.fontWeight}`);
|
|
500
|
+
if (el.color) parts.push(`color:${el.color}`);
|
|
501
|
+
if (el.bg) parts.push(`bg:${el.bg}`);
|
|
502
|
+
if (el.contrast) {
|
|
503
|
+
const score = el.contrast.ratio == null ? `?/${el.contrast.required}` : `${el.contrast.ratio}/${el.contrast.required}${el.contrast.ratio < el.contrast.required ? " FAIL" : ""}`;
|
|
504
|
+
parts.push(`contrast:${score}`);
|
|
505
|
+
if (el.contrast.background) parts.push(`on:${el.contrast.background}`);
|
|
506
|
+
}
|
|
507
|
+
if (el.opacity != null) parts.push(`opacity:${el.opacity}`);
|
|
508
|
+
if (el.textDecoration) parts.push(`deco:${el.textDecoration}`);
|
|
509
|
+
if (el.overflow) parts.push(`overflow:${el.overflow}`);
|
|
510
|
+
if (el.borderWidth) {
|
|
511
|
+
parts.push(
|
|
512
|
+
el.borderColor ? `border:${el.borderWidth}/${el.borderColor}` : `border:${el.borderWidth}`
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
if (el.text) {
|
|
517
|
+
const text = el.text.length > 40 ? `${el.text.slice(0, 39)}\u2026` : el.text;
|
|
518
|
+
parts.push(`"${text}"`);
|
|
519
|
+
}
|
|
520
|
+
return ` ${parts.join(" ")}`;
|
|
521
|
+
});
|
|
522
|
+
return lines.join("\n");
|
|
523
|
+
}
|
|
524
|
+
async function inspectAccessibilityTree(bridge) {
|
|
525
|
+
const result = await bridge.send({
|
|
526
|
+
type: "query",
|
|
527
|
+
query: { kind: "find", selector: { visible: true } }
|
|
528
|
+
});
|
|
529
|
+
if (!isSemanticQueryResult(result)) return [];
|
|
530
|
+
return result.nodes.flatMap((node) => {
|
|
531
|
+
if (!node.role && !node.label && !node.text && !node.pressable) return [];
|
|
532
|
+
return [
|
|
533
|
+
{
|
|
534
|
+
role: node.role ?? (node.pressable ? "button" : node.type === "text" ? "statictext" : "none"),
|
|
535
|
+
label: node.label ?? node.text ?? null,
|
|
536
|
+
hint: null,
|
|
537
|
+
state: null,
|
|
538
|
+
testID: node.testID ?? null,
|
|
539
|
+
position: {
|
|
540
|
+
x: Math.round(node.geometry.x),
|
|
541
|
+
y: Math.round(node.geometry.y)
|
|
542
|
+
},
|
|
543
|
+
size: {
|
|
544
|
+
w: Math.round(node.geometry.width),
|
|
545
|
+
h: Math.round(node.geometry.height)
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
];
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
function resolveFindMode(q) {
|
|
552
|
+
if (q.testId) {
|
|
553
|
+
return { mode: "testid", selector: { testID: q.testId } };
|
|
554
|
+
}
|
|
555
|
+
if (q.role) {
|
|
556
|
+
return { mode: "role", selector: { role: q.role } };
|
|
557
|
+
}
|
|
558
|
+
if (q.type) {
|
|
559
|
+
return { mode: "type", selector: { type: q.type } };
|
|
560
|
+
}
|
|
561
|
+
if (q.pressable) {
|
|
562
|
+
return { mode: "pressable", selector: { pressable: true } };
|
|
563
|
+
}
|
|
564
|
+
if (q.interactive) {
|
|
565
|
+
return { mode: "interactive-targets", selector: { pressable: true } };
|
|
566
|
+
}
|
|
567
|
+
if (q.visible) {
|
|
568
|
+
return { mode: "visible", selector: { visible: true } };
|
|
569
|
+
}
|
|
570
|
+
if (q.text) {
|
|
571
|
+
return { mode: "text", selector: { text: q.text } };
|
|
572
|
+
}
|
|
573
|
+
return null;
|
|
574
|
+
}
|
|
575
|
+
async function inspectFind(bridge, q) {
|
|
576
|
+
const resolved = resolveFindMode(q);
|
|
577
|
+
if (!resolved) return null;
|
|
578
|
+
const result = await bridge.send({
|
|
579
|
+
type: "query",
|
|
580
|
+
query: { kind: "find", selector: resolved.selector }
|
|
581
|
+
});
|
|
582
|
+
if (!isSemanticQueryResult(result)) return { mode: resolved.mode, result: [] };
|
|
583
|
+
const nodes = result.nodes.map(
|
|
584
|
+
(node) => ({
|
|
585
|
+
type: node.type,
|
|
586
|
+
nodeId: node.nodeId,
|
|
587
|
+
...node.testID ? { testID: node.testID } : {},
|
|
588
|
+
...node.text ? { text: node.text } : {},
|
|
589
|
+
...node.pressable ? { pressable: true } : {},
|
|
590
|
+
...node.role ? { accessibilityRole: node.role } : {},
|
|
591
|
+
...node.label ? { accessibilityLabel: node.label } : {},
|
|
592
|
+
absolutePosition: { x: node.geometry.x, y: node.geometry.y },
|
|
593
|
+
layout: { width: node.geometry.width, height: node.geometry.height }
|
|
594
|
+
})
|
|
595
|
+
);
|
|
596
|
+
const resultValue = resolved.mode === "testid" || resolved.mode === "text" ? nodes[0] ?? null : nodes;
|
|
597
|
+
return { mode: resolved.mode, result: resultValue };
|
|
598
|
+
}
|
|
599
|
+
function rankInteractive(nodes) {
|
|
600
|
+
return [...nodes].sort((a, b) => scoreInteractive(b) - scoreInteractive(a));
|
|
601
|
+
}
|
|
602
|
+
function scoreInteractive(n) {
|
|
603
|
+
let score = 0;
|
|
604
|
+
if (n.testID) score += 100;
|
|
605
|
+
if (typeof n.text === "string" && n.text.trim().length > 0) score += 60;
|
|
606
|
+
if (typeof n.accessibilityLabel === "string" && n.accessibilityLabel.trim().length > 0) {
|
|
607
|
+
score += 30;
|
|
608
|
+
}
|
|
609
|
+
if (n.accessibilityRole) score += 15;
|
|
610
|
+
const w = n.layout?.width ?? 0;
|
|
611
|
+
const h = n.layout?.height ?? 0;
|
|
612
|
+
const area = w * h;
|
|
613
|
+
if (area >= 400 && area <= 6e4) score += 25;
|
|
614
|
+
else if (area > 6e4) score -= 20;
|
|
615
|
+
const y = n.absolutePosition?.y ?? 0;
|
|
616
|
+
if (y < 0) score -= 30;
|
|
617
|
+
return score;
|
|
618
|
+
}
|
|
619
|
+
function tapCommandForNode(n) {
|
|
620
|
+
if (n.testID) return `rnx do tap-id ${shellEscape(n.testID)}`;
|
|
621
|
+
const text = typeof n.text === "string" ? n.text.trim() : "";
|
|
622
|
+
if (text.length > 0 && text.length <= 80) return `rnx do tap-text ${shellEscape(text)}`;
|
|
623
|
+
const x = Math.round(((n.absolutePosition?.x ?? 0) + (n.layout?.width ?? 0) / 2) * 10) / 10;
|
|
624
|
+
const y = Math.round(((n.absolutePosition?.y ?? 0) + (n.layout?.height ?? 0) / 2) * 10) / 10;
|
|
625
|
+
return `rnx do tap ${x} ${y}`;
|
|
626
|
+
}
|
|
627
|
+
function shellEscape(value) {
|
|
628
|
+
if (/^[A-Za-z0-9_./@:-]+$/.test(value)) return value;
|
|
629
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
630
|
+
}
|
|
631
|
+
function waitReadyReason(status) {
|
|
632
|
+
if (status.bridgeError) {
|
|
633
|
+
if (status.bridgeError.includes(HEADLESS_TENANT_FETCH_HUNG_MARKER) || status.bridgeError.includes(PUBLIC_NO_NETWORK_MARKER)) {
|
|
634
|
+
return status.bridgeError;
|
|
635
|
+
}
|
|
636
|
+
return `sim unreachable: ${status.bridgeError} \u2014 the target tab is closed or no sim is connected (check \`rnx list\`, reopen with \`rnx open\`)`;
|
|
637
|
+
}
|
|
638
|
+
const waitingOnGuestFetch = status.externalReady === false && /https?:\/\//.test(status.externalStatus);
|
|
639
|
+
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";
|
|
640
|
+
return !status.externalError && status.suppressedEntryError ? `${base} (suppressed entry error: ${status.suppressedEntryError})` : base;
|
|
641
|
+
}
|
|
642
|
+
async function inspectWaitReady(bridge, timeoutMs = 2e4, options = {}) {
|
|
643
|
+
const start = Date.now();
|
|
644
|
+
let result;
|
|
645
|
+
try {
|
|
646
|
+
result = await bridge.send(
|
|
647
|
+
{
|
|
648
|
+
type: "waitFor",
|
|
649
|
+
...options.simId ? { simId: options.simId } : {},
|
|
650
|
+
waitForOptions: { condition: { type: "ready" }, timeoutMs }
|
|
651
|
+
},
|
|
652
|
+
{ timeoutMs: timeoutMs + 1e3 }
|
|
653
|
+
);
|
|
654
|
+
} catch (error) {
|
|
655
|
+
const bridgeError = error instanceof Error ? error.message : String(error);
|
|
656
|
+
if (bridgeError.startsWith("multiple sims are connected:") || bridgeError.startsWith("saved sim ")) {
|
|
657
|
+
throw error;
|
|
658
|
+
}
|
|
659
|
+
result = {
|
|
660
|
+
matched: false,
|
|
661
|
+
elapsedMs: Date.now() - start,
|
|
662
|
+
polls: 0,
|
|
663
|
+
error: bridgeError
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
const probe = result.probe;
|
|
667
|
+
const externalError = probe?.externalError ?? "";
|
|
668
|
+
const status = {
|
|
669
|
+
ready: result.matched && !externalError,
|
|
670
|
+
elapsedMs: result.elapsedMs,
|
|
671
|
+
nodes: probe?.nodes ?? 0,
|
|
672
|
+
targets: probe?.targets ?? 0,
|
|
673
|
+
liveFrameActive: probe?.liveFrameActive ?? false,
|
|
674
|
+
liveFrameChannels: probe?.liveFrameChannels ?? 0,
|
|
675
|
+
liveFramePublishes: probe?.liveFramePublishes ?? 0,
|
|
676
|
+
flag: probe?.flag,
|
|
677
|
+
loadingText: probe?.loadingText ?? "",
|
|
678
|
+
externalReady: probe?.externalReady ?? null,
|
|
679
|
+
externalStatus: probe?.externalStatus ?? "",
|
|
680
|
+
externalError,
|
|
681
|
+
suppressedEntryError: probe?.suppressedEntryError ?? "",
|
|
682
|
+
errors: probe?.errors ?? 0,
|
|
683
|
+
bridgeError: result.error ?? ""
|
|
684
|
+
};
|
|
685
|
+
if (!status.ready && options.onProgress) options.onProgress(status);
|
|
686
|
+
return status;
|
|
687
|
+
}
|
|
688
|
+
async function inspectWaitSelector(bridge, testId, timeoutMs = 5e3, opts = {}) {
|
|
689
|
+
const gone = opts.gone === true;
|
|
690
|
+
const result = await bridge.send(
|
|
691
|
+
{
|
|
692
|
+
type: "waitFor",
|
|
693
|
+
waitForOptions: {
|
|
694
|
+
condition: {
|
|
695
|
+
type: gone ? "absent" : opts.structural === true ? "mounted" : "present",
|
|
696
|
+
selector: { testID: testId }
|
|
697
|
+
},
|
|
698
|
+
timeoutMs
|
|
699
|
+
}
|
|
700
|
+
},
|
|
701
|
+
{ timeoutMs: timeoutMs + 1e3 }
|
|
702
|
+
);
|
|
703
|
+
if (result?.error) throw new Error(result.error);
|
|
704
|
+
if (result?.node && !isSimSemanticTree([result.node])) {
|
|
705
|
+
throw new Error("waitFor returned an invalid semantic node");
|
|
706
|
+
}
|
|
707
|
+
return {
|
|
708
|
+
found: result?.matched === true,
|
|
709
|
+
...result?.node ? { node: result.node } : {},
|
|
710
|
+
elapsed: result?.elapsedMs ?? timeoutMs
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
function consoleEntriesEval(level, limit) {
|
|
714
|
+
const getter = level === "error" ? "getErrors" : "getWarnings";
|
|
715
|
+
return `(() => {
|
|
716
|
+
const out = []
|
|
717
|
+
const seen = new Set()
|
|
718
|
+
const norm = (s) => (typeof s === 'string' ? s : (() => { try { return JSON.stringify(s) } catch { return String(s) } })())
|
|
719
|
+
const key = (ts, args) => Math.round((ts || 0) / 250) + '|' + (Array.isArray(args) ? args.map(norm).join(' ') : norm(args))
|
|
720
|
+
const push = (e) => {
|
|
721
|
+
if (!e) return
|
|
722
|
+
const ts = typeof e.timestamp === 'number' ? e.timestamp : (typeof e.ts === 'number' ? e.ts : 0)
|
|
723
|
+
const k = key(ts, e.args)
|
|
724
|
+
if (seen.has(k)) return
|
|
725
|
+
seen.add(k)
|
|
726
|
+
out.push({ timestamp: ts, args: Array.isArray(e.args) ? e.args : [e.args], stack: e.stack || undefined, source: e.source || undefined })
|
|
727
|
+
}
|
|
728
|
+
// page-realm ws-bridge buffer first (richest stacks for page-origin errors)
|
|
729
|
+
try { for (const e of (window.__sootsimConsole?.${getter}(${limit}) || [])) push(e) } catch {}
|
|
730
|
+
// engine observability store \u2014 the always-on render-worker / forwarded path
|
|
731
|
+
try {
|
|
732
|
+
const obs = window.__sootsimObservability
|
|
733
|
+
const snap = obs && obs.logs && typeof obs.logs.getSnapshot === 'function' ? obs.logs.getSnapshot() : []
|
|
734
|
+
for (const e of snap) if (e && e.level === ${JSON.stringify(level)}) push(e)
|
|
735
|
+
} catch {}
|
|
736
|
+
return out.sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0)).slice(-${limit})
|
|
737
|
+
})()`;
|
|
738
|
+
}
|
|
739
|
+
async function inspectErrors(bridge, limit = 20) {
|
|
740
|
+
const result = await bridge.send({
|
|
741
|
+
type: "evaluate",
|
|
742
|
+
code: consoleEntriesEval("error", limit)
|
|
743
|
+
});
|
|
744
|
+
return Array.isArray(result) ? result : [];
|
|
745
|
+
}
|
|
746
|
+
async function inspectWarnings(bridge, limit = 20) {
|
|
747
|
+
const result = await bridge.send({
|
|
748
|
+
type: "evaluate",
|
|
749
|
+
code: consoleEntriesEval("warn", limit)
|
|
750
|
+
});
|
|
751
|
+
return Array.isArray(result) ? result : [];
|
|
752
|
+
}
|
|
753
|
+
var MERGED_CONSOLE_COUNT_EVAL = `(() => {
|
|
754
|
+
const norm = (s) => (typeof s === 'string' ? s : (() => { try { return JSON.stringify(s) } catch { return String(s) } })())
|
|
755
|
+
const key = (lvl, ts, args) => lvl + '|' + Math.round((ts || 0) / 250) + '|' + (Array.isArray(args) ? args.map(norm).join(' ') : norm(args))
|
|
756
|
+
const seen = new Set()
|
|
757
|
+
let errors = 0
|
|
758
|
+
let warnings = 0
|
|
759
|
+
const add = (lvl, ts, args) => {
|
|
760
|
+
if (lvl !== 'error' && lvl !== 'warn') return
|
|
761
|
+
const k = key(lvl, ts, args)
|
|
762
|
+
if (seen.has(k)) return
|
|
763
|
+
seen.add(k)
|
|
764
|
+
if (lvl === 'error') errors++
|
|
765
|
+
else warnings++
|
|
766
|
+
}
|
|
767
|
+
try {
|
|
768
|
+
const c = window.__sootsimConsole
|
|
769
|
+
for (const e of (c?.getErrors?.(200) || [])) add('error', e?.timestamp ?? e?.ts, e?.args)
|
|
770
|
+
for (const e of (c?.getWarnings?.(200) || [])) add('warn', e?.timestamp ?? e?.ts, e?.args)
|
|
771
|
+
} catch {}
|
|
772
|
+
try {
|
|
773
|
+
const obs = window.__sootsimObservability
|
|
774
|
+
const snap = obs && obs.logs && typeof obs.logs.getSnapshot === 'function' ? obs.logs.getSnapshot() : []
|
|
775
|
+
for (const e of snap) if (e) add(e.level, e.ts, e.args)
|
|
776
|
+
} catch {}
|
|
777
|
+
return { errors, warnings, total: errors + warnings }
|
|
778
|
+
})()`;
|
|
779
|
+
async function clearConsole(bridge) {
|
|
780
|
+
await bridge.send({
|
|
781
|
+
type: "evaluate",
|
|
782
|
+
code: 'window.__sootsimConsole?.clear(); window.__sootsimObservability?.logs?.clear?.(); "cleared"'
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
async function inspectRequests(bridge, opts = {}) {
|
|
786
|
+
const limit = opts.limit ?? 20;
|
|
787
|
+
const method = opts.failed === false ? "getRequests" : "getFailedRequests";
|
|
788
|
+
const result = await bridge.send({
|
|
789
|
+
type: "call",
|
|
790
|
+
path: `__sootsimTest.${method}`,
|
|
791
|
+
args: [limit]
|
|
792
|
+
});
|
|
793
|
+
return Array.isArray(result) ? result : [];
|
|
794
|
+
}
|
|
795
|
+
async function clearRequests(bridge) {
|
|
796
|
+
await bridge.send({ type: "call", path: "__sootsimTest.clearRequests", args: [] });
|
|
797
|
+
}
|
|
798
|
+
async function inspectScrollStateAt(bridge, x, y) {
|
|
799
|
+
const result = await bridge.send({
|
|
800
|
+
type: "call",
|
|
801
|
+
path: "__sootsimTest.getScrollStateAt",
|
|
802
|
+
args: [x, y]
|
|
803
|
+
});
|
|
804
|
+
if (!result || typeof result !== "object") return null;
|
|
805
|
+
const state = result;
|
|
806
|
+
return {
|
|
807
|
+
...state,
|
|
808
|
+
scrollOffsetX: typeof state.scrollOffsetX === "number" ? state.scrollOffsetX : state.offsetX,
|
|
809
|
+
scrollOffsetY: typeof state.scrollOffsetY === "number" ? state.scrollOffsetY : state.offsetY
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
async function inspectLogs(bridge) {
|
|
813
|
+
if (bridge.plane === "cloud") {
|
|
814
|
+
const state = await bridge.send({
|
|
815
|
+
type: "state",
|
|
816
|
+
stateOptions: {
|
|
817
|
+
screenshot: false,
|
|
818
|
+
tree: false,
|
|
819
|
+
route: false,
|
|
820
|
+
errors: false,
|
|
821
|
+
logs: true,
|
|
822
|
+
logLimit: 100
|
|
823
|
+
}
|
|
824
|
+
});
|
|
825
|
+
const recentLogs = state && typeof state === "object" && Array.isArray(Reflect.get(state, "recentLogs")) ? Reflect.get(state, "recentLogs") : [];
|
|
826
|
+
return recentLogs.flatMap((entry, index) => {
|
|
827
|
+
if (!entry || typeof entry !== "object") return [];
|
|
828
|
+
const text = Reflect.get(entry, "text");
|
|
829
|
+
const rawLevel = Reflect.get(entry, "level");
|
|
830
|
+
if (typeof text !== "string") return [];
|
|
831
|
+
const level = rawLevel === "log" || rawLevel === "info" || rawLevel === "warn" || rawLevel === "error" || rawLevel === "debug" ? rawLevel : "error";
|
|
832
|
+
const at = Reflect.get(entry, "at");
|
|
833
|
+
const ts = typeof at === "number" && Number.isFinite(at) ? at : 0;
|
|
834
|
+
return [
|
|
835
|
+
{
|
|
836
|
+
id: `cloud-${ts}-${index}`,
|
|
837
|
+
source: "rnx-cloud",
|
|
838
|
+
level,
|
|
839
|
+
ts,
|
|
840
|
+
args: [text],
|
|
841
|
+
stack: null
|
|
842
|
+
}
|
|
843
|
+
];
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
const res = await bridge.send({
|
|
847
|
+
type: "evaluate",
|
|
848
|
+
code: `(() => {
|
|
849
|
+
const obs = window.__sootsimObservability;
|
|
850
|
+
if (!obs) return { ok: false };
|
|
851
|
+
return { ok: true, entries: obs.logs.getSnapshot() };
|
|
852
|
+
})()`
|
|
853
|
+
});
|
|
854
|
+
if (!res || !res.ok) return [];
|
|
855
|
+
return res.entries ?? [];
|
|
856
|
+
}
|
|
857
|
+
async function clearLogs(bridge) {
|
|
858
|
+
await bridge.send({
|
|
859
|
+
type: "evaluate",
|
|
860
|
+
code: 'window.__sootsimObservability?.logs.clear(); "cleared"'
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
function isForwardedWorkerLogTwin(a, b) {
|
|
864
|
+
if (a.source === b.source) return false;
|
|
865
|
+
if (a.level !== b.level) return false;
|
|
866
|
+
if (Math.abs(a.ts - b.ts) > 1e3) return false;
|
|
867
|
+
if (a.args.length !== b.args.length) return false;
|
|
868
|
+
const sources = /* @__PURE__ */ new Set([a.source, b.source]);
|
|
869
|
+
if (!sources.has("sootsim-worker")) return false;
|
|
870
|
+
if (!sources.has("render-worker") && !sources.has("forwarded-render-worker")) {
|
|
871
|
+
return false;
|
|
872
|
+
}
|
|
873
|
+
return a.args.every((arg, index) => arg === b.args[index]);
|
|
874
|
+
}
|
|
875
|
+
function filterLogEntries(entries, opts = {}) {
|
|
876
|
+
let out = [];
|
|
877
|
+
for (const entry of entries) {
|
|
878
|
+
if (out.some((candidate) => isForwardedWorkerLogTwin(candidate, entry))) {
|
|
879
|
+
continue;
|
|
880
|
+
}
|
|
881
|
+
out.push(entry);
|
|
882
|
+
}
|
|
883
|
+
if (!opts.showInternal) {
|
|
884
|
+
out = out.filter((e) => {
|
|
885
|
+
const first = e.args[0];
|
|
886
|
+
return !(typeof first === "string" && /^\[(?:rnx|sootsim)(?:\s[^\]]*)?\]/.test(first));
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
if (opts.level) out = out.filter((e) => opts.level.has(e.level));
|
|
890
|
+
if (opts.filter) {
|
|
891
|
+
const lf = opts.filter.toLowerCase();
|
|
892
|
+
out = out.filter((e) => e.args.join(" ").toLowerCase().includes(lf));
|
|
893
|
+
}
|
|
894
|
+
return out;
|
|
895
|
+
}
|
|
896
|
+
async function inspectTimelineSummary(bridge, query) {
|
|
897
|
+
return await bridge.send({
|
|
898
|
+
type: "call",
|
|
899
|
+
path: "SootSim.bridges.timeline.summary",
|
|
900
|
+
args: [query]
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
async function inspectTimelineRecent(bridge, query) {
|
|
904
|
+
return await bridge.send({
|
|
905
|
+
type: "call",
|
|
906
|
+
path: "SootSim.bridges.timeline.recent",
|
|
907
|
+
args: [query]
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
async function detectOpenNativeUI(bridge) {
|
|
911
|
+
let events;
|
|
912
|
+
try {
|
|
913
|
+
const res = await inspectTimelineRecent(bridge, {
|
|
914
|
+
kinds: ["alert", "actionsheet"],
|
|
915
|
+
limit: 80
|
|
916
|
+
});
|
|
917
|
+
events = res.events;
|
|
918
|
+
} catch {
|
|
919
|
+
return [];
|
|
920
|
+
}
|
|
921
|
+
const latest = /* @__PURE__ */ new Map();
|
|
922
|
+
for (const ev of events) latest.set(ev.kind, ev);
|
|
923
|
+
const out = [];
|
|
924
|
+
const check = (kind, label) => {
|
|
925
|
+
const ev = latest.get(kind);
|
|
926
|
+
if (!ev || !ev.data || typeof ev.data !== "object") return;
|
|
927
|
+
const d = ev.data;
|
|
928
|
+
if (d.phase !== "show") return;
|
|
929
|
+
out.push({ label, title: typeof d.title === "string" ? d.title : null });
|
|
930
|
+
};
|
|
931
|
+
check("alert", "Alert");
|
|
932
|
+
check("actionsheet", "ActionSheet");
|
|
933
|
+
return out;
|
|
934
|
+
}
|
|
935
|
+
async function inspectTimelineAdvanceCursor(bridge, cursorKey, watermark) {
|
|
936
|
+
await bridge.send({
|
|
937
|
+
type: "call",
|
|
938
|
+
path: "SootSim.bridges.timeline.cursorAdvance",
|
|
939
|
+
args: [cursorKey, watermark]
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
async function inspectKeyboard(bridge) {
|
|
943
|
+
const result = await bridge.send({
|
|
944
|
+
type: "evaluate",
|
|
945
|
+
code: `(() => {
|
|
946
|
+
const kb = window.__sootsimKeyboard
|
|
947
|
+
if (!kb || typeof kb.getLayout !== 'function') {
|
|
948
|
+
return { error: 'keyboard bridge getLayout() not available' }
|
|
949
|
+
}
|
|
950
|
+
return kb.getLayout()
|
|
951
|
+
})()`
|
|
952
|
+
});
|
|
953
|
+
return result ?? { error: "keyboard bridge returned no result" };
|
|
954
|
+
}
|
|
955
|
+
function isShellCommandUnavailable(error) {
|
|
956
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
957
|
+
return message.includes("call target not found: SootSim.bridges.mainShell") || message.includes("test bridge unavailable before app-in-worker boot");
|
|
958
|
+
}
|
|
959
|
+
async function getShellState(bridge, readyTimeoutMs = 0) {
|
|
960
|
+
const deadline = Date.now() + Math.max(0, readyTimeoutMs);
|
|
961
|
+
while (true) {
|
|
962
|
+
try {
|
|
963
|
+
return await bridge.send({
|
|
964
|
+
type: "call",
|
|
965
|
+
path: "SootSim.bridges.mainShell.getState",
|
|
966
|
+
args: []
|
|
967
|
+
});
|
|
968
|
+
} catch (error) {
|
|
969
|
+
if (!isShellCommandUnavailable(error) || Date.now() >= deadline) throw error;
|
|
970
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
async function inspectScreens(bridge) {
|
|
975
|
+
const payload = await bridge.send({
|
|
976
|
+
type: "evaluate",
|
|
977
|
+
code: `(async () => {
|
|
978
|
+
const test = window.__sootsimTest
|
|
979
|
+
const kb = window.__sootsimKeyboard
|
|
980
|
+
const navSnap =
|
|
981
|
+
test && typeof test.getNavigationSnapshot === 'function'
|
|
982
|
+
? await test.getNavigationSnapshot()
|
|
983
|
+
: null
|
|
984
|
+
const route =
|
|
985
|
+
test && typeof test.getRouteInfo === 'function'
|
|
986
|
+
? await test.getRouteInfo()
|
|
987
|
+
: null
|
|
988
|
+
const keyboard =
|
|
989
|
+
kb && typeof kb.getLayout === 'function'
|
|
990
|
+
? (() => {
|
|
991
|
+
const layout = kb.getLayout()
|
|
992
|
+
return layout ? {
|
|
993
|
+
visible: layout.visible,
|
|
994
|
+
mode: layout.mode,
|
|
995
|
+
spec: layout.spec
|
|
996
|
+
? {
|
|
997
|
+
keyboardType: layout.spec.keyboardType,
|
|
998
|
+
returnKeyType: layout.spec.returnKeyType,
|
|
999
|
+
}
|
|
1000
|
+
: null,
|
|
1001
|
+
} : null
|
|
1002
|
+
})()
|
|
1003
|
+
: null
|
|
1004
|
+
return { nav: navSnap, route, keyboard }
|
|
1005
|
+
})()`
|
|
1006
|
+
});
|
|
1007
|
+
const shell = await getShellState(bridge, 500).catch(() => null);
|
|
1008
|
+
return {
|
|
1009
|
+
shell,
|
|
1010
|
+
nav: payload?.nav ?? null,
|
|
1011
|
+
route: payload?.route ?? null,
|
|
1012
|
+
keyboard: payload?.keyboard ?? null
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
var DEBUG_CHANNELS = [
|
|
1016
|
+
"portals",
|
|
1017
|
+
"sheets",
|
|
1018
|
+
"layout",
|
|
1019
|
+
"onlayout",
|
|
1020
|
+
"animated",
|
|
1021
|
+
"render",
|
|
1022
|
+
"touch",
|
|
1023
|
+
"yoga"
|
|
1024
|
+
];
|
|
1025
|
+
async function inspectDebugStatus(bridge) {
|
|
1026
|
+
return bridge.send({ type: "evaluate", code: "window.__sootsimDebug.status()" });
|
|
1027
|
+
}
|
|
1028
|
+
async function inspectDebugFlags(bridge) {
|
|
1029
|
+
return bridge.send({ type: "evaluate", code: "window.__sootsimDebug.flags()" });
|
|
1030
|
+
}
|
|
1031
|
+
async function inspectDebugFind(bridge, target) {
|
|
1032
|
+
return bridge.send({
|
|
1033
|
+
type: "evaluate",
|
|
1034
|
+
code: `window.__sootsimTest.debugFind(${JSON.stringify(target)})`
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
async function callDebugBridge(bridge, expression, host = false) {
|
|
1038
|
+
const code = `globalThis.__sootsimDebug.${expression}`;
|
|
1039
|
+
if (host) return bridge.send({ type: "evaluate", code });
|
|
1040
|
+
return bridge.send({
|
|
1041
|
+
type: "evaluate",
|
|
1042
|
+
code: `(async () => {
|
|
1043
|
+
const result = await window.__sootsimTest.evalInTenant(${JSON.stringify(code)})
|
|
1044
|
+
if (!result.ok) throw new Error(result.error)
|
|
1045
|
+
return result.value
|
|
1046
|
+
})()`
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
async function inspectDebugRecent(bridge, channel, limit = 50, host = false) {
|
|
1050
|
+
return callDebugBridge(
|
|
1051
|
+
bridge,
|
|
1052
|
+
`recent(${channel && channel !== "all" ? JSON.stringify(channel) : "undefined"}, ${limit})`,
|
|
1053
|
+
host
|
|
1054
|
+
);
|
|
1055
|
+
}
|
|
1056
|
+
async function setDebugChannels(bridge, action, channels, host = false) {
|
|
1057
|
+
const args = channels.length > 0 ? channels.map((c) => JSON.stringify(c)).join(", ") : action === "disable" ? "'all'" : "";
|
|
1058
|
+
const expression = `${action}(${args})`;
|
|
1059
|
+
const active = await callDebugBridge(bridge, expression, true);
|
|
1060
|
+
if (host) return active;
|
|
1061
|
+
return callDebugBridge(bridge, expression);
|
|
1062
|
+
}
|
|
1063
|
+
async function inspectMemory(bridge) {
|
|
1064
|
+
if (bridge.plane === "cloud") {
|
|
1065
|
+
const value = await bridge.send({ type: "memory" });
|
|
1066
|
+
const rawWasmHeapBytes = typeof value === "object" && value !== null ? Reflect.get(value, "wasmHeapBytes") : null;
|
|
1067
|
+
const wasmHeapBytes = typeof rawWasmHeapBytes === "number" ? rawWasmHeapBytes : null;
|
|
1068
|
+
return {
|
|
1069
|
+
tenant: {
|
|
1070
|
+
nodesRegistered: null,
|
|
1071
|
+
nodesDetached: null,
|
|
1072
|
+
detachedTypes: null,
|
|
1073
|
+
objects: null,
|
|
1074
|
+
activeNativeAnimations: null,
|
|
1075
|
+
imageLoader: null,
|
|
1076
|
+
workerHeap: null,
|
|
1077
|
+
wasmHeapBytes
|
|
1078
|
+
},
|
|
1079
|
+
shell: null,
|
|
1080
|
+
compositor: null,
|
|
1081
|
+
hostHeap: null
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
const result = await bridge.send({
|
|
1085
|
+
type: "evaluate",
|
|
1086
|
+
code: `(async () => {
|
|
1087
|
+
const host = window.__sootsimRenderHost
|
|
1088
|
+
const stats = host?.queryStats ? await host.queryStats() : null
|
|
1089
|
+
const shellRaw = window.__sootsimShellMemory
|
|
1090
|
+
? await window.__sootsimShellMemory()
|
|
1091
|
+
: null
|
|
1092
|
+
const shell = shellRaw && !shellRaw.error ? shellRaw : null
|
|
1093
|
+
// the compositor paints home + app:one + app:two, so every app-surface
|
|
1094
|
+
// picture and raster image is counted here and nowhere else.
|
|
1095
|
+
let compositor = null
|
|
1096
|
+
try {
|
|
1097
|
+
const cs = await window.__sootsimCompositor?.getStats?.(false, true)
|
|
1098
|
+
compositor = cs?.memory ?? null
|
|
1099
|
+
} catch {}
|
|
1100
|
+
const normalize = (m) =>
|
|
1101
|
+
m
|
|
1102
|
+
? {
|
|
1103
|
+
nodesRegistered: m.nodesRegistered ?? null,
|
|
1104
|
+
nodesDetached: m.nodesDetached ?? null,
|
|
1105
|
+
detachedTypes: m.detachedTypes ?? null,
|
|
1106
|
+
objects: m.objects ?? null,
|
|
1107
|
+
activeNativeAnimations: m.activeNativeAnimations ?? null,
|
|
1108
|
+
imageLoader: m.imageLoader ?? null,
|
|
1109
|
+
workerHeap: m.workerHeap ?? null,
|
|
1110
|
+
wasmHeapBytes: m.wasmHeapBytes ?? null,
|
|
1111
|
+
}
|
|
1112
|
+
: null
|
|
1113
|
+
const hostMem = performance.memory
|
|
1114
|
+
? {
|
|
1115
|
+
usedJSHeapSize: performance.memory.usedJSHeapSize,
|
|
1116
|
+
totalJSHeapSize: performance.memory.totalJSHeapSize,
|
|
1117
|
+
jsHeapSizeLimit: performance.memory.jsHeapSizeLimit,
|
|
1118
|
+
}
|
|
1119
|
+
: null
|
|
1120
|
+
return {
|
|
1121
|
+
tenant: normalize(stats?.memory),
|
|
1122
|
+
shell: normalize(shell),
|
|
1123
|
+
compositor: normalize(compositor),
|
|
1124
|
+
hostHeap: hostMem,
|
|
1125
|
+
}
|
|
1126
|
+
})()`
|
|
1127
|
+
});
|
|
1128
|
+
return result ?? { tenant: null, shell: null, compositor: null, hostHeap: null };
|
|
1129
|
+
}
|
|
1130
|
+
function formatTimelineSummary(summary) {
|
|
1131
|
+
if (summary.total === 0) return "nothing recorded";
|
|
1132
|
+
const parts = [];
|
|
1133
|
+
const ORDER = [
|
|
1134
|
+
"error",
|
|
1135
|
+
"warning",
|
|
1136
|
+
"console",
|
|
1137
|
+
"fetch",
|
|
1138
|
+
"toast",
|
|
1139
|
+
"alert",
|
|
1140
|
+
"actionsheet",
|
|
1141
|
+
"picker",
|
|
1142
|
+
"notification",
|
|
1143
|
+
"screen",
|
|
1144
|
+
"route",
|
|
1145
|
+
"keyboard",
|
|
1146
|
+
"app-launch",
|
|
1147
|
+
"shell",
|
|
1148
|
+
"scroll",
|
|
1149
|
+
"gesture",
|
|
1150
|
+
"text-input",
|
|
1151
|
+
"react-commit",
|
|
1152
|
+
"animation",
|
|
1153
|
+
"reanimated"
|
|
1154
|
+
];
|
|
1155
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1156
|
+
for (const k of ORDER) {
|
|
1157
|
+
const n = summary.byKind[k];
|
|
1158
|
+
if (n) {
|
|
1159
|
+
parts.push(`${n} ${k}${n === 1 ? "" : "s"}`);
|
|
1160
|
+
seen.add(k);
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
for (const [k, n] of Object.entries(summary.byKind)) {
|
|
1164
|
+
if (!seen.has(k) && n) parts.push(`${n} ${k}${n === 1 ? "" : "s"}`);
|
|
1165
|
+
}
|
|
1166
|
+
return parts.join(" \xB7 ");
|
|
1167
|
+
}
|
|
1168
|
+
var NOISY_TIMELINE_KINDS = /* @__PURE__ */ new Set([
|
|
1169
|
+
"react-commit",
|
|
1170
|
+
"layout",
|
|
1171
|
+
"scroll"
|
|
1172
|
+
]);
|
|
1173
|
+
function formatTimelineRelativeTime(t, anchor) {
|
|
1174
|
+
if (anchor === null) return new Date(t).toLocaleTimeString();
|
|
1175
|
+
const dt = (t - anchor) / 1e3;
|
|
1176
|
+
const sign = dt >= 0 ? "+" : "";
|
|
1177
|
+
return `${sign}${dt.toFixed(2)}s`;
|
|
1178
|
+
}
|
|
1179
|
+
function formatTimelinePayload(kind, d) {
|
|
1180
|
+
switch (kind) {
|
|
1181
|
+
case "app-launch":
|
|
1182
|
+
return d.phase === "launch" ? `launch ${d.appName ?? d.toAppId ?? ""}` : `dismiss ${d.appName ?? d.fromAppId ?? ""} \u2192 ${d.toAppId ?? ""}`;
|
|
1183
|
+
case "toast":
|
|
1184
|
+
return `"${d.text ?? ""}"${d.durationMs ? ` (${d.durationMs}ms)` : ""}`;
|
|
1185
|
+
case "keyboard":
|
|
1186
|
+
return `${d.phase ?? "?"}${d.heightPx ? ` h=${d.heightPx}` : ""}${d.mode ? ` ${d.mode}` : ""}`;
|
|
1187
|
+
case "screen":
|
|
1188
|
+
return `${d.phase ?? "?"} ${d.name ?? d.activeName ?? ""}`;
|
|
1189
|
+
case "flow-step":
|
|
1190
|
+
return `${d.status ?? "?"} ${d.stepName ?? "step"}${d.durationMs ? ` ${d.durationMs}ms` : ""}`;
|
|
1191
|
+
case "route":
|
|
1192
|
+
return `${d.phase ?? "?"} ${d.path ?? d.pathname ?? ""}`;
|
|
1193
|
+
case "alert":
|
|
1194
|
+
case "actionsheet":
|
|
1195
|
+
case "picker":
|
|
1196
|
+
return `${d.phase ?? "?"} ${d.title ?? d.message ?? ""}`;
|
|
1197
|
+
case "notification":
|
|
1198
|
+
return `${d.title ?? ""}${d.body ? ` \u2014 ${d.body}` : ""}`;
|
|
1199
|
+
case "fetch":
|
|
1200
|
+
return `${d.method ?? "GET"} ${d.url ?? ""}${d.status ? ` -> ${d.status}` : ""}`;
|
|
1201
|
+
case "console":
|
|
1202
|
+
case "console-log":
|
|
1203
|
+
return `${d.level ?? "log"}: ${(d.message ?? "").toString().slice(0, 120)}`;
|
|
1204
|
+
case "shell":
|
|
1205
|
+
return `${d.event ?? d.type ?? d.phase ?? ""}`;
|
|
1206
|
+
case "scroll":
|
|
1207
|
+
return `${d.phase ?? "?"} ${d.target ?? ""}`;
|
|
1208
|
+
case "gesture":
|
|
1209
|
+
return `${d.phase ?? "?"} ${d.type ?? ""}`;
|
|
1210
|
+
case "text-input":
|
|
1211
|
+
return `${d.phase ?? "?"}${d.value !== void 0 ? ` "${String(d.value).slice(0, 40)}"` : ""}`;
|
|
1212
|
+
case "layout":
|
|
1213
|
+
return `${d.kind ?? "?"} ${d.testID ?? d.type ?? ""}${d.skipped ? ` skipped:${d.reason ?? "unknown"}` : ""}`;
|
|
1214
|
+
case "react-commit": {
|
|
1215
|
+
const slowest = d.slowest;
|
|
1216
|
+
return `${d.fiberCount ?? "?"} fibers ${d.durationMs ?? "?"}ms${slowest?.displayName ? ` \xB7 ${slowest.displayName} ${slowest.durationMs ?? "?"}ms` : ""}`;
|
|
1217
|
+
}
|
|
1218
|
+
case "frame":
|
|
1219
|
+
return `${d.totalMs ?? "?"}ms${d.renderMs ? ` \xB7 render ${d.renderMs}ms` : ""}${d.layoutMs ? ` \xB7 layout ${d.layoutMs}ms` : ""}${d.copyMs ? ` \xB7 copy ${d.copyMs}ms` : ""}`;
|
|
1220
|
+
case "reanimated":
|
|
1221
|
+
case "animation":
|
|
1222
|
+
return `${d.kind ?? ""} ${d.target ?? ""}${d.durationMs ? ` ${d.durationMs}ms` : ""}`;
|
|
1223
|
+
}
|
|
1224
|
+
return "";
|
|
1225
|
+
}
|
|
1226
|
+
function formatTimelineEvent(event, anchor) {
|
|
1227
|
+
const ts = formatTimelineRelativeTime(event.t, anchor).padStart(8);
|
|
1228
|
+
const ctx = event.context.padEnd(6);
|
|
1229
|
+
const kind = `[${event.kind}]`.padEnd(15);
|
|
1230
|
+
const d = event.data;
|
|
1231
|
+
const payload = d && typeof d === "object" ? formatTimelinePayload(event.kind, d) : "";
|
|
1232
|
+
return ` ${ts} ${ctx} ${kind} ${payload}`;
|
|
1233
|
+
}
|
|
1234
|
+
function formatTimelineEvents(events) {
|
|
1235
|
+
if (!events.length) return "";
|
|
1236
|
+
const anchor = events[0]?.t ?? null;
|
|
1237
|
+
return events.map((e) => formatTimelineEvent(e, anchor)).join("\n");
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
// cli/commands/inspect/resolve-target.ts
|
|
1241
|
+
async function resolveTargetCoords(bridge, target) {
|
|
1242
|
+
const result = await bridge.send({
|
|
1243
|
+
type: "resolve",
|
|
1244
|
+
selector: target.mode === "testid" ? { testID: target.value } : { text: target.value }
|
|
1245
|
+
});
|
|
1246
|
+
if (result === null) return null;
|
|
1247
|
+
if (typeof result !== "object") throw new Error("resolve returned an invalid result");
|
|
1248
|
+
const match = Reflect.get(result, "match");
|
|
1249
|
+
const resolvedTarget = Reflect.get(result, "target");
|
|
1250
|
+
const semanticNodes = [match, resolvedTarget];
|
|
1251
|
+
const point = Reflect.get(result, "point");
|
|
1252
|
+
const x = point !== null && typeof point === "object" ? Reflect.get(point, "x") : void 0;
|
|
1253
|
+
const y = point !== null && typeof point === "object" ? Reflect.get(point, "y") : void 0;
|
|
1254
|
+
if (!isSimSemanticTree(semanticNodes) || typeof x !== "number" || !Number.isFinite(x) || typeof y !== "number" || !Number.isFinite(y)) {
|
|
1255
|
+
throw new Error("resolve returned an invalid result");
|
|
1256
|
+
}
|
|
1257
|
+
const semanticMatch = semanticNodes[1];
|
|
1258
|
+
if (!semanticMatch) throw new Error("resolve returned an invalid result");
|
|
1259
|
+
return {
|
|
1260
|
+
x,
|
|
1261
|
+
y,
|
|
1262
|
+
nodeId: semanticMatch.nodeId,
|
|
1263
|
+
testID: semanticMatch.testID ?? null,
|
|
1264
|
+
text: semanticMatch.text ?? (target.mode === "text" ? target.value : semanticMatch.label),
|
|
1265
|
+
type: semanticMatch.type
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
// cli/commands/inspect/settling.ts
|
|
1270
|
+
async function waitForSootsimIdle({
|
|
1271
|
+
bridge,
|
|
1272
|
+
simId,
|
|
1273
|
+
maxMs,
|
|
1274
|
+
pollMs = 50,
|
|
1275
|
+
stablePolls = 3,
|
|
1276
|
+
strict = false
|
|
1277
|
+
}) {
|
|
1278
|
+
const result = await bridge.send(
|
|
1279
|
+
{
|
|
1280
|
+
type: "evaluate",
|
|
1281
|
+
simId,
|
|
1282
|
+
code: `(async () => {
|
|
1283
|
+
const start = Date.now()
|
|
1284
|
+
const deadline = start + ${Math.max(0, Math.round(maxMs))}
|
|
1285
|
+
const pollMs = ${Math.max(1, Math.round(pollMs))}
|
|
1286
|
+
const requiredStablePolls = ${Math.max(1, Math.round(stablePolls))}
|
|
1287
|
+
const strict = ${strict ? "true" : "false"}
|
|
1288
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
1289
|
+
|
|
1290
|
+
// route-aware settle: a tap that pushes/pops a screen is async \u2014 the
|
|
1291
|
+
// old screen still renders for a moment, then the new one mounts and
|
|
1292
|
+
// its content loads. a pure layout-hash check reports "idle" on the
|
|
1293
|
+
// outgoing screen or on the incoming skeleton, so a driver re-taps
|
|
1294
|
+
// and stacks duplicate navigations. drain any in-flight screen
|
|
1295
|
+
// transition FIRST, bounded by the overall budget. when no
|
|
1296
|
+
// transition is happening this returns in ~80ms (startWindowMs), so
|
|
1297
|
+
// a plain button tap barely pays for it.
|
|
1298
|
+
try {
|
|
1299
|
+
const wfst = window.__sootsimTest?.waitForScreenTransitions
|
|
1300
|
+
if (typeof wfst === 'function') {
|
|
1301
|
+
const remaining = deadline - Date.now()
|
|
1302
|
+
if (remaining > 120) {
|
|
1303
|
+
await wfst({
|
|
1304
|
+
timeoutMs: Math.min(remaining - 80, 4000),
|
|
1305
|
+
settleMs: 64,
|
|
1306
|
+
startWindowMs: 80,
|
|
1307
|
+
})
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
} catch {}
|
|
1311
|
+
|
|
1312
|
+
const readSnapshot = async () => {
|
|
1313
|
+
let animating = false
|
|
1314
|
+
let layoutDirty = false
|
|
1315
|
+
let pendingFetches = 0
|
|
1316
|
+
let requestTotal = 0
|
|
1317
|
+
let requestInFlight = 0
|
|
1318
|
+
let renderStatsAvailable = false
|
|
1319
|
+
try {
|
|
1320
|
+
const stats = await window.__sootsimRenderHost?.queryStats?.()
|
|
1321
|
+
if (stats) {
|
|
1322
|
+
renderStatsAvailable = true
|
|
1323
|
+
animating =
|
|
1324
|
+
stats.hasActiveAnims === true ||
|
|
1325
|
+
stats.hasActiveNativeAnimations === true ||
|
|
1326
|
+
stats.hasPendingAnimationFrames === true
|
|
1327
|
+
layoutDirty =
|
|
1328
|
+
stats.layoutDirty === true && stats.renderRequested === true
|
|
1329
|
+
// a freshly-pushed screen showing a skeleton is layout-stable
|
|
1330
|
+
// but not actually settled \u2014 its data/images are still in
|
|
1331
|
+
// flight. treat bounded image-loader fetches as not-idle so
|
|
1332
|
+
// settle waits for real content, capped by the budget.
|
|
1333
|
+
const pf = stats.memory && stats.memory.imageLoader
|
|
1334
|
+
? stats.memory.imageLoader.pendingFetches
|
|
1335
|
+
: 0
|
|
1336
|
+
pendingFetches = typeof pf === 'number' ? pf : 0
|
|
1337
|
+
}
|
|
1338
|
+
} catch {}
|
|
1339
|
+
try {
|
|
1340
|
+
const counts = await window.__sootsimTest?.getRequestCounts?.()
|
|
1341
|
+
requestTotal = typeof counts?.total === 'number' ? counts.total : 0
|
|
1342
|
+
requestInFlight = typeof counts?.inFlight === 'number' ? counts.inFlight : 0
|
|
1343
|
+
} catch {}
|
|
1344
|
+
const root = window.__sootsimRoot
|
|
1345
|
+
const nodes = []
|
|
1346
|
+
if (root) {
|
|
1347
|
+
const walk = (n) => {
|
|
1348
|
+
if (n.layout && n.layout.width > 0) {
|
|
1349
|
+
nodes.push(Math.round(n.layout.x) + ',' + Math.round(n.layout.y) + ',' + Math.round(n.layout.width))
|
|
1350
|
+
}
|
|
1351
|
+
for (const c of n.children || []) walk(c)
|
|
1352
|
+
}
|
|
1353
|
+
walk(root)
|
|
1354
|
+
}
|
|
1355
|
+
return { layout: nodes.join(';'), animating, layoutDirty, pendingFetches, renderStatsAvailable, requestTotal, requestInFlight }
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
// how long generic background network is allowed to keep us waiting AFTER
|
|
1359
|
+
// the screen is otherwise visually settled. apps with continuous traffic
|
|
1360
|
+
// (polling, realtime sockets, a tap that fires 20-50 fetches) never reach
|
|
1361
|
+
// requestInFlight===0 / a stable requestTotal, so requiring full network
|
|
1362
|
+
// quiet burns the ENTIRE budget on every command \u2014 the leading idle that
|
|
1363
|
+
// failed PR-preview recordings for network-heavy apps (3pc fight-pulse).
|
|
1364
|
+
// layout-stability + the image-loader already prove visible content
|
|
1365
|
+
// loaded (a data load changes layout; an image load shows in the loader),
|
|
1366
|
+
// so the generic request counters are a courtesy, not a gate: honor them
|
|
1367
|
+
// only within this grace once the screen is visually still.
|
|
1368
|
+
const networkQuietGraceMs = 1200
|
|
1369
|
+
let lastLayout = ''
|
|
1370
|
+
let lastRequestTotal = -1
|
|
1371
|
+
let stable = 0
|
|
1372
|
+
let visuallyStillSince = 0
|
|
1373
|
+
// counts across the whole budget: a flag that is true on every poll is a
|
|
1374
|
+
// different bug from one that is true intermittently, and the two need
|
|
1375
|
+
// opposite fixes (engine vs budget).
|
|
1376
|
+
let polls = 0
|
|
1377
|
+
let dirtyPolls = 0
|
|
1378
|
+
let animatingPolls = 0
|
|
1379
|
+
let layoutChangedPolls = 0
|
|
1380
|
+
while (Date.now() < deadline) {
|
|
1381
|
+
const snapshot = await readSnapshot()
|
|
1382
|
+
polls++
|
|
1383
|
+
if (snapshot.layoutDirty) dirtyPolls++
|
|
1384
|
+
if (snapshot.animating) animatingPolls++
|
|
1385
|
+
if (polls > 1 && snapshot.layout !== lastLayout) layoutChangedPolls++
|
|
1386
|
+
const strictOk =
|
|
1387
|
+
!strict ||
|
|
1388
|
+
(snapshot.renderStatsAvailable && !snapshot.animating && !snapshot.layoutDirty)
|
|
1389
|
+
// visually settled: no animations, no image content loading, layout
|
|
1390
|
+
// unchanged since the last poll.
|
|
1391
|
+
const visuallyStill =
|
|
1392
|
+
strictOk && snapshot.pendingFetches === 0 && snapshot.layout === lastLayout
|
|
1393
|
+
// generic network quiet: nothing in flight and no new requests issued.
|
|
1394
|
+
const networkQuiet =
|
|
1395
|
+
snapshot.requestInFlight === 0 && snapshot.requestTotal === lastRequestTotal
|
|
1396
|
+
if (visuallyStill) {
|
|
1397
|
+
stable++
|
|
1398
|
+
if (visuallyStillSince === 0) visuallyStillSince = Date.now()
|
|
1399
|
+
// settle once the screen has held still long enough AND either the
|
|
1400
|
+
// network is genuinely quiet OR it has refused to quiet within the
|
|
1401
|
+
// grace (continuous background traffic must not block forever).
|
|
1402
|
+
if (
|
|
1403
|
+
stable >= requiredStablePolls &&
|
|
1404
|
+
(networkQuiet || Date.now() - visuallyStillSince >= networkQuietGraceMs)
|
|
1405
|
+
) {
|
|
1406
|
+
return { settled: true, elapsed: Date.now() - start, blockedBy: '' }
|
|
1407
|
+
}
|
|
1408
|
+
} else {
|
|
1409
|
+
stable = 0
|
|
1410
|
+
visuallyStillSince = 0
|
|
1411
|
+
}
|
|
1412
|
+
lastLayout = snapshot.layout
|
|
1413
|
+
lastRequestTotal = snapshot.requestTotal
|
|
1414
|
+
await sleep(pollMs)
|
|
1415
|
+
}
|
|
1416
|
+
const last = await readSnapshot()
|
|
1417
|
+
const blockers = []
|
|
1418
|
+
if (strict && !last.renderStatsAvailable) blockers.push('render stats unavailable')
|
|
1419
|
+
if (last.animating) blockers.push('animations still running')
|
|
1420
|
+
if (last.layoutDirty) blockers.push('layout still dirty')
|
|
1421
|
+
if (last.pendingFetches > 0) blockers.push(last.pendingFetches + ' image fetches pending')
|
|
1422
|
+
if (last.layout !== lastLayout) blockers.push('layout still changing')
|
|
1423
|
+
if (last.requestInFlight > 0) blockers.push(last.requestInFlight + ' requests in flight')
|
|
1424
|
+
return {
|
|
1425
|
+
settled: false,
|
|
1426
|
+
elapsed: Date.now() - start,
|
|
1427
|
+
blockedBy:
|
|
1428
|
+
(blockers.length ? blockers.join(', ') : 'unknown') +
|
|
1429
|
+
' over ' + polls + ' polls (layout changed ' + layoutChangedPolls +
|
|
1430
|
+
'x, layoutDirty ' + dirtyPolls + 'x, animating ' + animatingPolls + 'x)',
|
|
1431
|
+
}
|
|
1432
|
+
})()`
|
|
1433
|
+
},
|
|
1434
|
+
{
|
|
1435
|
+
timeoutMs: maxMs + 1e3
|
|
1436
|
+
}
|
|
1437
|
+
);
|
|
1438
|
+
const { elapsed, settled, blockedBy } = result ?? {};
|
|
1439
|
+
return {
|
|
1440
|
+
elapsed: typeof elapsed === "number" ? elapsed : maxMs,
|
|
1441
|
+
settled: settled === true,
|
|
1442
|
+
blockedBy: typeof blockedBy === "string" ? blockedBy : "unknown"
|
|
1443
|
+
};
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
// cli/commands/inspect/actions.ts
|
|
1447
|
+
function isScreenSize(value) {
|
|
1448
|
+
if (value === null || typeof value !== "object") return false;
|
|
1449
|
+
const width = Reflect.get(value, "width");
|
|
1450
|
+
const height = Reflect.get(value, "height");
|
|
1451
|
+
return typeof width === "number" && Number.isFinite(width) && width > 0 && typeof height === "number" && Number.isFinite(height) && height > 0;
|
|
1452
|
+
}
|
|
1453
|
+
function viewportFromCapture(value) {
|
|
1454
|
+
if (value === null || typeof value !== "object") return void 0;
|
|
1455
|
+
const tree = Reflect.get(value, "tree");
|
|
1456
|
+
return tree !== null && typeof tree === "object" ? Reflect.get(tree, "viewport") : void 0;
|
|
1457
|
+
}
|
|
1458
|
+
async function readScreen(bridge) {
|
|
1459
|
+
try {
|
|
1460
|
+
const viewport = viewportFromCapture(
|
|
1461
|
+
await bridge.send({ type: "capture", includeVisual: false })
|
|
1462
|
+
);
|
|
1463
|
+
return isScreenSize(viewport) ? { width: viewport.width, height: viewport.height } : null;
|
|
1464
|
+
} catch {
|
|
1465
|
+
return null;
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
function isOffscreen(cx, cy, screen) {
|
|
1469
|
+
return !(cx >= 0 && cy >= 0 && cx <= screen.width && cy <= screen.height);
|
|
1470
|
+
}
|
|
1471
|
+
var DEFAULT_AGENT_TIMING = {
|
|
1472
|
+
initialWaitMs: 3e3,
|
|
1473
|
+
deadlineMs: 5e3,
|
|
1474
|
+
retryWaitMs: 700
|
|
1475
|
+
};
|
|
1476
|
+
var DEFAULT_INTERACTIVE_TIMING = {
|
|
1477
|
+
initialWaitMs: 1200,
|
|
1478
|
+
deadlineMs: 2500,
|
|
1479
|
+
retryWaitMs: 700
|
|
1480
|
+
};
|
|
1481
|
+
function tapTiming(agent, opts = {}) {
|
|
1482
|
+
return {
|
|
1483
|
+
...agent ? DEFAULT_AGENT_TIMING : DEFAULT_INTERACTIVE_TIMING,
|
|
1484
|
+
...opts
|
|
1485
|
+
};
|
|
1486
|
+
}
|
|
1487
|
+
function isTapSuccess(result) {
|
|
1488
|
+
if (!result || result.hit === false || result.ok === false) return false;
|
|
1489
|
+
if (result.handled === false) return false;
|
|
1490
|
+
if (result.requestedTargetMatched === false) return false;
|
|
1491
|
+
if (result.pointerTapHandled === false && !result.keyboardOpened && !result.isTextInput) {
|
|
1492
|
+
return false;
|
|
1493
|
+
}
|
|
1494
|
+
return true;
|
|
1495
|
+
}
|
|
1496
|
+
function tapTargetFromPayload(payload, textFallback) {
|
|
1497
|
+
const id = payload.target?.id ?? payload.match?.id ?? payload.node?.id;
|
|
1498
|
+
return {
|
|
1499
|
+
nodeId: payload.target?.nodeId ?? payload.match?.nodeId ?? payload.node?.nodeId ?? null,
|
|
1500
|
+
id: typeof id === "string" ? id : null,
|
|
1501
|
+
testID: payload.target?.testID ?? payload.match?.testID ?? payload.node?.testID ?? null,
|
|
1502
|
+
text: payload.target?.text ?? payload.target?.accessibilityLabel ?? payload.match?.text ?? payload.match?.accessibilityLabel ?? payload.node?.text ?? textFallback ?? null,
|
|
1503
|
+
type: payload.target?.type ?? payload.match?.type ?? payload.node?.type ?? null
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
async function tapResolvedTarget(bridge, args) {
|
|
1507
|
+
const timing = tapTiming(!!args.agent, args.timing);
|
|
1508
|
+
let attempts = 0;
|
|
1509
|
+
let lastPayload = null;
|
|
1510
|
+
let lastResult = null;
|
|
1511
|
+
const settleCloud = async () => {
|
|
1512
|
+
try {
|
|
1513
|
+
await bridge.send({ type: "settle" });
|
|
1514
|
+
} catch {
|
|
1515
|
+
}
|
|
1516
|
+
};
|
|
1517
|
+
if (bridge.plane === "cloud") {
|
|
1518
|
+
await settleCloud();
|
|
1519
|
+
} else if (timing.initialWaitMs > 0) {
|
|
1520
|
+
try {
|
|
1521
|
+
await waitForSootsimIdle({
|
|
1522
|
+
bridge,
|
|
1523
|
+
maxMs: timing.initialWaitMs,
|
|
1524
|
+
pollMs: 32,
|
|
1525
|
+
stablePolls: 2
|
|
1526
|
+
});
|
|
1527
|
+
} catch {
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
const deadline = Date.now() + timing.deadlineMs;
|
|
1531
|
+
let lastOffscreenAt = null;
|
|
1532
|
+
const screen = bridge.plane === "cloud" ? null : await readScreen(bridge);
|
|
1533
|
+
while (Date.now() <= deadline || attempts === 0) {
|
|
1534
|
+
attempts++;
|
|
1535
|
+
const resolved = await args.resolve();
|
|
1536
|
+
const payload = resolved && typeof resolved.cx === "number" && typeof resolved.cy === "number" && screen && isOffscreen(resolved.cx, resolved.cy, screen) ? { ...resolved, offscreen: true, screen } : resolved;
|
|
1537
|
+
lastPayload = payload;
|
|
1538
|
+
if (payload?.error === "bridge-not-ready" || payload?.ambiguous || payload?.nthOutOfRange) {
|
|
1539
|
+
return { payload, result: null, attempts, failure: "special" };
|
|
1540
|
+
}
|
|
1541
|
+
if (payload && typeof payload.cx === "number" && typeof payload.cy === "number") {
|
|
1542
|
+
if (payload.offscreen) {
|
|
1543
|
+
lastResult = {
|
|
1544
|
+
hit: false,
|
|
1545
|
+
reason: "offscreen",
|
|
1546
|
+
x: payload.cx,
|
|
1547
|
+
y: payload.cy,
|
|
1548
|
+
screen: payload.screen
|
|
1549
|
+
};
|
|
1550
|
+
const at = `${Math.round(payload.cx)},${Math.round(payload.cy)}`;
|
|
1551
|
+
if (lastOffscreenAt === at) {
|
|
1552
|
+
return { payload, result: lastResult, attempts, failure: "missed" };
|
|
1553
|
+
}
|
|
1554
|
+
lastOffscreenAt = at;
|
|
1555
|
+
} else {
|
|
1556
|
+
lastOffscreenAt = null;
|
|
1557
|
+
const requestedTarget = tapTargetFromPayload(payload, args.textFallback);
|
|
1558
|
+
const result = await tapCoordinates(
|
|
1559
|
+
bridge,
|
|
1560
|
+
payload.cx,
|
|
1561
|
+
payload.cy,
|
|
1562
|
+
requestedTarget
|
|
1563
|
+
);
|
|
1564
|
+
lastResult = result?.reason === "offscreen" && result.diagnostics ? {
|
|
1565
|
+
...result,
|
|
1566
|
+
hit: false,
|
|
1567
|
+
x: result.diagnostics.point?.x ?? payload.cx,
|
|
1568
|
+
y: result.diagnostics.point?.y ?? payload.cy,
|
|
1569
|
+
screen: result.diagnostics.screen
|
|
1570
|
+
} : result;
|
|
1571
|
+
if (result?.requestedTargetMatched === false) {
|
|
1572
|
+
return { payload, result, attempts, failure: "missed" };
|
|
1573
|
+
}
|
|
1574
|
+
if (isTapSuccess(result)) return { payload, result, attempts };
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
const remaining = deadline - Date.now();
|
|
1578
|
+
if (remaining <= 0) break;
|
|
1579
|
+
if (bridge.plane === "cloud") {
|
|
1580
|
+
if (lastResult?.reason !== "target-moved" || attempts >= 3) break;
|
|
1581
|
+
await settleCloud();
|
|
1582
|
+
continue;
|
|
1583
|
+
}
|
|
1584
|
+
if (timing.retryWaitMs <= 0) break;
|
|
1585
|
+
try {
|
|
1586
|
+
await waitForSootsimIdle({
|
|
1587
|
+
bridge,
|
|
1588
|
+
maxMs: Math.min(remaining, timing.retryWaitMs),
|
|
1589
|
+
pollMs: 32,
|
|
1590
|
+
stablePolls: 2
|
|
1591
|
+
});
|
|
1592
|
+
} catch {
|
|
1593
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(120, remaining)));
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
return {
|
|
1597
|
+
payload: lastPayload,
|
|
1598
|
+
result: lastResult,
|
|
1599
|
+
attempts,
|
|
1600
|
+
failure: lastPayload && typeof lastPayload.cx === "number" ? "missed" : "not-found"
|
|
1601
|
+
};
|
|
1602
|
+
}
|
|
1603
|
+
async function tapCoordinates(bridge, x, y, target) {
|
|
1604
|
+
return bridge.send({
|
|
1605
|
+
type: "tap",
|
|
1606
|
+
x,
|
|
1607
|
+
y,
|
|
1608
|
+
...target ? { target } : {}
|
|
1609
|
+
});
|
|
1610
|
+
}
|
|
1611
|
+
function payloadFromResolved(resolved, match, extra = {}) {
|
|
1612
|
+
return {
|
|
1613
|
+
cx: resolved.x,
|
|
1614
|
+
cy: resolved.y,
|
|
1615
|
+
match: {
|
|
1616
|
+
nodeId: match.nodeId ?? null,
|
|
1617
|
+
id: match.testID ?? null,
|
|
1618
|
+
testID: match.testID ?? null,
|
|
1619
|
+
text: match.text ?? null,
|
|
1620
|
+
type: match.type ?? null
|
|
1621
|
+
},
|
|
1622
|
+
target: {
|
|
1623
|
+
nodeId: resolved.nodeId ?? match.nodeId ?? null,
|
|
1624
|
+
id: resolved.testID ?? match.testID ?? null,
|
|
1625
|
+
testID: resolved.testID ?? match.testID ?? null,
|
|
1626
|
+
text: resolved.text ?? match.text ?? null,
|
|
1627
|
+
type: resolved.type ?? match.type ?? null
|
|
1628
|
+
},
|
|
1629
|
+
strategy: "matched-node",
|
|
1630
|
+
...extra
|
|
1631
|
+
};
|
|
1632
|
+
}
|
|
1633
|
+
function collectTextCandidates(nodes, ancestors = [], pressableOwnerText = "") {
|
|
1634
|
+
const out = [];
|
|
1635
|
+
for (const node of nodes) {
|
|
1636
|
+
const text = nodeText(node);
|
|
1637
|
+
const caption = !!text && text === pressableOwnerText && !node.pressable;
|
|
1638
|
+
if (!caption) out.push({ node, ancestorTestIDs: ancestors });
|
|
1639
|
+
if (node.children?.length) {
|
|
1640
|
+
const next = node.testID ? [...ancestors, node.testID] : ancestors;
|
|
1641
|
+
const owner = text ? node.pressable ? text : "" : pressableOwnerText;
|
|
1642
|
+
out.push(...collectTextCandidates(node.children, next, owner));
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
return out;
|
|
1646
|
+
}
|
|
1647
|
+
function nodeText(node) {
|
|
1648
|
+
return node.text ?? node.label ?? "";
|
|
1649
|
+
}
|
|
1650
|
+
async function tapById(bridge, query, opts = {}) {
|
|
1651
|
+
return tapResolvedTarget(bridge, {
|
|
1652
|
+
agent: opts.agent,
|
|
1653
|
+
timing: opts.timing,
|
|
1654
|
+
resolve: async () => {
|
|
1655
|
+
const resolved = await resolveTargetCoords(bridge, { mode: "testid", value: query });
|
|
1656
|
+
if (!resolved) return { cx: null };
|
|
1657
|
+
return payloadFromResolved(resolved, resolved);
|
|
1658
|
+
}
|
|
1659
|
+
});
|
|
1660
|
+
}
|
|
1661
|
+
async function tapByText(bridge, query, options = {}, opts = {}) {
|
|
1662
|
+
return tapResolvedTarget(bridge, {
|
|
1663
|
+
agent: opts.agent,
|
|
1664
|
+
timing: opts.timing,
|
|
1665
|
+
textFallback: query,
|
|
1666
|
+
resolve: async () => {
|
|
1667
|
+
const { tree } = await inspectTree(bridge, 50);
|
|
1668
|
+
if (!Array.isArray(tree)) return { matched: 0, total: 0 };
|
|
1669
|
+
const needle = options.exact ? query : query.toLowerCase();
|
|
1670
|
+
let candidates = collectTextCandidates(tree).filter(({ node: node2, ancestorTestIDs }) => {
|
|
1671
|
+
const text = nodeText(node2);
|
|
1672
|
+
if (!text) return false;
|
|
1673
|
+
if (options.exact ? text !== needle : !text.toLowerCase().includes(needle)) {
|
|
1674
|
+
return false;
|
|
1675
|
+
}
|
|
1676
|
+
if (options.role && node2.role !== options.role) return false;
|
|
1677
|
+
if (options.within && !ancestorTestIDs.includes(options.within)) return false;
|
|
1678
|
+
const ax = node2.geometry.x;
|
|
1679
|
+
const ay = node2.geometry.y;
|
|
1680
|
+
if (options.minX != null && !(ax >= options.minX)) return false;
|
|
1681
|
+
if (options.maxX != null && !(ax <= options.maxX)) return false;
|
|
1682
|
+
if (options.minY != null && !(ay >= options.minY)) return false;
|
|
1683
|
+
if (options.maxY != null && !(ay <= options.maxY)) return false;
|
|
1684
|
+
return true;
|
|
1685
|
+
});
|
|
1686
|
+
if (options.near) {
|
|
1687
|
+
const { x, y } = options.near;
|
|
1688
|
+
candidates.sort(
|
|
1689
|
+
(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)
|
|
1690
|
+
);
|
|
1691
|
+
} else {
|
|
1692
|
+
candidates.sort((a, b) => {
|
|
1693
|
+
if (Math.abs(a.node.geometry.y - b.node.geometry.y) > 2) {
|
|
1694
|
+
return a.node.geometry.y - b.node.geometry.y;
|
|
1695
|
+
}
|
|
1696
|
+
return a.node.geometry.x - b.node.geometry.x;
|
|
1697
|
+
});
|
|
1698
|
+
}
|
|
1699
|
+
const total = candidates.length;
|
|
1700
|
+
if (total === 0) return { matched: 0, total: 0 };
|
|
1701
|
+
let idx = 0;
|
|
1702
|
+
if (options.nth != null) {
|
|
1703
|
+
idx = options.nth < 0 ? total + options.nth : options.nth;
|
|
1704
|
+
if (idx < 0 || idx >= total) {
|
|
1705
|
+
return { matched: 0, total, nthOutOfRange: true, nth: options.nth };
|
|
1706
|
+
}
|
|
1707
|
+
} else if (total > 1 && !options.first && !options.near) {
|
|
1708
|
+
return {
|
|
1709
|
+
ambiguous: true,
|
|
1710
|
+
total,
|
|
1711
|
+
candidates: candidates.slice(0, 10).map((candidate, index) => ({
|
|
1712
|
+
idx: index,
|
|
1713
|
+
nodeId: candidate.node.nodeId,
|
|
1714
|
+
type: candidate.node.type,
|
|
1715
|
+
testID: candidate.node.testID ?? null,
|
|
1716
|
+
text: nodeText(candidate.node).slice(0, 60),
|
|
1717
|
+
abs: { x: candidate.node.geometry.x, y: candidate.node.geometry.y },
|
|
1718
|
+
layout: {
|
|
1719
|
+
width: Math.round(candidate.node.geometry.width),
|
|
1720
|
+
height: Math.round(candidate.node.geometry.height)
|
|
1721
|
+
},
|
|
1722
|
+
ancestorTestIDs: candidate.ancestorTestIDs.slice(0, 5)
|
|
1723
|
+
}))
|
|
1724
|
+
};
|
|
1725
|
+
}
|
|
1726
|
+
const picked = candidates[idx];
|
|
1727
|
+
if (!picked) return { matched: 0, total };
|
|
1728
|
+
const node = picked.node;
|
|
1729
|
+
let resolved = node.testID ? await resolveTargetCoords(bridge, { mode: "testid", value: node.testID }) : await resolveTargetCoords(bridge, {
|
|
1730
|
+
mode: "text",
|
|
1731
|
+
value: nodeText(node) || query
|
|
1732
|
+
});
|
|
1733
|
+
if (!resolved) {
|
|
1734
|
+
resolved = {
|
|
1735
|
+
x: node.geometry.x + node.geometry.width / 2,
|
|
1736
|
+
y: node.geometry.y + node.geometry.height / 2,
|
|
1737
|
+
testID: node.testID ?? null,
|
|
1738
|
+
text: nodeText(node) || query,
|
|
1739
|
+
type: node.type
|
|
1740
|
+
};
|
|
1741
|
+
}
|
|
1742
|
+
return payloadFromResolved(resolved, node, { total, idx });
|
|
1743
|
+
}
|
|
1744
|
+
});
|
|
1745
|
+
}
|
|
1746
|
+
async function tapBest(bridge, query, opts = {}) {
|
|
1747
|
+
return tapResolvedTarget(bridge, {
|
|
1748
|
+
agent: opts.agent,
|
|
1749
|
+
timing: opts.timing,
|
|
1750
|
+
textFallback: query,
|
|
1751
|
+
resolve: async () => {
|
|
1752
|
+
const byId = await resolveTargetCoords(bridge, { mode: "testid", value: query });
|
|
1753
|
+
if (byId) {
|
|
1754
|
+
return {
|
|
1755
|
+
...payloadFromResolved(byId, byId),
|
|
1756
|
+
strategy: "testid",
|
|
1757
|
+
node: {
|
|
1758
|
+
nodeId: byId.nodeId ?? null,
|
|
1759
|
+
id: byId.testID ?? query,
|
|
1760
|
+
testID: byId.testID ?? query,
|
|
1761
|
+
type: byId.type ?? null,
|
|
1762
|
+
text: byId.text ?? null
|
|
1763
|
+
}
|
|
1764
|
+
};
|
|
1765
|
+
}
|
|
1766
|
+
const byText = await resolveTargetCoords(bridge, { mode: "text", value: query });
|
|
1767
|
+
if (byText) {
|
|
1768
|
+
return {
|
|
1769
|
+
...payloadFromResolved(byText, byText),
|
|
1770
|
+
strategy: "text",
|
|
1771
|
+
node: {
|
|
1772
|
+
nodeId: byText.nodeId ?? null,
|
|
1773
|
+
id: byText.testID ?? null,
|
|
1774
|
+
testID: byText.testID ?? null,
|
|
1775
|
+
type: byText.type ?? null,
|
|
1776
|
+
text: byText.text ?? query
|
|
1777
|
+
}
|
|
1778
|
+
};
|
|
1779
|
+
}
|
|
1780
|
+
return { strategy: "none" };
|
|
1781
|
+
}
|
|
1782
|
+
});
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
// src/app-url.ts
|
|
1786
|
+
var APP_URL_SCHEME_RE = /^[A-Za-z][A-Za-z0-9+.-]*:/;
|
|
1787
|
+
function normalizeAppUrlTarget(target) {
|
|
1788
|
+
const trimmed = target.trim();
|
|
1789
|
+
if (!trimmed) {
|
|
1790
|
+
throw new Error("expected <url-or-route>");
|
|
1791
|
+
}
|
|
1792
|
+
if (APP_URL_SCHEME_RE.test(trimmed) || trimmed.startsWith("/")) {
|
|
1793
|
+
return trimmed;
|
|
1794
|
+
}
|
|
1795
|
+
return `/${trimmed}`;
|
|
1796
|
+
}
|
|
1797
|
+
async function openAppUrl(bridge, target) {
|
|
1798
|
+
const normalized = normalizeAppUrlTarget(target);
|
|
1799
|
+
const result = await bridge.send({
|
|
1800
|
+
type: "openUrl",
|
|
1801
|
+
url: normalized
|
|
1802
|
+
});
|
|
1803
|
+
if (result !== null && typeof result === "object" && Reflect.get(result, "ok") === false) {
|
|
1804
|
+
const error = Reflect.get(result, "error");
|
|
1805
|
+
throw new Error(typeof error === "string" ? error : "deep-link dispatch failed");
|
|
1806
|
+
}
|
|
1807
|
+
return normalized;
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
// src/sim-client.ts
|
|
1811
|
+
var SimClientError = class extends Error {
|
|
1812
|
+
constructor(message) {
|
|
1813
|
+
super(message);
|
|
1814
|
+
this.name = new.target.name;
|
|
1815
|
+
}
|
|
1816
|
+
};
|
|
1817
|
+
var SimConnectError = class extends SimClientError {
|
|
1818
|
+
};
|
|
1819
|
+
var SimReadyTimeoutError = class extends SimClientError {
|
|
1820
|
+
constructor(status) {
|
|
1821
|
+
super(`sim not ready: ${waitReadyReason(status)}`);
|
|
1822
|
+
__publicField(this, "status");
|
|
1823
|
+
this.status = status;
|
|
1824
|
+
}
|
|
1825
|
+
};
|
|
1826
|
+
var SimTapError = class extends SimClientError {
|
|
1827
|
+
constructor(message, outcome = null) {
|
|
1828
|
+
super(message);
|
|
1829
|
+
__publicField(this, "outcome");
|
|
1830
|
+
this.outcome = outcome;
|
|
1831
|
+
}
|
|
1832
|
+
};
|
|
1833
|
+
var SimKeyboardError = class extends SimClientError {
|
|
1834
|
+
};
|
|
1835
|
+
var SimPerformError = class extends SimClientError {
|
|
1836
|
+
constructor(result) {
|
|
1837
|
+
const failed = result.steps.find((step) => !step.ok);
|
|
1838
|
+
const at = failed ? `${failed.index} (${failed.type})` : String(result.completed);
|
|
1839
|
+
const cause = failed?.error ?? result.error;
|
|
1840
|
+
super(`perform failed at step ${at}${cause ? `: ${cause}` : ""}`);
|
|
1841
|
+
__publicField(this, "result");
|
|
1842
|
+
this.result = result;
|
|
1843
|
+
}
|
|
1844
|
+
};
|
|
1845
|
+
var SimResetError = class extends SimClientError {
|
|
1846
|
+
constructor(result) {
|
|
1847
|
+
super(`reset (${result.strategy}) failed${result.error ? `: ${result.error}` : ""}`);
|
|
1848
|
+
__publicField(this, "result");
|
|
1849
|
+
this.result = result;
|
|
1850
|
+
}
|
|
1851
|
+
};
|
|
1852
|
+
function sleep(ms) {
|
|
1853
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1854
|
+
}
|
|
1855
|
+
function decodePngDataUrl(dataUrl) {
|
|
1856
|
+
if (typeof dataUrl !== "string" || !dataUrl.startsWith("data:image/png;base64,")) {
|
|
1857
|
+
throw new SimClientError(
|
|
1858
|
+
`screenshot bridge returned a non-png payload: ${JSON.stringify(
|
|
1859
|
+
typeof dataUrl === "string" ? dataUrl.slice(0, 80) : dataUrl
|
|
1860
|
+
)}`
|
|
1861
|
+
);
|
|
1862
|
+
}
|
|
1863
|
+
const base64 = dataUrl.slice("data:image/png;base64,".length);
|
|
1864
|
+
const binary = atob(base64);
|
|
1865
|
+
const bytes = new Uint8Array(binary.length);
|
|
1866
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
1867
|
+
return { dataUrl, bytes };
|
|
1868
|
+
}
|
|
1869
|
+
var TARGET_SEARCH_PARAMS = [
|
|
1870
|
+
"bundle",
|
|
1871
|
+
"demo",
|
|
1872
|
+
"app",
|
|
1873
|
+
"open",
|
|
1874
|
+
"port",
|
|
1875
|
+
"inspectOpen",
|
|
1876
|
+
"appFonts",
|
|
1877
|
+
"appSplash"
|
|
1878
|
+
];
|
|
1879
|
+
function buildSimOpenUrl(currentUrl, target) {
|
|
1880
|
+
const url = new URL(currentUrl);
|
|
1881
|
+
for (const param of TARGET_SEARCH_PARAMS) url.searchParams.delete(param);
|
|
1882
|
+
const pathname = url.pathname.replace(/\/+$/, "") || "/";
|
|
1883
|
+
const prefix = pathname === "/__soot" || pathname.startsWith("/__soot/") ? "/__soot" : "";
|
|
1884
|
+
const normalized = String(target).trim();
|
|
1885
|
+
if (/^\d+$/.test(normalized)) {
|
|
1886
|
+
url.pathname = `${prefix}/rn/${normalized}`;
|
|
1887
|
+
} else {
|
|
1888
|
+
url.pathname = `${prefix}/rn`;
|
|
1889
|
+
url.searchParams.set("open", normalized);
|
|
1890
|
+
}
|
|
1891
|
+
return url.toString();
|
|
1892
|
+
}
|
|
1893
|
+
function isTextTapTarget(target) {
|
|
1894
|
+
return typeof target === "object" && "text" in target && typeof target.text === "string" && !("layout" in target) && !("absolutePosition" in target);
|
|
1895
|
+
}
|
|
1896
|
+
var SimClient = class {
|
|
1897
|
+
constructor(bridge, simId, _options = {}) {
|
|
1898
|
+
__publicField(this, "simId");
|
|
1899
|
+
__publicField(this, "bridge");
|
|
1900
|
+
__publicField(this, "closed", false);
|
|
1901
|
+
this.bridge = bridge;
|
|
1902
|
+
this.simId = simId;
|
|
1903
|
+
}
|
|
1904
|
+
get plane() {
|
|
1905
|
+
return this.bridge.plane;
|
|
1906
|
+
}
|
|
1907
|
+
// --- transport (WsBridge/InspectBridge) ---
|
|
1908
|
+
send(cmd, opts) {
|
|
1909
|
+
if (this.closed) {
|
|
1910
|
+
return Promise.reject(new SimConnectError("sim client is closed"));
|
|
1911
|
+
}
|
|
1912
|
+
const payload = cmd.simId === void 0 ? { ...cmd, simId: this.simId } : cmd;
|
|
1913
|
+
return this.bridge.send(payload, opts);
|
|
1914
|
+
}
|
|
1915
|
+
listSims() {
|
|
1916
|
+
return this.bridge.listSims();
|
|
1917
|
+
}
|
|
1918
|
+
resolveReloadedSim(retiredSimId) {
|
|
1919
|
+
return this.bridge.resolveReloadedSim(retiredSimId);
|
|
1920
|
+
}
|
|
1921
|
+
focusSim(simId = this.simId) {
|
|
1922
|
+
return this.bridge.focusSim(simId);
|
|
1923
|
+
}
|
|
1924
|
+
closeSim(simId = this.simId) {
|
|
1925
|
+
return this.bridge.closeSim(simId);
|
|
1926
|
+
}
|
|
1927
|
+
claim(simId = this.simId, opts) {
|
|
1928
|
+
return this.bridge.claim(simId, opts);
|
|
1929
|
+
}
|
|
1930
|
+
/** release the socket. a script that connected must close to exit cleanly. */
|
|
1931
|
+
close() {
|
|
1932
|
+
if (this.closed) return;
|
|
1933
|
+
this.closed = true;
|
|
1934
|
+
this.bridge.close();
|
|
1935
|
+
}
|
|
1936
|
+
// --- navigation + readiness ---
|
|
1937
|
+
/**
|
|
1938
|
+
* point this sim at a dev target: a metro port (`8081`), or anything the
|
|
1939
|
+
* shell's connect flow accepts (`localhost:8081`, an app slug…). resolves
|
|
1940
|
+
* once the reloaded page answers on the new target route.
|
|
1941
|
+
*/
|
|
1942
|
+
async open(target, opts = {}) {
|
|
1943
|
+
const sims = await this.listSims();
|
|
1944
|
+
const sim = sims.find((entry) => entry.id === this.simId);
|
|
1945
|
+
if (!sim) {
|
|
1946
|
+
throw new SimConnectError(`sim ${this.simId} is no longer connected`);
|
|
1947
|
+
}
|
|
1948
|
+
const currentUrl = sim.url ?? sim.origin;
|
|
1949
|
+
if (!currentUrl) {
|
|
1950
|
+
throw new SimConnectError(
|
|
1951
|
+
`sim ${this.simId} reported no URL to derive the shell from`
|
|
1952
|
+
);
|
|
1953
|
+
}
|
|
1954
|
+
const nextUrl = new URL(buildSimOpenUrl(currentUrl, target));
|
|
1955
|
+
const token = `sim-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1956
|
+
nextUrl.searchParams.set("inspectOpen", token);
|
|
1957
|
+
await this.send({
|
|
1958
|
+
type: "evaluate",
|
|
1959
|
+
code: `window.location.href = ${JSON.stringify(nextUrl.toString())}`
|
|
1960
|
+
});
|
|
1961
|
+
const deadline = Date.now() + (opts.timeoutMs ?? 3e4);
|
|
1962
|
+
while (Date.now() < deadline) {
|
|
1963
|
+
await sleep(250);
|
|
1964
|
+
try {
|
|
1965
|
+
const { url } = await inspectUrl(this);
|
|
1966
|
+
if (new URL(url).searchParams.get("inspectOpen") === token) return;
|
|
1967
|
+
} catch {
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
throw new SimConnectError(
|
|
1971
|
+
`timed out waiting for sim ${this.simId} to load ${nextUrl}`
|
|
1972
|
+
);
|
|
1973
|
+
}
|
|
1974
|
+
/** dispatch a route or app URL through React Native Linking in this guest. */
|
|
1975
|
+
openUrl(target) {
|
|
1976
|
+
return openAppUrl(this, target);
|
|
1977
|
+
}
|
|
1978
|
+
async setAppearance(appearance) {
|
|
1979
|
+
await this.send({ type: "setAppearance", appearance });
|
|
1980
|
+
}
|
|
1981
|
+
/**
|
|
1982
|
+
* block until the guest app has mounted and painted real content.
|
|
1983
|
+
* throws SimReadyTimeoutError (with the last probe status) on timeout.
|
|
1984
|
+
*/
|
|
1985
|
+
async waitReady(opts = {}) {
|
|
1986
|
+
const status = await inspectWaitReady(this, opts.timeoutMs ?? 2e4, {
|
|
1987
|
+
onProgress: opts.onProgress
|
|
1988
|
+
});
|
|
1989
|
+
if (!status.ready) throw new SimReadyTimeoutError(status);
|
|
1990
|
+
return status;
|
|
1991
|
+
}
|
|
1992
|
+
/** wait for layout/animation/transition settle after an interaction. */
|
|
1993
|
+
settle(opts = {}) {
|
|
1994
|
+
return waitForSootsimIdle({
|
|
1995
|
+
bridge: this,
|
|
1996
|
+
maxMs: opts.maxMs ?? 3e3,
|
|
1997
|
+
strict: opts.strict
|
|
1998
|
+
});
|
|
1999
|
+
}
|
|
2000
|
+
// --- read verbs ---
|
|
2001
|
+
/** first matching node, or null. query precedence matches the CLI's find. */
|
|
2002
|
+
async find(query) {
|
|
2003
|
+
const nodes = await this.findAll(query);
|
|
2004
|
+
return nodes[0] ?? null;
|
|
2005
|
+
}
|
|
2006
|
+
async findAll(query) {
|
|
2007
|
+
const mapped = {
|
|
2008
|
+
testId: query.testId ?? null,
|
|
2009
|
+
role: query.role ?? null,
|
|
2010
|
+
type: query.type ?? null,
|
|
2011
|
+
text: query.text ?? null,
|
|
2012
|
+
pressable: query.pressable
|
|
2013
|
+
};
|
|
2014
|
+
const found = await inspectFind(this, mapped);
|
|
2015
|
+
if (!found) {
|
|
2016
|
+
throw new SimClientError(
|
|
2017
|
+
"find() needs at least one of testId, text, role, type, or pressable"
|
|
2018
|
+
);
|
|
2019
|
+
}
|
|
2020
|
+
const { result } = found;
|
|
2021
|
+
if (Array.isArray(result)) return result;
|
|
2022
|
+
return result ? [result] : [];
|
|
2023
|
+
}
|
|
2024
|
+
async tree(depth = 5) {
|
|
2025
|
+
const { tree } = await inspectTree(this, depth);
|
|
2026
|
+
return Array.isArray(tree) ? tree : [];
|
|
2027
|
+
}
|
|
2028
|
+
resolve(selector) {
|
|
2029
|
+
return this.send({ type: "resolve", selector });
|
|
2030
|
+
}
|
|
2031
|
+
/** compact visible semantic tree, formatted as the CLI prints it. */
|
|
2032
|
+
describe(opts = {}) {
|
|
2033
|
+
return inspectDescribe(this, {
|
|
2034
|
+
describe: true,
|
|
2035
|
+
verbose: opts.verbose ?? false,
|
|
2036
|
+
filter: opts.filter ?? "",
|
|
2037
|
+
compact: !opts.verbose,
|
|
2038
|
+
hideXy: false,
|
|
2039
|
+
fullText: true
|
|
2040
|
+
});
|
|
2041
|
+
}
|
|
2042
|
+
accessibilityTree() {
|
|
2043
|
+
return inspectAccessibilityTree(this);
|
|
2044
|
+
}
|
|
2045
|
+
async logs(opts = {}) {
|
|
2046
|
+
return filterLogEntries(await inspectLogs(this), opts);
|
|
2047
|
+
}
|
|
2048
|
+
errors(limit = 20) {
|
|
2049
|
+
return inspectErrors(this, limit);
|
|
2050
|
+
}
|
|
2051
|
+
warnings(limit = 20) {
|
|
2052
|
+
return inspectWarnings(this, limit);
|
|
2053
|
+
}
|
|
2054
|
+
keyboard() {
|
|
2055
|
+
return inspectKeyboard(this);
|
|
2056
|
+
}
|
|
2057
|
+
shellState(readyTimeoutMs = 0) {
|
|
2058
|
+
return getShellState(this, readyTimeoutMs);
|
|
2059
|
+
}
|
|
2060
|
+
async url() {
|
|
2061
|
+
const { url } = await inspectUrl(this);
|
|
2062
|
+
return url;
|
|
2063
|
+
}
|
|
2064
|
+
async screenshot(opts = {}) {
|
|
2065
|
+
const result = await this.send({
|
|
2066
|
+
type: "screenshot",
|
|
2067
|
+
...opts.layers ? { layers: opts.layers } : {},
|
|
2068
|
+
...opts.crop ? { crop: opts.crop } : {}
|
|
2069
|
+
});
|
|
2070
|
+
return decodePngDataUrl(result);
|
|
2071
|
+
}
|
|
2072
|
+
// --- input verbs ---
|
|
2073
|
+
/**
|
|
2074
|
+
* tap by testID, visible text, node, or coordinates. resolution, retry, and
|
|
2075
|
+
* hit verification are the CLI's shared tap kernels. throws SimTapError when
|
|
2076
|
+
* the target is missing, covered, offscreen, or the press missed.
|
|
2077
|
+
*/
|
|
2078
|
+
async tap(target) {
|
|
2079
|
+
if (typeof target === "string") {
|
|
2080
|
+
return this.throwUnlessTapped(await tapBest(this, target), target);
|
|
2081
|
+
}
|
|
2082
|
+
if ("testId" in target && typeof target.testId === "string") {
|
|
2083
|
+
return this.throwUnlessTapped(await tapById(this, target.testId), target.testId);
|
|
2084
|
+
}
|
|
2085
|
+
if (isTextTapTarget(target)) {
|
|
2086
|
+
const { text, ...textOpts } = target;
|
|
2087
|
+
return this.throwUnlessTapped(await tapByText(this, text, textOpts), text);
|
|
2088
|
+
}
|
|
2089
|
+
if ("testID" in target && typeof target.testID === "string" && typeof target.nodeId !== "number") {
|
|
2090
|
+
return this.throwUnlessTapped(await tapById(this, target.testID), target.testID);
|
|
2091
|
+
}
|
|
2092
|
+
const coords = this.coordsForTapTarget(target);
|
|
2093
|
+
const result = await this.send({
|
|
2094
|
+
type: "tap",
|
|
2095
|
+
x: coords.x,
|
|
2096
|
+
y: coords.y,
|
|
2097
|
+
..."nodeId" in target && typeof target.nodeId === "number" ? { target: { nodeId: target.nodeId } } : {}
|
|
2098
|
+
});
|
|
2099
|
+
if (!isTapSuccess(result)) {
|
|
2100
|
+
throw new SimTapError(
|
|
2101
|
+
`tap at ${coords.x},${coords.y} missed: ${JSON.stringify(result)}`
|
|
2102
|
+
);
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
async longPress(target, opts = {}) {
|
|
2106
|
+
const coords = await this.resolveCoords(target);
|
|
2107
|
+
const result = await this.send(
|
|
2108
|
+
{
|
|
2109
|
+
type: "longPress",
|
|
2110
|
+
x: coords.x,
|
|
2111
|
+
y: coords.y,
|
|
2112
|
+
durationMs: opts.durationMs ?? 600
|
|
2113
|
+
},
|
|
2114
|
+
{ timeoutMs: (opts.durationMs ?? 600) + 15e3 }
|
|
2115
|
+
);
|
|
2116
|
+
if (!result?.ok) {
|
|
2117
|
+
throw new SimTapError(`longPress at ${coords.x},${coords.y} missed`);
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
/** type through the visual iOS keyboard. requires a focused input. */
|
|
2121
|
+
async type(text) {
|
|
2122
|
+
await this.requireVisibleKeyboard("type");
|
|
2123
|
+
await this.send({ type: "keyboard", action: "type", text });
|
|
2124
|
+
}
|
|
2125
|
+
/** press a keyboard key ('return', 'delete', 'space', …). */
|
|
2126
|
+
async press(key) {
|
|
2127
|
+
await this.requireVisibleKeyboard("press");
|
|
2128
|
+
await this.send({ type: "keyboard", action: "press", text: key });
|
|
2129
|
+
}
|
|
2130
|
+
async dismissKeyboard() {
|
|
2131
|
+
await this.send({ type: "keyboard", action: "dismiss" });
|
|
2132
|
+
}
|
|
2133
|
+
// --- batched input, compound state, reset ---
|
|
2134
|
+
/**
|
|
2135
|
+
* execute an ordered step batch inside the page in ONE round-trip, so waits
|
|
2136
|
+
* and gesture physics run on real wall-clock timing. `tapText` steps are
|
|
2137
|
+
* resolved to coordinates client-side before the batch is sent. throws
|
|
2138
|
+
* SimPerformError (carrying the per-step result) when the batch fails.
|
|
2139
|
+
*/
|
|
2140
|
+
async perform(steps, options) {
|
|
2141
|
+
const resolved = [];
|
|
2142
|
+
for (const step of steps) {
|
|
2143
|
+
if (step.type !== "tapText") {
|
|
2144
|
+
resolved.push(step);
|
|
2145
|
+
continue;
|
|
2146
|
+
}
|
|
2147
|
+
const target = await resolveTargetCoords(this, {
|
|
2148
|
+
mode: "text",
|
|
2149
|
+
value: step.text
|
|
2150
|
+
});
|
|
2151
|
+
if (!target) {
|
|
2152
|
+
throw new SimTapError(
|
|
2153
|
+
`perform tapText: no node with text ${JSON.stringify(step.text)}`
|
|
2154
|
+
);
|
|
2155
|
+
}
|
|
2156
|
+
resolved.push({
|
|
2157
|
+
type: "tap",
|
|
2158
|
+
x: target.x,
|
|
2159
|
+
y: target.y,
|
|
2160
|
+
target: {
|
|
2161
|
+
nodeId: target.nodeId,
|
|
2162
|
+
testID: target.testID ?? void 0,
|
|
2163
|
+
text: target.text ?? void 0,
|
|
2164
|
+
type: target.type ?? void 0
|
|
2165
|
+
}
|
|
2166
|
+
});
|
|
2167
|
+
}
|
|
2168
|
+
const batchTimeoutMs = options?.timeoutMs ?? 3e4;
|
|
2169
|
+
const result = await this.send(
|
|
2170
|
+
{
|
|
2171
|
+
type: "perform",
|
|
2172
|
+
steps: resolved,
|
|
2173
|
+
...options ? { performOptions: options } : {}
|
|
2174
|
+
},
|
|
2175
|
+
{ timeoutMs: batchTimeoutMs + 5e3 }
|
|
2176
|
+
);
|
|
2177
|
+
if (!result?.ok) throw new SimPerformError(result);
|
|
2178
|
+
return result;
|
|
2179
|
+
}
|
|
2180
|
+
/** screenshot + tree + route + recent errors in one round-trip. */
|
|
2181
|
+
state(options) {
|
|
2182
|
+
return this.send({
|
|
2183
|
+
type: "state",
|
|
2184
|
+
...options ? { stateOptions: options } : {}
|
|
2185
|
+
});
|
|
2186
|
+
}
|
|
2187
|
+
/**
|
|
2188
|
+
* two-tier app state wipe ('data' clears storage/caches, 'full' also clears
|
|
2189
|
+
* keychain/permissions/native-module state). throws SimResetError on failure.
|
|
2190
|
+
*/
|
|
2191
|
+
async reset(options) {
|
|
2192
|
+
const result = await this.send(
|
|
2193
|
+
{ type: "reset", ...options ? { resetOptions: options } : {} },
|
|
2194
|
+
{ timeoutMs: 6e4 }
|
|
2195
|
+
);
|
|
2196
|
+
if (!result?.ok) throw new SimResetError(result);
|
|
2197
|
+
return result;
|
|
2198
|
+
}
|
|
2199
|
+
// --- internals ---
|
|
2200
|
+
throwUnlessTapped(outcome, label) {
|
|
2201
|
+
if (!outcome.failure && isTapSuccess(outcome.result)) return;
|
|
2202
|
+
const reason = outcome.failure === "not-found" ? "no matching node" : outcome.failure === "missed" ? outcome.result?.reason ?? "press missed" : outcome.payload?.ambiguous ? `ambiguous: ${outcome.payload.total} matches` : outcome.result?.reason ?? outcome.payload?.error ?? "tap failed";
|
|
2203
|
+
throw new SimTapError(`tap ${JSON.stringify(label)} failed: ${reason}`, outcome);
|
|
2204
|
+
}
|
|
2205
|
+
coordsForTapTarget(target) {
|
|
2206
|
+
if (typeof target === "object" && "x" in target && "y" in target && typeof target.x === "number" && typeof target.y === "number") {
|
|
2207
|
+
return { x: target.x, y: target.y };
|
|
2208
|
+
}
|
|
2209
|
+
if (typeof target === "object" && "absolutePosition" in target && target.absolutePosition && target.layout) {
|
|
2210
|
+
return {
|
|
2211
|
+
x: target.absolutePosition.x + (target.layout.width ?? 0) / 2,
|
|
2212
|
+
y: target.absolutePosition.y + (target.layout.height ?? 0) / 2
|
|
2213
|
+
};
|
|
2214
|
+
}
|
|
2215
|
+
throw new SimClientError(
|
|
2216
|
+
"tap target needs coordinates, a testId, text, or a node from find()"
|
|
2217
|
+
);
|
|
2218
|
+
}
|
|
2219
|
+
async resolveCoords(target) {
|
|
2220
|
+
if (typeof target === "string") {
|
|
2221
|
+
const resolved = await resolveTargetCoords(this, { mode: "testid", value: target }) ?? await resolveTargetCoords(this, { mode: "text", value: target });
|
|
2222
|
+
if (!resolved) throw new SimTapError(`no node matching ${JSON.stringify(target)}`);
|
|
2223
|
+
return resolved;
|
|
2224
|
+
}
|
|
2225
|
+
if ("testId" in target && typeof target.testId === "string") {
|
|
2226
|
+
const resolved = await resolveTargetCoords(this, {
|
|
2227
|
+
mode: "testid",
|
|
2228
|
+
value: target.testId
|
|
2229
|
+
});
|
|
2230
|
+
if (!resolved) {
|
|
2231
|
+
throw new SimTapError(`no node with testID ${JSON.stringify(target.testId)}`);
|
|
2232
|
+
}
|
|
2233
|
+
return resolved;
|
|
2234
|
+
}
|
|
2235
|
+
if ("text" in target && typeof target.text === "string" && !("layout" in target)) {
|
|
2236
|
+
const resolved = await resolveTargetCoords(this, {
|
|
2237
|
+
mode: "text",
|
|
2238
|
+
value: target.text
|
|
2239
|
+
});
|
|
2240
|
+
if (!resolved) {
|
|
2241
|
+
throw new SimTapError(`no node with text ${JSON.stringify(target.text)}`);
|
|
2242
|
+
}
|
|
2243
|
+
return resolved;
|
|
2244
|
+
}
|
|
2245
|
+
return this.coordsForTapTarget(target);
|
|
2246
|
+
}
|
|
2247
|
+
async requireVisibleKeyboard(action) {
|
|
2248
|
+
const deadline = Date.now() + 1500;
|
|
2249
|
+
for (; ; ) {
|
|
2250
|
+
const state = await inspectKeyboard(this);
|
|
2251
|
+
if (!("error" in state) && state.visible) return;
|
|
2252
|
+
if (Date.now() >= deadline) {
|
|
2253
|
+
throw new SimKeyboardError(
|
|
2254
|
+
`${action} requires the iOS keyboard to be visible \u2014 focus an input first (tap a TextInput)`
|
|
2255
|
+
);
|
|
2256
|
+
}
|
|
2257
|
+
await sleep(150);
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
};
|
|
2261
|
+
export {
|
|
2262
|
+
DEBUG_CHANNELS,
|
|
2263
|
+
LAYOUT_GET_NATIVE_EVAL,
|
|
2264
|
+
MERGED_CONSOLE_COUNT_EVAL,
|
|
2265
|
+
NOISY_TIMELINE_KINDS,
|
|
2266
|
+
SimClient,
|
|
2267
|
+
SimClientError,
|
|
2268
|
+
SimConnectError,
|
|
2269
|
+
SimKeyboardError,
|
|
2270
|
+
SimPerformError,
|
|
2271
|
+
SimReadyTimeoutError,
|
|
2272
|
+
SimResetError,
|
|
2273
|
+
SimTapError,
|
|
2274
|
+
buildSimOpenUrl,
|
|
2275
|
+
callDebugBridge,
|
|
2276
|
+
clearConsole,
|
|
2277
|
+
clearLogs,
|
|
2278
|
+
clearRequests,
|
|
2279
|
+
detectOpenNativeUI,
|
|
2280
|
+
filterLogEntries,
|
|
2281
|
+
formatLayoutElements,
|
|
2282
|
+
formatSemanticTree,
|
|
2283
|
+
formatTimelineEvent,
|
|
2284
|
+
formatTimelineEvents,
|
|
2285
|
+
formatTimelinePayload,
|
|
2286
|
+
formatTimelineSummary,
|
|
2287
|
+
getShellState,
|
|
2288
|
+
inspectAccessibilityTree,
|
|
2289
|
+
inspectDebugFind,
|
|
2290
|
+
inspectDebugFlags,
|
|
2291
|
+
inspectDebugRecent,
|
|
2292
|
+
inspectDebugStatus,
|
|
2293
|
+
inspectDescribe,
|
|
2294
|
+
inspectErrors,
|
|
2295
|
+
inspectFind,
|
|
2296
|
+
inspectGetLayout,
|
|
2297
|
+
inspectKeyboard,
|
|
2298
|
+
inspectLogs,
|
|
2299
|
+
inspectMemory,
|
|
2300
|
+
inspectNodeCount,
|
|
2301
|
+
inspectRequests,
|
|
2302
|
+
inspectScreens,
|
|
2303
|
+
inspectScrollStateAt,
|
|
2304
|
+
inspectTimelineAdvanceCursor,
|
|
2305
|
+
inspectTimelineRecent,
|
|
2306
|
+
inspectTimelineSummary,
|
|
2307
|
+
inspectTree,
|
|
2308
|
+
inspectUrl,
|
|
2309
|
+
inspectWaitReady,
|
|
2310
|
+
inspectWaitSelector,
|
|
2311
|
+
inspectWarnings,
|
|
2312
|
+
isInspectModeActive,
|
|
2313
|
+
isInspectPickCommand,
|
|
2314
|
+
isShellCommandUnavailable,
|
|
2315
|
+
isSimSemanticTree,
|
|
2316
|
+
isTapSuccess,
|
|
2317
|
+
layoutGetNativeEval,
|
|
2318
|
+
rankInteractive,
|
|
2319
|
+
readyProbeHasContent,
|
|
2320
|
+
readyProbeHasTargetContent,
|
|
2321
|
+
resolveFindMode,
|
|
2322
|
+
resolveMaxMsFlag,
|
|
2323
|
+
scoreInteractive,
|
|
2324
|
+
scoreLayoutTextContrast,
|
|
2325
|
+
setDebugChannels,
|
|
2326
|
+
shouldSkipAutoSettleForInspectPick,
|
|
2327
|
+
tapBest,
|
|
2328
|
+
tapById,
|
|
2329
|
+
tapByText,
|
|
2330
|
+
tapCommandForNode,
|
|
2331
|
+
tapCoordinates,
|
|
2332
|
+
tapResolvedTarget,
|
|
2333
|
+
tapTargetFromPayload,
|
|
2334
|
+
waitForSootsimIdle,
|
|
2335
|
+
waitReadyReason
|
|
2336
|
+
};
|