sootsim 0.1.112 → 0.1.113
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/dist-cli/bin.js +3 -3
- package/dist-cli/chunks/{agent-7LGWSKBP.js → agent-3JTU2UL4.js} +2 -2
- package/dist-cli/chunks/{agent-wrapper-GRVMTBFJ.js → agent-wrapper-CR7GJ7FP.js} +2 -2
- package/dist-cli/chunks/{assert-GSP3A337.js → assert-N2OG5JQ3.js} +2 -2
- package/dist-cli/chunks/auto-bootstrap-RUMSH2SU.js +2 -0
- package/dist-cli/chunks/beta-WAJQAPBI.js +2 -0
- package/dist-cli/chunks/{chunk-QJL6QTAW.js → chunk-2OGV4GCY.js} +1 -1
- package/dist-cli/chunks/{chunk-RY25ZLAP.js → chunk-2SXHMBEC.js} +1 -1
- package/dist-cli/chunks/{chunk-ZGFOEFIT.js → chunk-3EQEJK4E.js} +2 -2
- package/dist-cli/chunks/chunk-3J3AQWIW.js +1 -0
- package/dist-cli/chunks/{chunk-YEG6THC2.js → chunk-3MBITUPW.js} +1 -1
- package/dist-cli/chunks/{chunk-QXBSCY2G.js → chunk-3OUCFCUV.js} +2 -2
- package/dist-cli/chunks/{chunk-3EE7UVER.js → chunk-4IA3F7NN.js} +2 -2
- package/dist-cli/chunks/{chunk-UFHLNGFE.js → chunk-4K22TYNZ.js} +2 -2
- package/dist-cli/chunks/{chunk-O7T3S7GK.js → chunk-5ALMZP2C.js} +7 -7
- package/dist-cli/chunks/{chunk-5DW7CLVT.js → chunk-5QY6UIVH.js} +2 -2
- package/dist-cli/chunks/{chunk-FTRDTDJF.js → chunk-73HWAXE2.js} +2 -2
- package/dist-cli/chunks/{chunk-DKZKYTOM.js → chunk-7HFQ2NWW.js} +2 -2
- package/dist-cli/chunks/{chunk-K6U4PNYP.js → chunk-AC7WDB6V.js} +1 -1
- package/dist-cli/chunks/{chunk-FKL3NMR5.js → chunk-BKUVXSRQ.js} +1 -1
- package/dist-cli/chunks/{chunk-RCDZBNJW.js → chunk-D43UWL27.js} +2 -2
- package/dist-cli/chunks/{chunk-P37J53P3.js → chunk-D5KQSLTO.js} +2 -2
- package/dist-cli/chunks/{chunk-AL4B57VD.js → chunk-E3H5ZULL.js} +1 -1
- package/dist-cli/chunks/{chunk-P74E724D.js → chunk-EBWNCJET.js} +2 -2
- package/dist-cli/chunks/{chunk-MACTWR2J.js → chunk-FXSPK6L3.js} +1 -1
- package/dist-cli/chunks/{chunk-GN7LBY6G.js → chunk-H7MKPGGV.js} +3 -3
- package/dist-cli/chunks/{chunk-WIO2R4W7.js → chunk-IZYZPIRZ.js} +2 -2
- package/dist-cli/chunks/chunk-K24H4XOV.js +1 -0
- package/dist-cli/chunks/{chunk-7CCONVJK.js → chunk-K5LENBS5.js} +1 -1
- package/dist-cli/chunks/{chunk-XR6VRNF4.js → chunk-L76VLJIZ.js} +2 -2
- package/dist-cli/chunks/{chunk-ZTBVBNY2.js → chunk-LQTTCT5C.js} +1 -1
- package/dist-cli/chunks/{chunk-DY7HYEEP.js → chunk-LW3KJUVL.js} +1 -1
- package/dist-cli/chunks/{chunk-ERD5IQRP.js → chunk-MWQUK6QO.js} +2 -2
- package/dist-cli/chunks/{chunk-MSZWCIEH.js → chunk-MYNBI2ND.js} +1 -1
- package/dist-cli/chunks/{chunk-UQ4WCJOG.js → chunk-NIUQAJCI.js} +2 -2
- package/dist-cli/chunks/{chunk-ALVFKTMD.js → chunk-OIWS3EXW.js} +2 -2
- package/dist-cli/chunks/{chunk-GMXUIARA.js → chunk-RMJMYLLR.js} +2 -2
- package/dist-cli/chunks/{chunk-TOETDEFL.js → chunk-RURKHOVS.js} +1 -1
- package/dist-cli/chunks/{chunk-KSQTJY3Z.js → chunk-UUFG4HVZ.js} +1 -1
- package/dist-cli/chunks/chunk-VX6D2AI3.js +1 -0
- package/dist-cli/chunks/{chunk-DYBNT4T7.js → chunk-WGJRZVLO.js} +1 -1
- package/dist-cli/chunks/{chunk-6WK4XJLI.js → chunk-WOKSYJIY.js} +2 -2
- package/dist-cli/chunks/{chunk-GGBWCCP2.js → chunk-WSGVQEXB.js} +3 -3
- package/dist-cli/chunks/{chunk-BYFAYZRV.js → chunk-XK275OXC.js} +1 -1
- package/dist-cli/chunks/{chunk-E3JNCJMG.js → chunk-XKKCAC3J.js} +2 -2
- package/dist-cli/chunks/{chunk-OXFTMHJQ.js → chunk-XODSEUOW.js} +2 -2
- package/dist-cli/chunks/{chunk-6TPWDY6K.js → chunk-XU6LOLAB.js} +2 -2
- package/dist-cli/chunks/{chunk-QTVNFT2F.js → chunk-XXORXONW.js} +2 -2
- package/dist-cli/chunks/chunk-YHK6RX3N.js +2 -0
- package/dist-cli/chunks/{chunk-SJPXB24I.js → chunk-ZVE6F53R.js} +2 -2
- package/dist-cli/chunks/cli-version-2F4UFHUR.js +2 -0
- package/dist-cli/chunks/{compat-TDDZ65HJ.js → compat-F6VRDJEA.js} +3 -3
- package/dist-cli/chunks/{config-ZYXUW3HJ.js → config-K2VVAYH6.js} +2 -2
- package/dist-cli/chunks/control-UCTK6SGP.js +2 -0
- package/dist-cli/chunks/{cpu-profile-Q3TJ6RXT.js → cpu-profile-6SQKDUCL.js} +2 -2
- package/dist-cli/chunks/{daemon-RITDRE4H.js → daemon-AWSYWILR.js} +2 -2
- package/dist-cli/chunks/{debug-XKFB4225.js → debug-PX35BAY4.js} +3 -3
- package/dist-cli/chunks/{detox-XIZ3DBRQ.js → detox-5SPMUGEX.js} +2 -2
- package/dist-cli/chunks/{device-GZBJJJEB.js → device-ACGNWZ3G.js} +2 -2
- package/dist-cli/chunks/{diagnose-H2KIQ752.js → diagnose-V2VT4ASE.js} +2 -2
- package/dist-cli/chunks/drivers-G4ZQAYTR.js +2 -0
- package/dist-cli/chunks/{electron-VFZYBO4G.js → electron-Y6E5R4SF.js} +3 -3
- package/dist-cli/chunks/flow-I642RDRA.js +2 -0
- package/dist-cli/chunks/help-BPNT2YES.js +2 -0
- package/dist-cli/chunks/{hints-NYSBGRRV.js → hints-2V7ZHG6L.js} +2 -2
- package/dist-cli/chunks/{home-paths-ISIMYDEJ.js → home-paths-MVPKADD6.js} +2 -2
- package/dist-cli/chunks/inspect-DNRAUTAB.js +974 -0
- package/dist-cli/chunks/install-BMWYQTOZ.js +2 -0
- package/dist-cli/chunks/{install-desktop-GACFW2HY.js → install-desktop-CYJDYA53.js} +3 -3
- package/dist-cli/chunks/{keys-LZLQVCHR.js → keys-FUYSXI2L.js} +2 -2
- package/dist-cli/chunks/{launch-JYZNGXU6.js → launch-PQ6QTEP7.js} +3 -3
- package/dist-cli/chunks/{login-WY3VAO3Y.js → login-BMFJHJ3B.js} +4 -4
- package/dist-cli/chunks/{logout-F74SOJ54.js → logout-PG2JFWQL.js} +2 -2
- package/dist-cli/chunks/{maestro-ENB7YUJM.js → maestro-2QFWLFF4.js} +2 -2
- package/dist-cli/chunks/{preview-KF5L3JID.js → preview-L6YAPMSD.js} +2 -2
- package/dist-cli/chunks/{profile-IWEB6CZL.js → profile-2QQGLQBN.js} +2 -2
- package/dist-cli/chunks/{react-D2YTYZIP.js → react-ZKAMEZ6H.js} +2 -2
- package/dist-cli/chunks/{record-BOUB4J4P.js → record-XZNFFK4K.js} +2 -2
- package/dist-cli/chunks/runtime-4X66LQCP.js +2 -0
- package/dist-cli/chunks/{runtime-delivery-GZ5VNBLN.js → runtime-delivery-R5E2YPXP.js} +2 -2
- package/dist-cli/chunks/{screenshot-TXB77HDC.js → screenshot-5BDZO36S.js} +2 -2
- package/dist-cli/chunks/{screenshot-mode-D7K5IYF3.js → screenshot-mode-OUMQT4EP.js} +2 -2
- package/dist-cli/chunks/{screenshots-RUJ5NM3X.js → screenshots-RTIXDSFS.js} +2 -2
- package/dist-cli/chunks/{server-KWY5Q5KH.js → server-ZF2XUVP7.js} +2 -2
- package/dist-cli/chunks/setup-repo-BBC3QXE7.js +2 -0
- package/dist-cli/chunks/{skills-S4H3WCQE.js → skills-YN2QLIXK.js} +2 -2
- package/dist-cli/chunks/{start-MTI2FM7W.js → start-5HZPS3B6.js} +4 -4
- package/dist-cli/chunks/store-JJAGDHAI.js +2 -0
- package/dist-cli/chunks/telemetry-MRP2ISHT.js +2 -0
- package/dist-cli/chunks/{test-MNVKQFII.js → test-RQWNRURE.js} +3 -3
- package/dist-cli/chunks/{three-mode-V75AD5VC.js → three-mode-STWOAVTT.js} +2 -2
- package/dist-cli/chunks/{timeline-PFHSMH67.js → timeline-NZS6LDPR.js} +2 -2
- package/dist-cli/chunks/{upgrade-INEYUJTS.js → upgrade-PM4RZ7CD.js} +2 -2
- package/dist-cli/chunks/upload-LLA3GT6U.js +2 -0
- package/dist-cli/chunks/{web-L7FHHETW.js → web-YQE55355.js} +2 -2
- package/dist-cli/chunks/{what-happened-NY72VJPW.js → what-happened-4HEXIQ23.js} +2 -2
- package/dist-cli/chunks/{whoami-XTTFI7QK.js → whoami-RZVBLNXN.js} +2 -2
- package/dist-lib/agent-daemon-client.cjs +1 -1
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/agent-sessions.cjs +1 -1
- package/dist-lib/attached-projects.cjs +1 -1
- package/dist-lib/auth/shared-session.cjs +1 -1
- package/dist-lib/backend-origin.cjs +1 -1
- package/dist-lib/beta.cjs +1 -1
- package/dist-lib/beta.mjs +1 -1
- package/dist-lib/bridge-constants.cjs +1 -1
- package/dist-lib/cli-constants.cjs +1 -1
- package/dist-lib/config.cjs +1 -1
- package/dist-lib/detox/index.cjs +1 -1
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +1 -1
- package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
- package/dist-lib/host/websocket-proxy.cjs +1 -1
- package/dist-lib/index.cjs +1 -1
- package/dist-lib/metro.cjs +1 -1
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/render-mode.cjs +1 -1
- package/dist-lib/scripts/demo-app-registry.cjs +1 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
- package/dist-lib/sdk.cjs +1440 -0
- package/dist-lib/sdk.mjs +1360 -0
- package/dist-lib/skills.cjs +1 -1
- package/dist-lib/vite.cjs +1 -1
- package/package.json +7 -1
- package/src/sdk.ts +7 -0
- package/dist-cli/chunks/auto-bootstrap-736PQWPN.js +0 -2
- package/dist-cli/chunks/beta-BKLH7QSW.js +0 -2
- package/dist-cli/chunks/chunk-2ZZUG4LB.js +0 -2
- package/dist-cli/chunks/chunk-3ISNSUXK.js +0 -1
- package/dist-cli/chunks/chunk-ILDC36C5.js +0 -1
- package/dist-cli/chunks/chunk-LSPX3IR3.js +0 -1
- package/dist-cli/chunks/cli-version-7YTDRSHR.js +0 -2
- package/dist-cli/chunks/control-N5OQDWFT.js +0 -2
- package/dist-cli/chunks/drivers-KJ55BQZI.js +0 -2
- package/dist-cli/chunks/flow-TBVVFAO5.js +0 -2
- package/dist-cli/chunks/help-XPU3EWZ7.js +0 -2
- package/dist-cli/chunks/inspect-FVGSOR3L.js +0 -980
- package/dist-cli/chunks/install-HW4GGTYA.js +0 -2
- package/dist-cli/chunks/runtime-4HPGMK52.js +0 -2
- package/dist-cli/chunks/setup-repo-6ZFTYKUS.js +0 -2
- package/dist-cli/chunks/store-6Q4Y5VIK.js +0 -2
- package/dist-cli/chunks/telemetry-DJTKUHAH.js +0 -2
- package/dist-cli/chunks/upload-4GNZRMI4.js +0 -2
package/dist-lib/sdk.cjs
ADDED
|
@@ -0,0 +1,1440 @@
|
|
|
1
|
+
/*! sootsim v0.1.113 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
|
+
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
|
+
"use strict";
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
21
|
+
|
|
22
|
+
// src/sdk.ts
|
|
23
|
+
var sdk_exports = {};
|
|
24
|
+
__export(sdk_exports, {
|
|
25
|
+
DEBUG_CHANNELS: () => DEBUG_CHANNELS,
|
|
26
|
+
READY_CONTENT_NODE_FLOOR: () => READY_CONTENT_NODE_FLOOR,
|
|
27
|
+
READY_NODE_STABLE_MS: () => READY_NODE_STABLE_MS,
|
|
28
|
+
WAIT_READY_PROBE: () => WAIT_READY_PROBE,
|
|
29
|
+
WAIT_READY_PROGRESS_INTERVAL_MS: () => WAIT_READY_PROGRESS_INTERVAL_MS,
|
|
30
|
+
clearConsole: () => clearConsole,
|
|
31
|
+
clearLogs: () => clearLogs,
|
|
32
|
+
clearRequests: () => clearRequests,
|
|
33
|
+
filterLogEntries: () => filterLogEntries,
|
|
34
|
+
formatTimelineSummary: () => formatTimelineSummary,
|
|
35
|
+
getShellState: () => getShellState,
|
|
36
|
+
inspectAccessibilityTree: () => inspectAccessibilityTree,
|
|
37
|
+
inspectDebugFind: () => inspectDebugFind,
|
|
38
|
+
inspectDebugFlags: () => inspectDebugFlags,
|
|
39
|
+
inspectDebugRecent: () => inspectDebugRecent,
|
|
40
|
+
inspectDebugStatus: () => inspectDebugStatus,
|
|
41
|
+
inspectDescribe: () => inspectDescribe,
|
|
42
|
+
inspectErrors: () => inspectErrors,
|
|
43
|
+
inspectFind: () => inspectFind,
|
|
44
|
+
inspectKeyboard: () => inspectKeyboard,
|
|
45
|
+
inspectLogs: () => inspectLogs,
|
|
46
|
+
inspectMemory: () => inspectMemory,
|
|
47
|
+
inspectNodeCount: () => inspectNodeCount,
|
|
48
|
+
inspectRequests: () => inspectRequests,
|
|
49
|
+
inspectScreens: () => inspectScreens,
|
|
50
|
+
inspectScrollStateAt: () => inspectScrollStateAt,
|
|
51
|
+
inspectTimelineAdvanceCursor: () => inspectTimelineAdvanceCursor,
|
|
52
|
+
inspectTimelineRecent: () => inspectTimelineRecent,
|
|
53
|
+
inspectTimelineSummary: () => inspectTimelineSummary,
|
|
54
|
+
inspectTree: () => inspectTree,
|
|
55
|
+
inspectUrl: () => inspectUrl,
|
|
56
|
+
inspectWaitReady: () => inspectWaitReady,
|
|
57
|
+
inspectWaitSelector: () => inspectWaitSelector,
|
|
58
|
+
inspectWarnings: () => inspectWarnings,
|
|
59
|
+
isInspectModeActive: () => isInspectModeActive,
|
|
60
|
+
isInspectPickCommand: () => isInspectPickCommand,
|
|
61
|
+
isShellCommandUnavailable: () => isShellCommandUnavailable,
|
|
62
|
+
isTapSuccess: () => isTapSuccess,
|
|
63
|
+
rankInteractive: () => rankInteractive,
|
|
64
|
+
readyProbeHasContent: () => readyProbeHasContent,
|
|
65
|
+
resolveFindMode: () => resolveFindMode,
|
|
66
|
+
resolveMaxMsFlag: () => resolveMaxMsFlag,
|
|
67
|
+
scoreInteractive: () => scoreInteractive,
|
|
68
|
+
setDebugChannels: () => setDebugChannels,
|
|
69
|
+
shouldSkipAutoSettleForInspectPick: () => shouldSkipAutoSettleForInspectPick,
|
|
70
|
+
tapBest: () => tapBest,
|
|
71
|
+
tapById: () => tapById,
|
|
72
|
+
tapByText: () => tapByText,
|
|
73
|
+
tapCommandForNode: () => tapCommandForNode,
|
|
74
|
+
tapCoordinates: () => tapCoordinates,
|
|
75
|
+
tapResolvedTarget: () => tapResolvedTarget,
|
|
76
|
+
tapTargetFromPayload: () => tapTargetFromPayload,
|
|
77
|
+
waitForSootsimIdle: () => waitForSootsimIdle,
|
|
78
|
+
waitReadyReason: () => waitReadyReason
|
|
79
|
+
});
|
|
80
|
+
module.exports = __toCommonJS(sdk_exports);
|
|
81
|
+
|
|
82
|
+
// cli/commands/inspect/core.ts
|
|
83
|
+
function resolveMaxMsFlag(args, fallbackMs) {
|
|
84
|
+
const aliases = ["--max-ms", "--maxMs", "--maxms", "--max_ms"];
|
|
85
|
+
for (const flag of aliases) {
|
|
86
|
+
const i = args.indexOf(flag);
|
|
87
|
+
if (i >= 0 && args[i + 1]) {
|
|
88
|
+
const n = Number(args[i + 1]);
|
|
89
|
+
if (Number.isFinite(n)) return Math.max(100, n);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return fallbackMs;
|
|
93
|
+
}
|
|
94
|
+
var INSPECT_PICK_COMMANDS = /* @__PURE__ */ new Set([
|
|
95
|
+
"tap",
|
|
96
|
+
"double-tap",
|
|
97
|
+
"tap-text",
|
|
98
|
+
"tap-id",
|
|
99
|
+
"long-press",
|
|
100
|
+
"touch"
|
|
101
|
+
]);
|
|
102
|
+
function isInspectPickCommand(subcommand) {
|
|
103
|
+
return typeof subcommand === "string" && INSPECT_PICK_COMMANDS.has(subcommand);
|
|
104
|
+
}
|
|
105
|
+
async function isInspectModeActive(bridge) {
|
|
106
|
+
const active = await bridge.send({
|
|
107
|
+
type: "evaluate",
|
|
108
|
+
code: "window.__sootsimEngineState?.inspectActive === true"
|
|
109
|
+
});
|
|
110
|
+
return active === true;
|
|
111
|
+
}
|
|
112
|
+
async function shouldSkipAutoSettleForInspectPick(bridge, subcommand) {
|
|
113
|
+
if (!isInspectPickCommand(subcommand)) return false;
|
|
114
|
+
try {
|
|
115
|
+
return await isInspectModeActive(bridge);
|
|
116
|
+
} catch {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async function inspectNodeCount(bridge) {
|
|
121
|
+
const count = await bridge.send({
|
|
122
|
+
type: "evaluate",
|
|
123
|
+
code: "(async () => await window.__sootsimTest.getNodeCount())()"
|
|
124
|
+
});
|
|
125
|
+
return { nodes: typeof count === "number" ? count : 0 };
|
|
126
|
+
}
|
|
127
|
+
async function inspectTree(bridge, depth = 5) {
|
|
128
|
+
const tree = await bridge.send({
|
|
129
|
+
type: "evaluate",
|
|
130
|
+
code: `(async () => await window.__sootsimTest.dumpTree(${depth}))()`
|
|
131
|
+
});
|
|
132
|
+
return { depth, tree };
|
|
133
|
+
}
|
|
134
|
+
async function inspectUrl(bridge) {
|
|
135
|
+
const url = await bridge.send({ type: "evaluate", code: "window.location.href" });
|
|
136
|
+
return { url: typeof url === "string" ? url : "" };
|
|
137
|
+
}
|
|
138
|
+
async function inspectDescribe(bridge, dumpOpts) {
|
|
139
|
+
const code = `(async () => {
|
|
140
|
+
const t = window.__sootsimTest
|
|
141
|
+
const mainShell = window.SootSim?.bridges?.mainShell
|
|
142
|
+
const kb = window.__sootsimKeyboard
|
|
143
|
+
if (!t) return { error: 'no test bridge' }
|
|
144
|
+
|
|
145
|
+
let shell = null
|
|
146
|
+
try {
|
|
147
|
+
shell = typeof mainShell?.getState === 'function' ? await mainShell.getState() : null
|
|
148
|
+
} catch {}
|
|
149
|
+
|
|
150
|
+
const tree = await t.dumpTree(12, ${JSON.stringify(dumpOpts)})
|
|
151
|
+
const nodeCount = (await t.getNodeCount?.()) || 0
|
|
152
|
+
const keyboard = kb && typeof kb.getLayout === 'function' ? kb.getLayout() : null
|
|
153
|
+
return { tree, shell, nodeCount, keyboard }
|
|
154
|
+
})()`;
|
|
155
|
+
const result = await bridge.send({ type: "evaluate", code });
|
|
156
|
+
return result ?? {};
|
|
157
|
+
}
|
|
158
|
+
var INSPECT_A11Y_CODE = `(async () => {
|
|
159
|
+
const t = window.__sootsimTest
|
|
160
|
+
if (!t) return []
|
|
161
|
+
|
|
162
|
+
const readLayout = (n) => n?.layout || null
|
|
163
|
+
const readAbs = (n) => n?.absolutePosition || n?.absolute || null
|
|
164
|
+
const readRole = (n) => n?.accessibilityRole || n?.role || null
|
|
165
|
+
const readLabel = (n) => n?.accessibilityLabel || n?.label || n?.text || null
|
|
166
|
+
const readHint = (n) => n?.accessibilityHint || n?.hint || null
|
|
167
|
+
const readState = (n) => n?.accessibilityState || n?.state || null
|
|
168
|
+
const readTestId = (n) => n?.testID || n?.testId || null
|
|
169
|
+
const readType = (n) => typeof n?.type === 'string' ? n.type.toLowerCase() : n?.type
|
|
170
|
+
const readPressable = (n) => {
|
|
171
|
+
if (n?.pressable) return true
|
|
172
|
+
const handlers = Array.isArray(n?.handlers) ? n.handlers : []
|
|
173
|
+
const role = readRole(n)
|
|
174
|
+
return handlers.length > 0 || role === 'button' || role === 'link' || role === 'tab'
|
|
175
|
+
}
|
|
176
|
+
const isTextInput = (n) => n?.isTextInput === true
|
|
177
|
+
const isTextNode = (n) => readType(n) === 'text'
|
|
178
|
+
const isVisibleTarget = (n) => {
|
|
179
|
+
const layout = readLayout(n)
|
|
180
|
+
if (!layout || layout.width <= 0 || layout.height <= 0) return false
|
|
181
|
+
const abs = readAbs(n)
|
|
182
|
+
if (!abs) return true
|
|
183
|
+
const device = n?.device || {}
|
|
184
|
+
const screenW = Number(device.width) || window.innerWidth || 0
|
|
185
|
+
const screenH = Number(device.height) || window.innerHeight || 0
|
|
186
|
+
return (
|
|
187
|
+
abs.x + layout.width > 0 &&
|
|
188
|
+
abs.y + layout.height > 0 &&
|
|
189
|
+
abs.x < screenW &&
|
|
190
|
+
abs.y < screenH
|
|
191
|
+
)
|
|
192
|
+
}
|
|
193
|
+
const hasAccessibleSignal = (n) => {
|
|
194
|
+
const role = readRole(n)
|
|
195
|
+
const label = readLabel(n)
|
|
196
|
+
const hint = readHint(n)
|
|
197
|
+
if (role) return true
|
|
198
|
+
if (hint) return true
|
|
199
|
+
if (isTextInput(n)) return true
|
|
200
|
+
if (readPressable(n)) return true
|
|
201
|
+
if (isTextNode(n) && n?.text) return true
|
|
202
|
+
if (typeof label === 'string' && label.length <= 30 && label !== n?.text) return true
|
|
203
|
+
return false
|
|
204
|
+
}
|
|
205
|
+
const normalize = (n) => {
|
|
206
|
+
const layout = readLayout(n)
|
|
207
|
+
const abs = readAbs(n)
|
|
208
|
+
const role = readRole(n) || (readPressable(n) ? 'button' : isTextInput(n) ? 'textfield' : isTextNode(n) ? 'statictext' : 'none')
|
|
209
|
+
return {
|
|
210
|
+
role,
|
|
211
|
+
label: readLabel(n),
|
|
212
|
+
hint: readHint(n),
|
|
213
|
+
state: readState(n),
|
|
214
|
+
testID: readTestId(n),
|
|
215
|
+
position: abs ? { x: Math.round(abs.x), y: Math.round(abs.y) } : null,
|
|
216
|
+
size: layout ? { w: Math.round(layout.width), h: Math.round(layout.height) } : null,
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (typeof t.listInspectable === 'function') {
|
|
221
|
+
const list = await t.listInspectable({})
|
|
222
|
+
if (Array.isArray(list)) {
|
|
223
|
+
return list.filter(n => isVisibleTarget(n) && hasAccessibleSignal(n)).map(normalize)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const all = await t.queryAll({ pruneHidden: true })
|
|
228
|
+
return all
|
|
229
|
+
.filter(n => isVisibleTarget(n) && hasAccessibleSignal(n))
|
|
230
|
+
.map(normalize)
|
|
231
|
+
})()`;
|
|
232
|
+
async function inspectAccessibilityTree(bridge) {
|
|
233
|
+
const nodes = await bridge.send({ type: "evaluate", code: INSPECT_A11Y_CODE });
|
|
234
|
+
return Array.isArray(nodes) ? nodes : [];
|
|
235
|
+
}
|
|
236
|
+
var FIND_INSPECTABLE_TARGETS = `
|
|
237
|
+
const fromInspectable = async () => {
|
|
238
|
+
if (typeof t.listInspectable !== 'function') return null
|
|
239
|
+
const list = await t.listInspectable({})
|
|
240
|
+
if (!Array.isArray(list)) return null
|
|
241
|
+
return list.map((n) => {
|
|
242
|
+
const role = n.accessibilityRole ?? n.role
|
|
243
|
+
const label = n.accessibilityLabel ?? n.label
|
|
244
|
+
const layout = n.layout
|
|
245
|
+
const absolutePosition = n.absolutePosition ?? n.absolute
|
|
246
|
+
const screenW = Number(n.device?.width) || window.innerWidth || 0
|
|
247
|
+
const screenH = Number(n.device?.height) || window.innerHeight || 0
|
|
248
|
+
const visibleFrame = layout && absolutePosition
|
|
249
|
+
? {
|
|
250
|
+
x: Math.max(0, absolutePosition.x),
|
|
251
|
+
y: Math.max(0, absolutePosition.y),
|
|
252
|
+
width: Math.max(
|
|
253
|
+
0,
|
|
254
|
+
Math.min(screenW, absolutePosition.x + layout.width) -
|
|
255
|
+
Math.max(0, absolutePosition.x),
|
|
256
|
+
),
|
|
257
|
+
height: Math.max(
|
|
258
|
+
0,
|
|
259
|
+
Math.min(screenH, absolutePosition.y + layout.height) -
|
|
260
|
+
Math.max(0, absolutePosition.y),
|
|
261
|
+
),
|
|
262
|
+
}
|
|
263
|
+
: null
|
|
264
|
+
const handlers = Array.isArray(n.handlers) ? n.handlers : []
|
|
265
|
+
const pressable =
|
|
266
|
+
handlers.length > 0 ||
|
|
267
|
+
role === 'button' ||
|
|
268
|
+
role === 'link' ||
|
|
269
|
+
role === 'tab'
|
|
270
|
+
return {
|
|
271
|
+
type: typeof n.type === 'string' ? n.type.toLowerCase() : n.type,
|
|
272
|
+
id: n.id ?? n.nodeId,
|
|
273
|
+
nodeId: n.nodeId,
|
|
274
|
+
testID: n.testID ?? n.testId,
|
|
275
|
+
text: n.text,
|
|
276
|
+
layout,
|
|
277
|
+
absolutePosition,
|
|
278
|
+
visibleFrame,
|
|
279
|
+
style: n.computedStyle ?? n.style ?? {},
|
|
280
|
+
childCount: n.childCount ?? 0,
|
|
281
|
+
pressed: n.pressed ?? false,
|
|
282
|
+
pressable,
|
|
283
|
+
isTextInput: n.isTextInput ?? false,
|
|
284
|
+
accessible: n.accessible ?? true,
|
|
285
|
+
accessibilityLabel: label,
|
|
286
|
+
accessibilityRole: role,
|
|
287
|
+
accessibilityHint: n.accessibilityHint,
|
|
288
|
+
accessibilityState: n.accessibilityState,
|
|
289
|
+
accessibilityValue: n.accessibilityValue,
|
|
290
|
+
}
|
|
291
|
+
})
|
|
292
|
+
}
|
|
293
|
+
const isVisibleTarget = (n) => {
|
|
294
|
+
if (!n?.layout) return false
|
|
295
|
+
const frame = n.visibleFrame
|
|
296
|
+
if (frame) return frame.width > 0 && frame.height > 0
|
|
297
|
+
const abs = n.absolutePosition
|
|
298
|
+
if (!abs) return n.layout.width > 0 && n.layout.height > 0
|
|
299
|
+
const screenW = window.innerWidth || 0
|
|
300
|
+
const screenH = window.innerHeight || 0
|
|
301
|
+
return (
|
|
302
|
+
abs.x + n.layout.width > 0 &&
|
|
303
|
+
abs.y + n.layout.height > 0 &&
|
|
304
|
+
abs.x < screenW &&
|
|
305
|
+
abs.y < screenH
|
|
306
|
+
)
|
|
307
|
+
}
|
|
308
|
+
`;
|
|
309
|
+
function resolveFindMode(q) {
|
|
310
|
+
if (q.testId) {
|
|
311
|
+
return {
|
|
312
|
+
mode: "testid",
|
|
313
|
+
code: `(async () => {
|
|
314
|
+
const t = window.__sootsimTest
|
|
315
|
+
return (await t.findByTestId(${JSON.stringify(q.testId)})) || (await t.findById(${JSON.stringify(q.testId)}))
|
|
316
|
+
})()`
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
if (q.role) {
|
|
320
|
+
return {
|
|
321
|
+
mode: "role",
|
|
322
|
+
code: `(async () => await window.__sootsimTest.queryAll({ hasRole: ${JSON.stringify(q.role)}, pruneHidden: true }))()`
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
if (q.type) {
|
|
326
|
+
return {
|
|
327
|
+
mode: "type",
|
|
328
|
+
code: `(async () => await window.__sootsimTest.queryAll({ type: ${JSON.stringify(q.type)}, pruneHidden: true }))()`
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
if (q.pressable) {
|
|
332
|
+
return {
|
|
333
|
+
mode: "pressable",
|
|
334
|
+
code: `(async () => {
|
|
335
|
+
const t = window.__sootsimTest
|
|
336
|
+
${FIND_INSPECTABLE_TARGETS}
|
|
337
|
+
const inspectable = await fromInspectable()
|
|
338
|
+
if (inspectable) return inspectable.filter(n => n.pressable && isVisibleTarget(n))
|
|
339
|
+
const all = await t.queryAll({ pruneHidden: true })
|
|
340
|
+
return all.filter(n => n.pressable && isVisibleTarget(n))
|
|
341
|
+
})()`
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
if (q.interactive) {
|
|
345
|
+
return {
|
|
346
|
+
mode: "interactive-targets",
|
|
347
|
+
code: `(async () => {
|
|
348
|
+
const t = window.__sootsimTest
|
|
349
|
+
${FIND_INSPECTABLE_TARGETS}
|
|
350
|
+
const inspectable = await fromInspectable()
|
|
351
|
+
if (inspectable) {
|
|
352
|
+
return inspectable.filter(n => n.pressable && isVisibleTarget(n))
|
|
353
|
+
}
|
|
354
|
+
const all = await t.queryAll({ pruneHidden: true })
|
|
355
|
+
return all.filter(n => n.pressable && isVisibleTarget(n))
|
|
356
|
+
})()`
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
if (q.visible) {
|
|
360
|
+
return {
|
|
361
|
+
mode: "visible",
|
|
362
|
+
code: `(async () => {
|
|
363
|
+
const all = await window.__sootsimTest.queryAll({ pruneHidden: true })
|
|
364
|
+
return all.filter(n => n.layout && n.layout.width > 0 && n.layout.height > 0)
|
|
365
|
+
})()`
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
if (q.text) {
|
|
369
|
+
return {
|
|
370
|
+
mode: "text",
|
|
371
|
+
code: `(async () => await window.__sootsimTest.findByText(${JSON.stringify(q.text)}))()`
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
async function inspectFind(bridge, q) {
|
|
377
|
+
const resolved = resolveFindMode(q);
|
|
378
|
+
if (!resolved) return null;
|
|
379
|
+
const result = await bridge.send({ type: "evaluate", code: resolved.code });
|
|
380
|
+
return { mode: resolved.mode, result };
|
|
381
|
+
}
|
|
382
|
+
function rankInteractive(nodes) {
|
|
383
|
+
return [...nodes].sort((a, b) => scoreInteractive(b) - scoreInteractive(a));
|
|
384
|
+
}
|
|
385
|
+
function scoreInteractive(n) {
|
|
386
|
+
let score = 0;
|
|
387
|
+
if (n.testID) score += 100;
|
|
388
|
+
if (typeof n.text === "string" && n.text.trim().length > 0) score += 60;
|
|
389
|
+
if (typeof n.accessibilityLabel === "string" && n.accessibilityLabel.trim().length > 0) {
|
|
390
|
+
score += 30;
|
|
391
|
+
}
|
|
392
|
+
if (n.accessibilityRole) score += 15;
|
|
393
|
+
const w = n.layout?.width ?? 0;
|
|
394
|
+
const h = n.layout?.height ?? 0;
|
|
395
|
+
const area = w * h;
|
|
396
|
+
if (area >= 400 && area <= 6e4) score += 25;
|
|
397
|
+
else if (area > 6e4) score -= 20;
|
|
398
|
+
const y = n.absolutePosition?.y ?? 0;
|
|
399
|
+
if (y < 0) score -= 30;
|
|
400
|
+
return score;
|
|
401
|
+
}
|
|
402
|
+
function tapCommandForNode(n) {
|
|
403
|
+
if (n.testID) return `sootsim do tap-id ${shellEscape(n.testID)}`;
|
|
404
|
+
const text = typeof n.text === "string" ? n.text.trim() : "";
|
|
405
|
+
if (text.length > 0 && text.length <= 80)
|
|
406
|
+
return `sootsim do tap-text ${shellEscape(text)}`;
|
|
407
|
+
const x = Math.round(((n.absolutePosition?.x ?? 0) + (n.layout?.width ?? 0) / 2) * 10) / 10;
|
|
408
|
+
const y = Math.round(((n.absolutePosition?.y ?? 0) + (n.layout?.height ?? 0) / 2) * 10) / 10;
|
|
409
|
+
return `sootsim do tap ${x} ${y}`;
|
|
410
|
+
}
|
|
411
|
+
function shellEscape(value) {
|
|
412
|
+
if (/^[A-Za-z0-9_./@:-]+$/.test(value)) return value;
|
|
413
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
414
|
+
}
|
|
415
|
+
var WAIT_READY_PROBE = `(async () => {
|
|
416
|
+
const t = window.__sootsimTest
|
|
417
|
+
let nodes = 0
|
|
418
|
+
try { nodes = (await t?.getNodeCount?.()) || 0 } catch {}
|
|
419
|
+
let targets = 0
|
|
420
|
+
let loadingText = ''
|
|
421
|
+
try {
|
|
422
|
+
if (typeof t?.listInspectable === 'function') {
|
|
423
|
+
const list = await t.listInspectable({})
|
|
424
|
+
if (Array.isArray(list)) {
|
|
425
|
+
const screenW = window.innerWidth || 0
|
|
426
|
+
const screenH = window.innerHeight || 0
|
|
427
|
+
const isVisible = (n) => {
|
|
428
|
+
const layout = n?.layout
|
|
429
|
+
if (!layout || layout.width <= 0 || layout.height <= 0) return false
|
|
430
|
+
const abs = n?.absolutePosition || n?.absolute
|
|
431
|
+
if (!abs) return true
|
|
432
|
+
const device = n?.device || {}
|
|
433
|
+
const w = Number(device.width) || screenW
|
|
434
|
+
const h = Number(device.height) || screenH
|
|
435
|
+
return (
|
|
436
|
+
abs.x + layout.width > 0 &&
|
|
437
|
+
abs.y + layout.height > 0 &&
|
|
438
|
+
abs.x < w &&
|
|
439
|
+
abs.y < h
|
|
440
|
+
)
|
|
441
|
+
}
|
|
442
|
+
const hasContentSignal = (n) => {
|
|
443
|
+
const role = n?.accessibilityRole || n?.role
|
|
444
|
+
const label = n?.accessibilityLabel || n?.label
|
|
445
|
+
const handlers = Array.isArray(n?.handlers) ? n.handlers : []
|
|
446
|
+
const text = typeof n?.text === 'string' ? n.text.trim() : ''
|
|
447
|
+
const testID = typeof (n?.testID || n?.testId) === 'string' ? (n.testID || n.testId) : ''
|
|
448
|
+
if (!loadingText && /^(opening app|loading|capturing|connecting)$/i.test(text)) {
|
|
449
|
+
loadingText = text
|
|
450
|
+
}
|
|
451
|
+
if (handlers.length > 0) return true
|
|
452
|
+
if (role) return true
|
|
453
|
+
if (n?.isTextInput) return true
|
|
454
|
+
if (text) return true
|
|
455
|
+
if (label) return true
|
|
456
|
+
if (testID && !/^splash|loading/i.test(testID)) return true
|
|
457
|
+
return false
|
|
458
|
+
}
|
|
459
|
+
targets = list.filter(n => isVisible(n) && hasContentSignal(n)).length
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
} catch {}
|
|
463
|
+
let externalReady = null
|
|
464
|
+
let externalStatus = ''
|
|
465
|
+
let externalError = ''
|
|
466
|
+
try {
|
|
467
|
+
const getExternalAppState = t?.getExternalAppState
|
|
468
|
+
const external = typeof getExternalAppState === 'function'
|
|
469
|
+
? await Promise.race([
|
|
470
|
+
getExternalAppState(),
|
|
471
|
+
new Promise((resolve) => setTimeout(
|
|
472
|
+
() => resolve({
|
|
473
|
+
state: {
|
|
474
|
+
ready: false,
|
|
475
|
+
loading: true,
|
|
476
|
+
status: 'waiting for tenant worker...',
|
|
477
|
+
},
|
|
478
|
+
}),
|
|
479
|
+
1000,
|
|
480
|
+
)),
|
|
481
|
+
])
|
|
482
|
+
: null
|
|
483
|
+
const state = external?.state
|
|
484
|
+
if (state && typeof state === 'object') {
|
|
485
|
+
externalReady = state.ready === true
|
|
486
|
+
if (typeof state.status === 'string') externalStatus = state.status
|
|
487
|
+
if (typeof state.error === 'string') externalError = state.error
|
|
488
|
+
}
|
|
489
|
+
if (!externalError && Array.isArray(external?.entryErrors)) {
|
|
490
|
+
const entry = external.entryErrors.find((item) => {
|
|
491
|
+
return item && typeof item.message === 'string' && item.message.trim()
|
|
492
|
+
})
|
|
493
|
+
if (entry) externalError = entry.message
|
|
494
|
+
}
|
|
495
|
+
if (externalReady === false && externalStatus) loadingText = externalStatus
|
|
496
|
+
} catch {}
|
|
497
|
+
let errors = 0
|
|
498
|
+
try { errors = window.__sootsimConsole?.count?.()?.errors ?? 0 } catch {}
|
|
499
|
+
return {
|
|
500
|
+
flag: (window).__sootsimExternalAppReady,
|
|
501
|
+
at: (window).__sootsimExternalAppReadyAt || 0,
|
|
502
|
+
nodes,
|
|
503
|
+
targets,
|
|
504
|
+
errors,
|
|
505
|
+
loadingText,
|
|
506
|
+
externalReady,
|
|
507
|
+
externalStatus,
|
|
508
|
+
externalError,
|
|
509
|
+
}
|
|
510
|
+
})()`;
|
|
511
|
+
var READY_CONTENT_NODE_FLOOR = 100;
|
|
512
|
+
var READY_NODE_STABLE_MS = 750;
|
|
513
|
+
var WAIT_READY_PROGRESS_INTERVAL_MS = 2e3;
|
|
514
|
+
function readyProbeHasContent(probe) {
|
|
515
|
+
return probe.targets > 0 || probe.nodes >= READY_CONTENT_NODE_FLOOR;
|
|
516
|
+
}
|
|
517
|
+
function waitReadyReason(status) {
|
|
518
|
+
return status.externalError ? `guest app errored: ${status.externalError}` : status.loadingText ? `still showing "${status.loadingText}"` : status.externalReady === false ? "guest app is still loading" : 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";
|
|
519
|
+
}
|
|
520
|
+
async function inspectWaitReady(bridge, timeoutMs = 2e4, options = {}) {
|
|
521
|
+
const start = Date.now();
|
|
522
|
+
const deadline = start + timeoutMs;
|
|
523
|
+
const progressIntervalMs = options.progressIntervalMs ?? WAIT_READY_PROGRESS_INTERVAL_MS;
|
|
524
|
+
let nextProgressAt = start + progressIntervalMs;
|
|
525
|
+
let lastNodes = -1;
|
|
526
|
+
let nodeStableSince = start;
|
|
527
|
+
let last = {
|
|
528
|
+
flag: void 0,
|
|
529
|
+
at: 0,
|
|
530
|
+
nodes: 0,
|
|
531
|
+
targets: 0,
|
|
532
|
+
errors: 0,
|
|
533
|
+
loadingText: "",
|
|
534
|
+
externalReady: null,
|
|
535
|
+
externalStatus: "",
|
|
536
|
+
externalError: ""
|
|
537
|
+
};
|
|
538
|
+
while (Date.now() < deadline) {
|
|
539
|
+
try {
|
|
540
|
+
last = await bridge.send({ type: "evaluate", code: WAIT_READY_PROBE }) ?? last;
|
|
541
|
+
} catch {
|
|
542
|
+
}
|
|
543
|
+
const now = Date.now();
|
|
544
|
+
const status = {
|
|
545
|
+
ready: false,
|
|
546
|
+
elapsedMs: now - start,
|
|
547
|
+
nodes: last.nodes,
|
|
548
|
+
targets: last.targets,
|
|
549
|
+
flag: last.flag,
|
|
550
|
+
loadingText: last.loadingText,
|
|
551
|
+
externalReady: last.externalReady,
|
|
552
|
+
externalStatus: last.externalStatus,
|
|
553
|
+
externalError: last.externalError,
|
|
554
|
+
errors: last.errors
|
|
555
|
+
};
|
|
556
|
+
if (last.nodes !== lastNodes) {
|
|
557
|
+
lastNodes = last.nodes;
|
|
558
|
+
nodeStableSince = now;
|
|
559
|
+
}
|
|
560
|
+
if (last.flag === true) {
|
|
561
|
+
if (last.externalReady !== false && !last.externalError && !last.loadingText && readyProbeHasContent(last)) {
|
|
562
|
+
if (now - nodeStableSince >= READY_NODE_STABLE_MS) {
|
|
563
|
+
return {
|
|
564
|
+
...status,
|
|
565
|
+
ready: true
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
if (options.onProgress && progressIntervalMs > 0 && now >= nextProgressAt) {
|
|
571
|
+
options.onProgress(status);
|
|
572
|
+
do {
|
|
573
|
+
nextProgressAt += progressIntervalMs;
|
|
574
|
+
} while (now >= nextProgressAt);
|
|
575
|
+
}
|
|
576
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
577
|
+
}
|
|
578
|
+
return {
|
|
579
|
+
ready: false,
|
|
580
|
+
elapsedMs: Date.now() - start,
|
|
581
|
+
nodes: last.nodes,
|
|
582
|
+
targets: last.targets,
|
|
583
|
+
flag: last.flag,
|
|
584
|
+
loadingText: last.loadingText,
|
|
585
|
+
externalReady: last.externalReady,
|
|
586
|
+
externalStatus: last.externalStatus,
|
|
587
|
+
externalError: last.externalError,
|
|
588
|
+
errors: last.errors
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
async function inspectWaitSelector(bridge, testId, timeoutMs = 5e3, opts = {}) {
|
|
592
|
+
const gone = opts.gone === true;
|
|
593
|
+
const result = await bridge.send(
|
|
594
|
+
{
|
|
595
|
+
type: "evaluate",
|
|
596
|
+
code: `(async () => {
|
|
597
|
+
const start = Date.now()
|
|
598
|
+
const deadline = start + ${timeoutMs}
|
|
599
|
+
const gone = ${gone}
|
|
600
|
+
const ID = ${JSON.stringify(testId)}
|
|
601
|
+
const present = (node) => !!(node && node.layout && node.layout.width > 0 && node.layout.height > 0)
|
|
602
|
+
const find = async () => {
|
|
603
|
+
const t = window.__sootsimTest
|
|
604
|
+
if (!t) return undefined // bridge not ready \u2014 indeterminate, not "gone"
|
|
605
|
+
try {
|
|
606
|
+
const matches = await t.queryAll?.({ hasId: ID, pruneHidden: true })
|
|
607
|
+
if (!Array.isArray(matches)) return undefined
|
|
608
|
+
return matches.find(present) || null
|
|
609
|
+
} catch {
|
|
610
|
+
return undefined
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
let goneStreak = 0
|
|
614
|
+
while (Date.now() < deadline) {
|
|
615
|
+
const node = await find()
|
|
616
|
+
if (gone) {
|
|
617
|
+
if (node === null) {
|
|
618
|
+
goneStreak += 1
|
|
619
|
+
if (goneStreak >= 2) return { found: true, elapsed: Date.now() - start }
|
|
620
|
+
} else {
|
|
621
|
+
goneStreak = 0
|
|
622
|
+
}
|
|
623
|
+
} else if (present(node)) {
|
|
624
|
+
return { found: true, node, elapsed: Date.now() - start }
|
|
625
|
+
}
|
|
626
|
+
await new Promise((r) => setTimeout(r, 80))
|
|
627
|
+
}
|
|
628
|
+
return { found: false, elapsed: Date.now() - start }
|
|
629
|
+
})()`
|
|
630
|
+
},
|
|
631
|
+
{
|
|
632
|
+
timeoutMs: timeoutMs + 1e3
|
|
633
|
+
}
|
|
634
|
+
);
|
|
635
|
+
return result ?? { found: false, elapsed: timeoutMs };
|
|
636
|
+
}
|
|
637
|
+
async function inspectErrors(bridge, limit = 20) {
|
|
638
|
+
const result = await bridge.send({
|
|
639
|
+
type: "evaluate",
|
|
640
|
+
code: `window.__sootsimConsole?.getErrors(${limit}) || []`
|
|
641
|
+
});
|
|
642
|
+
return Array.isArray(result) ? result : [];
|
|
643
|
+
}
|
|
644
|
+
async function inspectWarnings(bridge, limit = 20) {
|
|
645
|
+
const result = await bridge.send({
|
|
646
|
+
type: "evaluate",
|
|
647
|
+
code: `window.__sootsimConsole?.getWarnings(${limit}) || []`
|
|
648
|
+
});
|
|
649
|
+
return Array.isArray(result) ? result : [];
|
|
650
|
+
}
|
|
651
|
+
async function clearConsole(bridge) {
|
|
652
|
+
await bridge.send({
|
|
653
|
+
type: "evaluate",
|
|
654
|
+
code: 'window.__sootsimConsole?.clear(); "cleared"'
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
async function inspectRequests(bridge, opts = {}) {
|
|
658
|
+
const limit = opts.limit ?? 20;
|
|
659
|
+
const method = opts.failed === false ? "getRequests" : "getFailedRequests";
|
|
660
|
+
const result = await bridge.send({
|
|
661
|
+
type: "call",
|
|
662
|
+
path: `__sootsimTest.${method}`,
|
|
663
|
+
args: [limit]
|
|
664
|
+
});
|
|
665
|
+
return Array.isArray(result) ? result : [];
|
|
666
|
+
}
|
|
667
|
+
async function clearRequests(bridge) {
|
|
668
|
+
await bridge.send({ type: "call", path: "__sootsimTest.clearRequests", args: [] });
|
|
669
|
+
}
|
|
670
|
+
async function inspectScrollStateAt(bridge, x, y) {
|
|
671
|
+
const result = await bridge.send({
|
|
672
|
+
type: "call",
|
|
673
|
+
path: "__sootsimTest.getScrollStateAt",
|
|
674
|
+
args: [x, y]
|
|
675
|
+
});
|
|
676
|
+
return result && typeof result === "object" ? result : null;
|
|
677
|
+
}
|
|
678
|
+
async function inspectLogs(bridge) {
|
|
679
|
+
const res = await bridge.send({
|
|
680
|
+
type: "evaluate",
|
|
681
|
+
code: `(() => {
|
|
682
|
+
const obs = window.__sootsimObservability;
|
|
683
|
+
if (!obs) return { ok: false };
|
|
684
|
+
return { ok: true, entries: obs.logs.getSnapshot() };
|
|
685
|
+
})()`
|
|
686
|
+
});
|
|
687
|
+
if (!res || !res.ok) return [];
|
|
688
|
+
return res.entries ?? [];
|
|
689
|
+
}
|
|
690
|
+
async function clearLogs(bridge) {
|
|
691
|
+
await bridge.send({
|
|
692
|
+
type: "evaluate",
|
|
693
|
+
code: 'window.__sootsimObservability?.logs.clear(); "cleared"'
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
function isForwardedWorkerLogTwin(a, b) {
|
|
697
|
+
if (a.source === b.source) return false;
|
|
698
|
+
if (a.level !== b.level) return false;
|
|
699
|
+
if (Math.abs(a.ts - b.ts) > 1e3) return false;
|
|
700
|
+
if (a.args.length !== b.args.length) return false;
|
|
701
|
+
const sources = /* @__PURE__ */ new Set([a.source, b.source]);
|
|
702
|
+
if (!sources.has("sootsim-worker")) return false;
|
|
703
|
+
if (!sources.has("render-worker") && !sources.has("forwarded-render-worker")) {
|
|
704
|
+
return false;
|
|
705
|
+
}
|
|
706
|
+
return a.args.every((arg, index) => arg === b.args[index]);
|
|
707
|
+
}
|
|
708
|
+
function filterLogEntries(entries, opts = {}) {
|
|
709
|
+
let out = [];
|
|
710
|
+
for (const entry of entries) {
|
|
711
|
+
if (out.some((candidate) => isForwardedWorkerLogTwin(candidate, entry))) {
|
|
712
|
+
continue;
|
|
713
|
+
}
|
|
714
|
+
out.push(entry);
|
|
715
|
+
}
|
|
716
|
+
if (!opts.showInternal) {
|
|
717
|
+
out = out.filter((e) => {
|
|
718
|
+
const first = e.args[0];
|
|
719
|
+
return !(typeof first === "string" && first.startsWith("[sootsim]"));
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
if (opts.level) out = out.filter((e) => opts.level.has(e.level));
|
|
723
|
+
if (opts.filter) {
|
|
724
|
+
const lf = opts.filter.toLowerCase();
|
|
725
|
+
out = out.filter((e) => e.args.join(" ").toLowerCase().includes(lf));
|
|
726
|
+
}
|
|
727
|
+
return out;
|
|
728
|
+
}
|
|
729
|
+
async function inspectTimelineSummary(bridge, query) {
|
|
730
|
+
return await bridge.send({
|
|
731
|
+
type: "call",
|
|
732
|
+
path: "SootSim.bridges.timeline.summary",
|
|
733
|
+
args: [query]
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
async function inspectTimelineRecent(bridge, query) {
|
|
737
|
+
return await bridge.send({
|
|
738
|
+
type: "call",
|
|
739
|
+
path: "SootSim.bridges.timeline.recent",
|
|
740
|
+
args: [query]
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
async function inspectTimelineAdvanceCursor(bridge, cursorKey, watermark) {
|
|
744
|
+
await bridge.send({
|
|
745
|
+
type: "call",
|
|
746
|
+
path: "SootSim.bridges.timeline.cursorAdvance",
|
|
747
|
+
args: [cursorKey, watermark]
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
async function inspectKeyboard(bridge) {
|
|
751
|
+
const result = await bridge.send({
|
|
752
|
+
type: "evaluate",
|
|
753
|
+
code: `(() => {
|
|
754
|
+
const kb = window.__sootsimKeyboard
|
|
755
|
+
if (!kb || typeof kb.getLayout !== 'function') {
|
|
756
|
+
return { error: 'keyboard bridge getLayout() not available' }
|
|
757
|
+
}
|
|
758
|
+
return kb.getLayout()
|
|
759
|
+
})()`
|
|
760
|
+
});
|
|
761
|
+
return result ?? { error: "keyboard bridge returned no result" };
|
|
762
|
+
}
|
|
763
|
+
function isShellCommandUnavailable(error) {
|
|
764
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
765
|
+
return message.includes("call target not found: SootSim.bridges.mainShell") || message.includes("test bridge unavailable before app-in-worker boot");
|
|
766
|
+
}
|
|
767
|
+
async function getShellState(bridge, readyTimeoutMs = 0) {
|
|
768
|
+
const deadline = Date.now() + Math.max(0, readyTimeoutMs);
|
|
769
|
+
while (true) {
|
|
770
|
+
try {
|
|
771
|
+
return await bridge.send({
|
|
772
|
+
type: "call",
|
|
773
|
+
path: "SootSim.bridges.mainShell.getState",
|
|
774
|
+
args: []
|
|
775
|
+
});
|
|
776
|
+
} catch (error) {
|
|
777
|
+
if (!isShellCommandUnavailable(error) || Date.now() >= deadline) throw error;
|
|
778
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
async function inspectScreens(bridge) {
|
|
783
|
+
const payload = await bridge.send({
|
|
784
|
+
type: "evaluate",
|
|
785
|
+
code: `(async () => {
|
|
786
|
+
const test = window.__sootsimTest
|
|
787
|
+
const kb = window.__sootsimKeyboard
|
|
788
|
+
const navSnap =
|
|
789
|
+
test && typeof test.getNavigationSnapshot === 'function'
|
|
790
|
+
? await test.getNavigationSnapshot()
|
|
791
|
+
: null
|
|
792
|
+
const keyboard =
|
|
793
|
+
kb && typeof kb.getLayout === 'function'
|
|
794
|
+
? (() => {
|
|
795
|
+
const layout = kb.getLayout()
|
|
796
|
+
return layout ? {
|
|
797
|
+
visible: layout.visible,
|
|
798
|
+
mode: layout.mode,
|
|
799
|
+
spec: layout.spec
|
|
800
|
+
? {
|
|
801
|
+
keyboardType: layout.spec.keyboardType,
|
|
802
|
+
returnKeyType: layout.spec.returnKeyType,
|
|
803
|
+
}
|
|
804
|
+
: null,
|
|
805
|
+
} : null
|
|
806
|
+
})()
|
|
807
|
+
: null
|
|
808
|
+
return { nav: navSnap, keyboard }
|
|
809
|
+
})()`
|
|
810
|
+
});
|
|
811
|
+
const shell = await getShellState(bridge, 500).catch(() => null);
|
|
812
|
+
return {
|
|
813
|
+
shell,
|
|
814
|
+
nav: payload?.nav ?? null,
|
|
815
|
+
keyboard: payload?.keyboard ?? null
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
var DEBUG_CHANNELS = [
|
|
819
|
+
"portals",
|
|
820
|
+
"sheets",
|
|
821
|
+
"layout",
|
|
822
|
+
"onlayout",
|
|
823
|
+
"animated",
|
|
824
|
+
"render",
|
|
825
|
+
"touch",
|
|
826
|
+
"yoga"
|
|
827
|
+
];
|
|
828
|
+
async function inspectDebugStatus(bridge) {
|
|
829
|
+
return bridge.send({ type: "evaluate", code: "window.__sootsimDebug.status()" });
|
|
830
|
+
}
|
|
831
|
+
async function inspectDebugFlags(bridge) {
|
|
832
|
+
return bridge.send({ type: "evaluate", code: "window.__sootsimDebug.flags()" });
|
|
833
|
+
}
|
|
834
|
+
async function inspectDebugFind(bridge, target) {
|
|
835
|
+
const method = target === "sheets" ? "findSheets" : "findPortals";
|
|
836
|
+
return bridge.send({
|
|
837
|
+
type: "evaluate",
|
|
838
|
+
code: `window.__sootsimDebug.${method}()`
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
async function inspectDebugRecent(bridge, channel, limit = 50) {
|
|
842
|
+
const code = channel && channel !== "all" ? `window.__sootsimDebug.recent(${JSON.stringify(channel)}, ${limit})` : `window.__sootsimDebug.recent(undefined, ${limit})`;
|
|
843
|
+
return bridge.send({ type: "evaluate", code });
|
|
844
|
+
}
|
|
845
|
+
async function setDebugChannels(bridge, action, channels) {
|
|
846
|
+
const args = channels.length > 0 ? channels.map((c) => JSON.stringify(c)).join(", ") : action === "disable" ? "'all'" : "";
|
|
847
|
+
return bridge.send({
|
|
848
|
+
type: "evaluate",
|
|
849
|
+
code: `window.__sootsimDebug.${action}(${args})`
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
async function inspectMemory(bridge) {
|
|
853
|
+
const result = await bridge.send({
|
|
854
|
+
type: "evaluate",
|
|
855
|
+
code: `(async () => {
|
|
856
|
+
const host = window.__sootsimRenderHost
|
|
857
|
+
const stats = host?.queryStats ? await host.queryStats() : null
|
|
858
|
+
const hostMem = performance.memory
|
|
859
|
+
? {
|
|
860
|
+
usedJSHeapSize: performance.memory.usedJSHeapSize,
|
|
861
|
+
totalJSHeapSize: performance.memory.totalJSHeapSize,
|
|
862
|
+
jsHeapSizeLimit: performance.memory.jsHeapSizeLimit,
|
|
863
|
+
}
|
|
864
|
+
: null
|
|
865
|
+
return {
|
|
866
|
+
imageLoader: stats?.memory?.imageLoader ?? null,
|
|
867
|
+
workerHeap: stats?.memory?.workerHeap ?? null,
|
|
868
|
+
hostHeap: hostMem,
|
|
869
|
+
}
|
|
870
|
+
})()`
|
|
871
|
+
});
|
|
872
|
+
return result ?? { imageLoader: null, workerHeap: null, hostHeap: null };
|
|
873
|
+
}
|
|
874
|
+
function formatTimelineSummary(summary) {
|
|
875
|
+
if (summary.total === 0) return "nothing recorded";
|
|
876
|
+
const parts = [];
|
|
877
|
+
const ORDER = [
|
|
878
|
+
"error",
|
|
879
|
+
"warning",
|
|
880
|
+
"console",
|
|
881
|
+
"fetch",
|
|
882
|
+
"toast",
|
|
883
|
+
"alert",
|
|
884
|
+
"actionsheet",
|
|
885
|
+
"picker",
|
|
886
|
+
"notification",
|
|
887
|
+
"screen",
|
|
888
|
+
"route",
|
|
889
|
+
"keyboard",
|
|
890
|
+
"app-launch",
|
|
891
|
+
"shell",
|
|
892
|
+
"scroll",
|
|
893
|
+
"gesture",
|
|
894
|
+
"text-input",
|
|
895
|
+
"react-commit",
|
|
896
|
+
"animation",
|
|
897
|
+
"reanimated"
|
|
898
|
+
];
|
|
899
|
+
const seen = /* @__PURE__ */ new Set();
|
|
900
|
+
for (const k of ORDER) {
|
|
901
|
+
const n = summary.byKind[k];
|
|
902
|
+
if (n) {
|
|
903
|
+
parts.push(`${n} ${k}${n === 1 ? "" : "s"}`);
|
|
904
|
+
seen.add(k);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
for (const [k, n] of Object.entries(summary.byKind)) {
|
|
908
|
+
if (!seen.has(k) && n) parts.push(`${n} ${k}${n === 1 ? "" : "s"}`);
|
|
909
|
+
}
|
|
910
|
+
return parts.join(" \xB7 ");
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// cli/commands/inspect/settling.ts
|
|
914
|
+
async function waitForSootsimIdle({
|
|
915
|
+
bridge,
|
|
916
|
+
simId,
|
|
917
|
+
maxMs,
|
|
918
|
+
pollMs = 50,
|
|
919
|
+
stablePolls = 3,
|
|
920
|
+
strict = false
|
|
921
|
+
}) {
|
|
922
|
+
const result = await bridge.send(
|
|
923
|
+
{
|
|
924
|
+
type: "evaluate",
|
|
925
|
+
simId,
|
|
926
|
+
code: `(async () => {
|
|
927
|
+
const start = Date.now()
|
|
928
|
+
const deadline = start + ${Math.max(0, Math.round(maxMs))}
|
|
929
|
+
const pollMs = ${Math.max(1, Math.round(pollMs))}
|
|
930
|
+
const requiredStablePolls = ${Math.max(1, Math.round(stablePolls))}
|
|
931
|
+
const strict = ${strict ? "true" : "false"}
|
|
932
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
933
|
+
|
|
934
|
+
// route-aware settle: a tap that pushes/pops a screen is async \u2014 the
|
|
935
|
+
// old screen still renders for a moment, then the new one mounts and
|
|
936
|
+
// its content loads. a pure layout-hash check reports "idle" on the
|
|
937
|
+
// outgoing screen or on the incoming skeleton, so a driver re-taps
|
|
938
|
+
// and stacks duplicate navigations. drain any in-flight screen
|
|
939
|
+
// transition FIRST, bounded by the overall budget. when no
|
|
940
|
+
// transition is happening this returns in ~80ms (startWindowMs), so
|
|
941
|
+
// a plain button tap barely pays for it.
|
|
942
|
+
try {
|
|
943
|
+
const wfst = window.__sootsimTest?.waitForScreenTransitions
|
|
944
|
+
if (typeof wfst === 'function') {
|
|
945
|
+
const remaining = deadline - Date.now()
|
|
946
|
+
if (remaining > 120) {
|
|
947
|
+
await wfst({
|
|
948
|
+
timeoutMs: Math.min(remaining - 80, 4000),
|
|
949
|
+
settleMs: 64,
|
|
950
|
+
startWindowMs: 80,
|
|
951
|
+
})
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
} catch {}
|
|
955
|
+
|
|
956
|
+
const readSnapshot = async () => {
|
|
957
|
+
let animating = false
|
|
958
|
+
let pendingFetches = 0
|
|
959
|
+
let requestTotal = 0
|
|
960
|
+
let requestInFlight = 0
|
|
961
|
+
try {
|
|
962
|
+
const stats = await window.__sootsimRenderHost?.queryStats?.()
|
|
963
|
+
if (stats) {
|
|
964
|
+
animating =
|
|
965
|
+
stats.hasActiveAnims === true ||
|
|
966
|
+
stats.hasActiveNativeAnimations === true ||
|
|
967
|
+
stats.hasPendingAnimationFrames === true
|
|
968
|
+
// a freshly-pushed screen showing a skeleton is layout-stable
|
|
969
|
+
// but not actually settled \u2014 its data/images are still in
|
|
970
|
+
// flight. treat bounded image-loader fetches as not-idle so
|
|
971
|
+
// settle waits for real content, capped by the budget.
|
|
972
|
+
const pf = stats.memory && stats.memory.imageLoader
|
|
973
|
+
? stats.memory.imageLoader.pendingFetches
|
|
974
|
+
: 0
|
|
975
|
+
pendingFetches = typeof pf === 'number' ? pf : 0
|
|
976
|
+
}
|
|
977
|
+
} catch {}
|
|
978
|
+
try {
|
|
979
|
+
const counts = await window.__sootsimTest?.getRequestCounts?.()
|
|
980
|
+
requestTotal = typeof counts?.total === 'number' ? counts.total : 0
|
|
981
|
+
requestInFlight = typeof counts?.inFlight === 'number' ? counts.inFlight : 0
|
|
982
|
+
} catch {}
|
|
983
|
+
const root = window.__sootsimRoot
|
|
984
|
+
const nodes = []
|
|
985
|
+
if (root) {
|
|
986
|
+
const walk = (n) => {
|
|
987
|
+
if (n.layout && n.layout.width > 0) {
|
|
988
|
+
nodes.push(Math.round(n.layout.x) + ',' + Math.round(n.layout.y) + ',' + Math.round(n.layout.width))
|
|
989
|
+
}
|
|
990
|
+
for (const c of n.children || []) walk(c)
|
|
991
|
+
}
|
|
992
|
+
walk(root)
|
|
993
|
+
}
|
|
994
|
+
return { layout: nodes.join(';'), animating, pendingFetches, requestTotal, requestInFlight }
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
let lastLayout = ''
|
|
998
|
+
let lastRequestTotal = -1
|
|
999
|
+
let stable = 0
|
|
1000
|
+
while (Date.now() < deadline) {
|
|
1001
|
+
const snapshot = await readSnapshot()
|
|
1002
|
+
const strictOk = !strict || !snapshot.animating
|
|
1003
|
+
const contentReady = snapshot.pendingFetches === 0 && snapshot.requestInFlight === 0
|
|
1004
|
+
const requestsStable = snapshot.requestTotal === lastRequestTotal
|
|
1005
|
+
if (strictOk && contentReady && requestsStable && snapshot.layout === lastLayout) {
|
|
1006
|
+
stable++
|
|
1007
|
+
if (stable >= requiredStablePolls) {
|
|
1008
|
+
return { settled: true, elapsed: Date.now() - start }
|
|
1009
|
+
}
|
|
1010
|
+
} else {
|
|
1011
|
+
stable = 0
|
|
1012
|
+
}
|
|
1013
|
+
lastLayout = snapshot.layout
|
|
1014
|
+
lastRequestTotal = snapshot.requestTotal
|
|
1015
|
+
await sleep(pollMs)
|
|
1016
|
+
}
|
|
1017
|
+
return { settled: false, elapsed: Date.now() - start }
|
|
1018
|
+
})()`
|
|
1019
|
+
},
|
|
1020
|
+
{
|
|
1021
|
+
timeoutMs: maxMs + 1e3
|
|
1022
|
+
}
|
|
1023
|
+
);
|
|
1024
|
+
const { elapsed, settled } = result ?? {};
|
|
1025
|
+
return {
|
|
1026
|
+
elapsed: typeof elapsed === "number" ? elapsed : maxMs,
|
|
1027
|
+
settled: settled === true
|
|
1028
|
+
};
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
// cli/commands/inspect/actions.ts
|
|
1032
|
+
var DEFAULT_AGENT_TIMING = {
|
|
1033
|
+
initialWaitMs: 3e3,
|
|
1034
|
+
deadlineMs: 5e3,
|
|
1035
|
+
retryWaitMs: 700
|
|
1036
|
+
};
|
|
1037
|
+
var DEFAULT_INTERACTIVE_TIMING = {
|
|
1038
|
+
initialWaitMs: 1200,
|
|
1039
|
+
deadlineMs: 2500,
|
|
1040
|
+
retryWaitMs: 700
|
|
1041
|
+
};
|
|
1042
|
+
function tapTiming(agent, opts = {}) {
|
|
1043
|
+
return {
|
|
1044
|
+
...agent ? DEFAULT_AGENT_TIMING : DEFAULT_INTERACTIVE_TIMING,
|
|
1045
|
+
...opts
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
function isTapSuccess(result) {
|
|
1049
|
+
if (!result || result.hit === false || result.ok === false) return false;
|
|
1050
|
+
if (result.pointerTapHandled === false && !result.keyboardOpened && !result.isTextInput) {
|
|
1051
|
+
return false;
|
|
1052
|
+
}
|
|
1053
|
+
return true;
|
|
1054
|
+
}
|
|
1055
|
+
function tapTargetFromPayload(payload, textFallback) {
|
|
1056
|
+
return {
|
|
1057
|
+
id: payload.target?.id ?? payload.match?.id ?? payload.node?.id ?? null,
|
|
1058
|
+
testID: payload.target?.testID ?? payload.match?.testID ?? payload.node?.testID ?? null,
|
|
1059
|
+
text: payload.target?.text ?? payload.target?.accessibilityLabel ?? payload.match?.text ?? payload.match?.accessibilityLabel ?? payload.node?.text ?? textFallback ?? null,
|
|
1060
|
+
type: payload.target?.type ?? payload.match?.type ?? payload.node?.type ?? null
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
1063
|
+
async function tapResolvedTarget(bridge, args) {
|
|
1064
|
+
const timing = tapTiming(!!args.agent, args.timing);
|
|
1065
|
+
let attempts = 0;
|
|
1066
|
+
let lastPayload = null;
|
|
1067
|
+
let lastResult = null;
|
|
1068
|
+
try {
|
|
1069
|
+
await waitForSootsimIdle({
|
|
1070
|
+
bridge,
|
|
1071
|
+
maxMs: timing.initialWaitMs,
|
|
1072
|
+
pollMs: 32,
|
|
1073
|
+
stablePolls: 2
|
|
1074
|
+
});
|
|
1075
|
+
} catch {
|
|
1076
|
+
}
|
|
1077
|
+
const deadline = Date.now() + timing.deadlineMs;
|
|
1078
|
+
while (Date.now() <= deadline || attempts === 0) {
|
|
1079
|
+
attempts++;
|
|
1080
|
+
const payload = await args.resolve();
|
|
1081
|
+
lastPayload = payload;
|
|
1082
|
+
if (payload?.error === "bridge-not-ready" || payload?.ambiguous || payload?.nthOutOfRange) {
|
|
1083
|
+
return { payload, result: null, attempts, failure: "special" };
|
|
1084
|
+
}
|
|
1085
|
+
if (payload && typeof payload.cx === "number" && typeof payload.cy === "number") {
|
|
1086
|
+
const result = await bridge.send({
|
|
1087
|
+
type: "tap",
|
|
1088
|
+
x: payload.cx,
|
|
1089
|
+
y: payload.cy,
|
|
1090
|
+
target: tapTargetFromPayload(payload, args.textFallback)
|
|
1091
|
+
});
|
|
1092
|
+
lastResult = result;
|
|
1093
|
+
if (isTapSuccess(result)) return { payload, result, attempts };
|
|
1094
|
+
}
|
|
1095
|
+
const remaining = deadline - Date.now();
|
|
1096
|
+
if (remaining <= 0) break;
|
|
1097
|
+
try {
|
|
1098
|
+
await waitForSootsimIdle({
|
|
1099
|
+
bridge,
|
|
1100
|
+
maxMs: Math.min(remaining, timing.retryWaitMs),
|
|
1101
|
+
pollMs: 32,
|
|
1102
|
+
stablePolls: 2
|
|
1103
|
+
});
|
|
1104
|
+
} catch {
|
|
1105
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(120, remaining)));
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
return {
|
|
1109
|
+
payload: lastPayload,
|
|
1110
|
+
result: lastResult,
|
|
1111
|
+
attempts,
|
|
1112
|
+
failure: lastPayload && typeof lastPayload.cx === "number" ? "missed" : "not-found"
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
async function tapCoordinates(bridge, x, y, target) {
|
|
1116
|
+
return bridge.send({
|
|
1117
|
+
type: "tap",
|
|
1118
|
+
x,
|
|
1119
|
+
y,
|
|
1120
|
+
...target ? { target } : {}
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
async function tapById(bridge, query, opts = {}) {
|
|
1124
|
+
const arg = JSON.stringify(query);
|
|
1125
|
+
return tapResolvedTarget(bridge, {
|
|
1126
|
+
agent: opts.agent,
|
|
1127
|
+
timing: opts.timing,
|
|
1128
|
+
resolve: () => bridge.send({
|
|
1129
|
+
type: "evaluate",
|
|
1130
|
+
code: `(async () => {
|
|
1131
|
+
const t = window.__sootsimTest
|
|
1132
|
+
if (!t) return null
|
|
1133
|
+
const n = (await t.findByTestId(${arg})) || (await t.findById(${arg}))
|
|
1134
|
+
if (!n || !n.absolutePosition || !n.layout) return { cx: null }
|
|
1135
|
+
const resolved =
|
|
1136
|
+
typeof n.nodeId === 'number' && typeof t.resolveTapTarget === 'function'
|
|
1137
|
+
? await t.resolveTapTarget(n.nodeId)
|
|
1138
|
+
: null
|
|
1139
|
+
const target = (resolved && resolved.target) || n
|
|
1140
|
+
const cx =
|
|
1141
|
+
resolved && typeof resolved.cx === 'number'
|
|
1142
|
+
? resolved.cx
|
|
1143
|
+
: n.absolutePosition.x + (n.layout.width || 0) / 2
|
|
1144
|
+
const cy =
|
|
1145
|
+
resolved && typeof resolved.cy === 'number'
|
|
1146
|
+
? resolved.cy
|
|
1147
|
+
: n.absolutePosition.y + (n.layout.height || 0) / 2
|
|
1148
|
+
return {
|
|
1149
|
+
cx,
|
|
1150
|
+
cy,
|
|
1151
|
+
match: {
|
|
1152
|
+
nodeId: n.nodeId ?? null,
|
|
1153
|
+
id: n.id,
|
|
1154
|
+
testID: n.testID,
|
|
1155
|
+
text: n.text ?? n.accessibilityLabel ?? null,
|
|
1156
|
+
type: n.type,
|
|
1157
|
+
},
|
|
1158
|
+
target: {
|
|
1159
|
+
nodeId: target.nodeId ?? null,
|
|
1160
|
+
id: target.id,
|
|
1161
|
+
testID: target.testID,
|
|
1162
|
+
text:
|
|
1163
|
+
target.text ??
|
|
1164
|
+
target.accessibilityLabel ??
|
|
1165
|
+
n.text ??
|
|
1166
|
+
n.accessibilityLabel ??
|
|
1167
|
+
null,
|
|
1168
|
+
type: target.type,
|
|
1169
|
+
},
|
|
1170
|
+
strategy: (resolved && resolved.strategy) || 'matched-node',
|
|
1171
|
+
}
|
|
1172
|
+
})()`
|
|
1173
|
+
})
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
1176
|
+
async function tapByText(bridge, query, options = {}, opts = {}) {
|
|
1177
|
+
const filterArg = JSON.stringify({
|
|
1178
|
+
query,
|
|
1179
|
+
exact: !!options.exact,
|
|
1180
|
+
role: options.role ?? null,
|
|
1181
|
+
within: options.within ?? null,
|
|
1182
|
+
minX: options.minX ?? null,
|
|
1183
|
+
maxX: options.maxX ?? null,
|
|
1184
|
+
minY: options.minY ?? null,
|
|
1185
|
+
maxY: options.maxY ?? null,
|
|
1186
|
+
near: options.near ?? null,
|
|
1187
|
+
nth: options.nth ?? null,
|
|
1188
|
+
first: !!options.first
|
|
1189
|
+
});
|
|
1190
|
+
return tapResolvedTarget(bridge, {
|
|
1191
|
+
agent: opts.agent,
|
|
1192
|
+
timing: opts.timing,
|
|
1193
|
+
textFallback: query,
|
|
1194
|
+
resolve: () => bridge.send({
|
|
1195
|
+
type: "evaluate",
|
|
1196
|
+
code: `(async () => {
|
|
1197
|
+
const t = window.__sootsimTest
|
|
1198
|
+
if (!t) return { error: 'bridge-not-ready' }
|
|
1199
|
+
const F = ${filterArg}
|
|
1200
|
+
|
|
1201
|
+
const res = await t.queryTextCandidates({
|
|
1202
|
+
query: F.query,
|
|
1203
|
+
exact: !!F.exact,
|
|
1204
|
+
...(F.role ? { role: F.role } : {}),
|
|
1205
|
+
})
|
|
1206
|
+
|
|
1207
|
+
let candidates = res.candidates || []
|
|
1208
|
+
|
|
1209
|
+
candidates = candidates.filter((c) => {
|
|
1210
|
+
const ax = c.info.absolutePosition && c.info.absolutePosition.x
|
|
1211
|
+
const ay = c.info.absolutePosition && c.info.absolutePosition.y
|
|
1212
|
+
if (F.minX !== null && !(ax >= F.minX)) return false
|
|
1213
|
+
if (F.maxX !== null && !(ax <= F.maxX)) return false
|
|
1214
|
+
if (F.minY !== null && !(ay >= F.minY)) return false
|
|
1215
|
+
if (F.maxY !== null && !(ay <= F.maxY)) return false
|
|
1216
|
+
if (F.within && !c.ancestorTestIDs.includes(F.within)) return false
|
|
1217
|
+
return true
|
|
1218
|
+
})
|
|
1219
|
+
|
|
1220
|
+
if (F.near) {
|
|
1221
|
+
candidates.sort((a, b) => {
|
|
1222
|
+
const ax = (a.info.absolutePosition && a.info.absolutePosition.x) || 0
|
|
1223
|
+
const ay = (a.info.absolutePosition && a.info.absolutePosition.y) || 0
|
|
1224
|
+
const bx = (b.info.absolutePosition && b.info.absolutePosition.x) || 0
|
|
1225
|
+
const by = (b.info.absolutePosition && b.info.absolutePosition.y) || 0
|
|
1226
|
+
return (
|
|
1227
|
+
Math.hypot(ax - F.near.x, ay - F.near.y) -
|
|
1228
|
+
Math.hypot(bx - F.near.x, by - F.near.y)
|
|
1229
|
+
)
|
|
1230
|
+
})
|
|
1231
|
+
} else {
|
|
1232
|
+
candidates.sort((a, b) => {
|
|
1233
|
+
const ay = (a.info.absolutePosition && a.info.absolutePosition.y) || 0
|
|
1234
|
+
const by = (b.info.absolutePosition && b.info.absolutePosition.y) || 0
|
|
1235
|
+
if (Math.abs(ay - by) > 2) return ay - by
|
|
1236
|
+
const ax = (a.info.absolutePosition && a.info.absolutePosition.x) || 0
|
|
1237
|
+
const bx = (b.info.absolutePosition && b.info.absolutePosition.x) || 0
|
|
1238
|
+
return ax - bx
|
|
1239
|
+
})
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
const total = candidates.length
|
|
1243
|
+
if (total === 0) return { matched: 0, total: 0 }
|
|
1244
|
+
|
|
1245
|
+
let idx = 0
|
|
1246
|
+
if (F.nth !== null) {
|
|
1247
|
+
idx = F.nth < 0 ? total + F.nth : F.nth
|
|
1248
|
+
if (idx < 0 || idx >= total) {
|
|
1249
|
+
return { matched: 0, total, nthOutOfRange: true, nth: F.nth }
|
|
1250
|
+
}
|
|
1251
|
+
} else if (total > 1 && !F.first && !F.near) {
|
|
1252
|
+
return {
|
|
1253
|
+
ambiguous: true,
|
|
1254
|
+
total,
|
|
1255
|
+
candidates: candidates.slice(0, 10).map((c, i) => ({
|
|
1256
|
+
idx: i,
|
|
1257
|
+
nodeId: c.info.nodeId,
|
|
1258
|
+
type: c.info.type,
|
|
1259
|
+
testID: c.info.testID,
|
|
1260
|
+
text: (c.info.text || '').slice(0, 60),
|
|
1261
|
+
abs: c.info.absolutePosition,
|
|
1262
|
+
layout: c.info.layout
|
|
1263
|
+
? {
|
|
1264
|
+
width: Math.round(c.info.layout.width || 0),
|
|
1265
|
+
height: Math.round(c.info.layout.height || 0),
|
|
1266
|
+
}
|
|
1267
|
+
: null,
|
|
1268
|
+
ancestorTestIDs: (c.ancestorTestIDs || []).slice(0, 5),
|
|
1269
|
+
})),
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
const picked = candidates[idx]
|
|
1274
|
+
const n = picked.info
|
|
1275
|
+
if (!n.absolutePosition || !n.layout) return { matched: 0, total }
|
|
1276
|
+
|
|
1277
|
+
const resolved =
|
|
1278
|
+
typeof n.nodeId === 'number' &&
|
|
1279
|
+
typeof t.resolveTapTarget === 'function'
|
|
1280
|
+
? await t.resolveTapTarget(n.nodeId)
|
|
1281
|
+
: null
|
|
1282
|
+
const target = (resolved && resolved.target) || n
|
|
1283
|
+
const cx =
|
|
1284
|
+
resolved && typeof resolved.cx === 'number'
|
|
1285
|
+
? resolved.cx
|
|
1286
|
+
: n.absolutePosition.x + (n.layout.width || 0) / 2
|
|
1287
|
+
const cy =
|
|
1288
|
+
resolved && typeof resolved.cy === 'number'
|
|
1289
|
+
? resolved.cy
|
|
1290
|
+
: n.absolutePosition.y + (n.layout.height || 0) / 2
|
|
1291
|
+
return {
|
|
1292
|
+
cx,
|
|
1293
|
+
cy,
|
|
1294
|
+
match: {
|
|
1295
|
+
nodeId: n.nodeId ?? null,
|
|
1296
|
+
id: n.id,
|
|
1297
|
+
testID: n.testID,
|
|
1298
|
+
type: n.type,
|
|
1299
|
+
},
|
|
1300
|
+
target: {
|
|
1301
|
+
nodeId: target.nodeId ?? null,
|
|
1302
|
+
id: target.id,
|
|
1303
|
+
testID: target.testID,
|
|
1304
|
+
text:
|
|
1305
|
+
target.text ??
|
|
1306
|
+
target.accessibilityLabel ??
|
|
1307
|
+
n.text ??
|
|
1308
|
+
n.accessibilityLabel ??
|
|
1309
|
+
null,
|
|
1310
|
+
type: target.type,
|
|
1311
|
+
},
|
|
1312
|
+
strategy: (resolved && resolved.strategy) || 'matched-node',
|
|
1313
|
+
total,
|
|
1314
|
+
idx,
|
|
1315
|
+
}
|
|
1316
|
+
})()`
|
|
1317
|
+
})
|
|
1318
|
+
});
|
|
1319
|
+
}
|
|
1320
|
+
async function tapBest(bridge, query, opts = {}) {
|
|
1321
|
+
const arg = JSON.stringify(query);
|
|
1322
|
+
return tapResolvedTarget(bridge, {
|
|
1323
|
+
agent: opts.agent,
|
|
1324
|
+
timing: opts.timing,
|
|
1325
|
+
textFallback: query,
|
|
1326
|
+
resolve: async () => {
|
|
1327
|
+
const payload = await bridge.send({
|
|
1328
|
+
type: "evaluate",
|
|
1329
|
+
code: `(async () => {
|
|
1330
|
+
const t = window.__sootsimTest
|
|
1331
|
+
if (!t) return { error: 'bridge-not-ready' }
|
|
1332
|
+
const byTestId =
|
|
1333
|
+
(await t.findByTestId(${arg})) || (await t.findById(${arg}))
|
|
1334
|
+
if (byTestId && byTestId.absolutePosition && byTestId.layout) {
|
|
1335
|
+
return {
|
|
1336
|
+
strategy: 'testid',
|
|
1337
|
+
node: {
|
|
1338
|
+
nodeId: byTestId.nodeId ?? null,
|
|
1339
|
+
id: byTestId.id,
|
|
1340
|
+
testID: byTestId.testID,
|
|
1341
|
+
type: byTestId.type,
|
|
1342
|
+
text: byTestId.text,
|
|
1343
|
+
absolutePosition: byTestId.absolutePosition,
|
|
1344
|
+
layout: byTestId.layout,
|
|
1345
|
+
},
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
const byText = await t.findByText(${arg})
|
|
1349
|
+
if (byText && byText.absolutePosition && byText.layout) {
|
|
1350
|
+
return {
|
|
1351
|
+
strategy: 'text',
|
|
1352
|
+
node: {
|
|
1353
|
+
nodeId: byText.nodeId ?? null,
|
|
1354
|
+
id: byText.id,
|
|
1355
|
+
testID: byText.testID,
|
|
1356
|
+
type: byText.type,
|
|
1357
|
+
text: byText.text,
|
|
1358
|
+
absolutePosition: byText.absolutePosition,
|
|
1359
|
+
layout: byText.layout,
|
|
1360
|
+
},
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
return { strategy: 'none' }
|
|
1364
|
+
})()`
|
|
1365
|
+
});
|
|
1366
|
+
if (!payload || !("node" in payload)) return payload;
|
|
1367
|
+
const node = payload.node;
|
|
1368
|
+
const cx = node.absolutePosition.x + node.layout.width / 2;
|
|
1369
|
+
const cy = node.absolutePosition.y + node.layout.height / 2;
|
|
1370
|
+
return {
|
|
1371
|
+
...payload,
|
|
1372
|
+
cx,
|
|
1373
|
+
cy,
|
|
1374
|
+
target: {
|
|
1375
|
+
id: node.id,
|
|
1376
|
+
testID: node.testID,
|
|
1377
|
+
text: payload.strategy === "text" ? query : node.text,
|
|
1378
|
+
type: node.type
|
|
1379
|
+
}
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1382
|
+
});
|
|
1383
|
+
}
|
|
1384
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1385
|
+
0 && (module.exports = {
|
|
1386
|
+
DEBUG_CHANNELS,
|
|
1387
|
+
READY_CONTENT_NODE_FLOOR,
|
|
1388
|
+
READY_NODE_STABLE_MS,
|
|
1389
|
+
WAIT_READY_PROBE,
|
|
1390
|
+
WAIT_READY_PROGRESS_INTERVAL_MS,
|
|
1391
|
+
clearConsole,
|
|
1392
|
+
clearLogs,
|
|
1393
|
+
clearRequests,
|
|
1394
|
+
filterLogEntries,
|
|
1395
|
+
formatTimelineSummary,
|
|
1396
|
+
getShellState,
|
|
1397
|
+
inspectAccessibilityTree,
|
|
1398
|
+
inspectDebugFind,
|
|
1399
|
+
inspectDebugFlags,
|
|
1400
|
+
inspectDebugRecent,
|
|
1401
|
+
inspectDebugStatus,
|
|
1402
|
+
inspectDescribe,
|
|
1403
|
+
inspectErrors,
|
|
1404
|
+
inspectFind,
|
|
1405
|
+
inspectKeyboard,
|
|
1406
|
+
inspectLogs,
|
|
1407
|
+
inspectMemory,
|
|
1408
|
+
inspectNodeCount,
|
|
1409
|
+
inspectRequests,
|
|
1410
|
+
inspectScreens,
|
|
1411
|
+
inspectScrollStateAt,
|
|
1412
|
+
inspectTimelineAdvanceCursor,
|
|
1413
|
+
inspectTimelineRecent,
|
|
1414
|
+
inspectTimelineSummary,
|
|
1415
|
+
inspectTree,
|
|
1416
|
+
inspectUrl,
|
|
1417
|
+
inspectWaitReady,
|
|
1418
|
+
inspectWaitSelector,
|
|
1419
|
+
inspectWarnings,
|
|
1420
|
+
isInspectModeActive,
|
|
1421
|
+
isInspectPickCommand,
|
|
1422
|
+
isShellCommandUnavailable,
|
|
1423
|
+
isTapSuccess,
|
|
1424
|
+
rankInteractive,
|
|
1425
|
+
readyProbeHasContent,
|
|
1426
|
+
resolveFindMode,
|
|
1427
|
+
resolveMaxMsFlag,
|
|
1428
|
+
scoreInteractive,
|
|
1429
|
+
setDebugChannels,
|
|
1430
|
+
shouldSkipAutoSettleForInspectPick,
|
|
1431
|
+
tapBest,
|
|
1432
|
+
tapById,
|
|
1433
|
+
tapByText,
|
|
1434
|
+
tapCommandForNode,
|
|
1435
|
+
tapCoordinates,
|
|
1436
|
+
tapResolvedTarget,
|
|
1437
|
+
tapTargetFromPayload,
|
|
1438
|
+
waitForSootsimIdle,
|
|
1439
|
+
waitReadyReason
|
|
1440
|
+
});
|