rnxsim 0.1.312 → 0.1.314
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 +11 -10
- package/README.md +5 -0
- package/cli/app-config.ts +65 -0
- package/cli/app-fonts.ts +408 -0
- package/cli/app-project.ts +231 -0
- package/cli/app-splash.ts +185 -0
- package/cli/app-state-reset.ts +24 -0
- package/cli/auth.ts +155 -0
- package/cli/bin.ts +594 -0
- package/cli/bridge-diagnostics.ts +226 -0
- package/cli/bridge-flow-runner.ts +2830 -0
- package/cli/browser-evals.ts +96 -0
- package/cli/commands/agent-wrapper.ts +986 -0
- package/cli/commands/agent.ts +423 -0
- package/cli/commands/app-fonts.ts +98 -0
- package/cli/commands/assert.ts +541 -0
- package/cli/commands/auth.ts +59 -0
- package/cli/commands/camera.ts +266 -0
- package/cli/commands/cleanup.ts +169 -0
- package/cli/commands/compat.ts +87 -0
- package/cli/commands/config.ts +32 -0
- package/cli/commands/control.ts +2142 -0
- package/cli/commands/cpu-profile.ts +269 -0
- package/cli/commands/daemon-mac-app.ts +169 -0
- package/cli/commands/daemon.ts +874 -0
- package/cli/commands/debug.ts +719 -0
- package/cli/commands/desktop.ts +39 -0
- package/cli/commands/detect.ts +197 -0
- package/cli/commands/detox.ts +385 -0
- package/cli/commands/device.ts +133 -0
- package/cli/commands/diagnose.ts +589 -0
- package/cli/commands/electron.ts +95 -0
- package/cli/commands/film.ts +379 -0
- package/cli/commands/flow.ts +1124 -0
- package/cli/commands/inspect/actions.ts +622 -0
- package/cli/commands/inspect/core.ts +2405 -0
- package/cli/commands/inspect/count.ts +17 -0
- package/cli/commands/inspect/describe.ts +192 -0
- package/cli/commands/inspect/env.ts +23 -0
- package/cli/commands/inspect/find.ts +171 -0
- package/cli/commands/inspect/get-layout.ts +39 -0
- package/cli/commands/inspect/keyboard.ts +52 -0
- package/cli/commands/inspect/list.ts +58 -0
- package/cli/commands/inspect/memory.ts +215 -0
- package/cli/commands/inspect/redaction.ts +39 -0
- package/cli/commands/inspect/resolve-target.ts +82 -0
- package/cli/commands/inspect/screens.ts +78 -0
- package/cli/commands/inspect/settle.ts +22 -0
- package/cli/commands/inspect/settling.ts +158 -0
- package/cli/commands/inspect/shared.ts +353 -0
- package/cli/commands/inspect/sleep.ts +14 -0
- package/cli/commands/inspect/tree.ts +32 -0
- package/cli/commands/inspect/url.ts +17 -0
- package/cli/commands/inspect/wait-event.ts +210 -0
- package/cli/commands/inspect/wait-idle.ts +24 -0
- package/cli/commands/inspect/wait-ready.ts +74 -0
- package/cli/commands/inspect/wait-selector.ts +54 -0
- package/cli/commands/inspect/wait.ts +31 -0
- package/cli/commands/inspect.ts +4519 -0
- package/cli/commands/install-desktop.ts +351 -0
- package/cli/commands/login.ts +331 -0
- package/cli/commands/logout.ts +31 -0
- package/cli/commands/maestro-generate.ts +361 -0
- package/cli/commands/maestro.ts +453 -0
- package/cli/commands/mode.ts +57 -0
- package/cli/commands/no-bridge-hint.ts +80 -0
- package/cli/commands/perf.ts +66 -0
- package/cli/commands/permissions.ts +203 -0
- package/cli/commands/profile.ts +108 -0
- package/cli/commands/react.ts +353 -0
- package/cli/commands/record.ts +1434 -0
- package/cli/commands/report-issue.ts +305 -0
- package/cli/commands/reset.ts +85 -0
- package/cli/commands/runtime.ts +351 -0
- package/cli/commands/screenshot-command.ts +106 -0
- package/cli/commands/screenshot-layers.ts +143 -0
- package/cli/commands/screenshot-mode.ts +37 -0
- package/cli/commands/screenshot.ts +488 -0
- package/cli/commands/screenshots-capture.ts +607 -0
- package/cli/commands/screenshots.ts +127 -0
- package/cli/commands/serve.ts +168 -0
- package/cli/commands/setup.ts +545 -0
- package/cli/commands/shell-boolean-mode.ts +81 -0
- package/cli/commands/skills.ts +467 -0
- package/cli/commands/slides.ts +361 -0
- package/cli/commands/state.ts +87 -0
- package/cli/commands/storage.ts +58 -0
- package/cli/commands/telemetry.ts +54 -0
- package/cli/commands/three-mode.ts +763 -0
- package/cli/commands/timeline.ts +122 -0
- package/cli/commands/upgrade.ts +208 -0
- package/cli/commands/upload.ts +1225 -0
- package/cli/commands/version.ts +54 -0
- package/cli/commands/what-happened.ts +327 -0
- package/cli/current-sim.ts +204 -0
- package/cli/desktop-companion.ts +300 -0
- package/cli/drivers/electron.ts +70 -0
- package/cli/drivers/index.ts +20 -0
- package/cli/drivers/playwright-provisioning.ts +180 -0
- package/cli/drivers/playwright.ts +698 -0
- package/cli/drivers/registry.ts +65 -0
- package/cli/drivers/types.ts +102 -0
- package/cli/flow-file.ts +142 -0
- package/cli/flow-live-status.ts +120 -0
- package/cli/flow-session.ts +187 -0
- package/cli/help.ts +80 -0
- package/cli/hidden-runtime-alias.ts +19 -0
- package/cli/hints.ts +216 -0
- package/cli/inspect-notice-state.ts +114 -0
- package/cli/maestro-js.ts +334 -0
- package/cli/open-url.ts +8 -0
- package/cli/parent-pid.ts +204 -0
- package/cli/parse-args.ts +211 -0
- package/cli/prompt.ts +51 -0
- package/cli/recording-access.ts +107 -0
- package/cli/registry.ts +1 -0
- package/cli/resolve-assets.ts +63 -0
- package/cli/run-registry.ts +226 -0
- package/cli/runtime-notes.ts +66 -0
- package/cli/runtime-summary.ts +25 -0
- package/cli/setup-repository.ts +187 -0
- package/cli/telemetry.ts +187 -0
- package/cli/ws-bridge.ts +798 -0
- package/dist-cli/bin.js +5 -5
- package/dist-cli/chunks/{agent-NXVDDCFG.js → agent-7YBDCYMA.js} +2 -2
- package/dist-cli/chunks/{agent-wrapper-DXJCFKL3.js → agent-wrapper-2GHFBHCR.js} +2 -2
- package/dist-cli/chunks/{app-fonts-W5AJZQJF.js → app-fonts-RSNVPQSU.js} +2 -2
- package/dist-cli/chunks/{assert-3LOJEUDT.js → assert-XCMX3XJX.js} +2 -2
- package/dist-cli/chunks/{auth-MFTYYPNX.js → auth-B442HRAX.js} +2 -2
- package/dist-cli/chunks/{beta-GT3NN67B.js → beta-XJ55JK3M.js} +2 -2
- package/dist-cli/chunks/camera-UGYSSLIK.js +33 -0
- package/dist-cli/chunks/{chunk-EUAVBTKU.js → chunk-277AEQZX.js} +2 -2
- package/dist-cli/chunks/{chunk-5OX7ANRA.js → chunk-2JNSK774.js} +2 -2
- package/dist-cli/chunks/{chunk-L24Z3CRS.js → chunk-32WOTSTR.js} +3 -3
- package/dist-cli/chunks/{chunk-BKNOLP3J.js → chunk-3S753SNQ.js} +2 -2
- package/dist-cli/chunks/{chunk-N6A3SXMR.js → chunk-46ZOLOYA.js} +2 -2
- package/dist-cli/chunks/{chunk-GZDHEONW.js → chunk-5L7ELDQL.js} +8 -9
- package/dist-cli/chunks/{chunk-YZRFF4YA.js → chunk-7OOPFSQS.js} +2 -2
- package/dist-cli/chunks/{chunk-TBMVBI26.js → chunk-7SV3RPRW.js} +2 -2
- package/dist-cli/chunks/{chunk-JG6S4RSH.js → chunk-7ZC35MOU.js} +1 -1
- package/dist-cli/chunks/chunk-AMG5E6CC.js +9 -0
- package/dist-cli/chunks/{chunk-MENV7VLD.js → chunk-APWNH3A4.js} +1 -1
- package/dist-cli/chunks/chunk-B57XUKY3.js +4 -0
- package/dist-cli/chunks/{chunk-3D57IBPC.js → chunk-C2NL26TD.js} +1 -1
- package/dist-cli/chunks/chunk-D2FNUWAB.js +15 -0
- package/dist-cli/chunks/{chunk-4BXCZHAU.js → chunk-E5T4XSJ3.js} +2 -2
- package/dist-cli/chunks/{chunk-FTC4M26Q.js → chunk-EAC34EQS.js} +1 -1
- package/dist-cli/chunks/{chunk-NPVGMH2N.js → chunk-EG32ML36.js} +2 -2
- package/dist-cli/chunks/{chunk-DM6KUAOZ.js → chunk-FXUAC6D5.js} +1 -1
- package/dist-cli/chunks/chunk-GQSL4USA.js +6 -0
- package/dist-cli/chunks/{chunk-U4PN6JOT.js → chunk-HAXW27SS.js} +2 -2
- package/dist-cli/chunks/{chunk-ZMZTHU7V.js → chunk-IZAHPAN6.js} +1 -1
- package/dist-cli/chunks/{chunk-SI3BUTQR.js → chunk-J62KM5TB.js} +2 -2
- package/dist-cli/chunks/{chunk-Y2FSZSO7.js → chunk-JEMCD5E4.js} +1 -1
- package/dist-cli/chunks/{chunk-T4FT6CM7.js → chunk-JZS3Q37N.js} +2 -2
- package/dist-cli/chunks/{chunk-2SA2VTGP.js → chunk-KR4JON7D.js} +1 -1
- package/dist-cli/chunks/chunk-KWCYKQIQ.js +4 -0
- package/dist-cli/chunks/{chunk-HG7ORJMT.js → chunk-MCWPL644.js} +2 -2
- package/dist-cli/chunks/chunk-MJYK3N2I.js +4 -0
- package/dist-cli/chunks/{chunk-YRV7HR3P.js → chunk-O6TRIZNS.js} +2 -2
- package/dist-cli/chunks/{chunk-WBT32R6N.js → chunk-P7XL2E73.js} +3 -3
- package/dist-cli/chunks/{chunk-MNGQ42QE.js → chunk-PFQTUKQ4.js} +62 -87
- package/dist-cli/chunks/{chunk-Q3RGLERJ.js → chunk-PG5RZCTN.js} +2 -2
- package/dist-cli/chunks/{chunk-OR3XTXEB.js → chunk-QGRI2Z4M.js} +2 -2
- package/dist-cli/chunks/chunk-QLXXE7GE.js +125 -0
- package/dist-cli/chunks/{chunk-KZW5WBGU.js → chunk-QOJJJWJE.js} +89 -133
- package/dist-cli/chunks/{chunk-BRIZOR4K.js → chunk-RWZY5427.js} +2 -2
- package/dist-cli/chunks/{chunk-TYE3WBSR.js → chunk-RZKU2K3J.js} +2 -2
- package/dist-cli/chunks/{chunk-7VWOSTD5.js → chunk-SBV4IK4H.js} +1 -1
- package/dist-cli/chunks/{chunk-FWYYCUAZ.js → chunk-SGMVFFMK.js} +1 -1
- package/dist-cli/chunks/{chunk-LDQ7HVCW.js → chunk-TAX4UT2N.js} +1 -1
- package/dist-cli/chunks/{chunk-G74YIEVS.js → chunk-TUOFAWXT.js} +1 -1
- package/dist-cli/chunks/{chunk-274UD5FD.js → chunk-UHZLOHGP.js} +1 -1
- package/dist-cli/chunks/{chunk-37I5AIS7.js → chunk-VC7V76U3.js} +1 -1
- package/dist-cli/chunks/{chunk-B7KMNHX7.js → chunk-VQVMLW4U.js} +1 -1
- package/dist-cli/chunks/{chunk-VZBLFMYC.js → chunk-VUKKYPZN.js} +2 -2
- package/dist-cli/chunks/{chunk-VUFRB347.js → chunk-WGGRJDRE.js} +1 -1
- package/dist-cli/chunks/chunk-X6H76EKP.js +15 -0
- package/dist-cli/chunks/chunk-XLZ5FNRT.js +27 -0
- package/dist-cli/chunks/{chunk-TCWQJILS.js → chunk-XULEACM4.js} +2 -2
- package/dist-cli/chunks/{chunk-XRGEEPJ5.js → chunk-YFSDM7AX.js} +4 -4
- package/dist-cli/chunks/chunk-YWI3UEVX.js +5 -0
- package/dist-cli/chunks/{chunk-TGQNQ6XT.js → chunk-ZBSJO4NB.js} +10 -9
- package/dist-cli/chunks/{cleanup-H3GWQB63.js → cleanup-EYLGCA6Z.js} +2 -2
- package/dist-cli/chunks/cli-version-LL2UGIHE.js +4 -0
- package/dist-cli/chunks/{compat-5WG2XJV7.js → compat-TLJYHB4E.js} +2 -2
- package/dist-cli/chunks/{config-ANMBVVLL.js → config-4JWUOEXK.js} +2 -2
- package/dist-cli/chunks/{control-NSGNQFJG.js → control-HPAOYF4N.js} +2 -2
- package/dist-cli/chunks/daemon-OYLASXLE.js +4 -0
- package/dist-cli/chunks/{debug-CHV2O2KF.js → debug-4BKXF6KI.js} +5 -5
- package/dist-cli/chunks/{desktop-FIPIK4CH.js → desktop-ZQ6ZD2S6.js} +3 -3
- package/dist-cli/chunks/{detox-CVUJP7K6.js → detox-WNPASSS3.js} +2 -2
- package/dist-cli/chunks/{device-6WCUM4W6.js → device-BXLXVG3V.js} +2 -2
- package/dist-cli/chunks/{diagnose-I6EMWEII.js → diagnose-3QX7W5RW.js} +2 -2
- package/dist-cli/chunks/{disk-cleanup-P27CYFTN.js → disk-cleanup-KSWKI7WB.js} +2 -2
- package/dist-cli/chunks/drivers-EPIEFF7P.js +4 -0
- package/dist-cli/chunks/{film-UOQFFYNF.js → film-GDMR33VO.js} +3 -3
- package/dist-cli/chunks/flow-NKMPBYCJ.js +4 -0
- package/dist-cli/chunks/help-OGCBHOKA.js +4 -0
- package/dist-cli/chunks/{hidden-runtime-alias-NJYFSJKZ.js → hidden-runtime-alias-S2GTDX3T.js} +2 -2
- package/dist-cli/chunks/home-paths-QRCDLTTV.js +4 -0
- package/dist-cli/chunks/inspect-FUYMOZPY.js +4 -0
- package/dist-cli/chunks/install-desktop-FR6YKW7Y.js +4 -0
- package/dist-cli/chunks/{login-BPFQENCX.js → login-IJAPUZHI.js} +4 -4
- package/dist-cli/chunks/{logout-5A45ZGY7.js → logout-QMXDFU2X.js} +2 -2
- package/dist-cli/chunks/{maestro-E4CSDMLM.js → maestro-ZXU3YCVX.js} +3 -3
- package/dist-cli/chunks/{maestro-generate-CKMOJQX2.js → maestro-generate-PYB5QY7K.js} +3 -3
- package/dist-cli/chunks/{mode-SZYNMIQF.js → mode-WGUCL5FZ.js} +2 -2
- package/dist-cli/chunks/{optional-demo-registry-FRH4SFVG.js → optional-demo-registry-WH2O6H36.js} +2 -2
- package/dist-cli/chunks/{perf-VXU2XJAX.js → perf-TOD3UFAH.js} +2 -2
- package/dist-cli/chunks/{permissions-IRIAOVND.js → permissions-I5BRJGTB.js} +2 -2
- package/dist-cli/chunks/{record-RY7FPLLH.js → record-ZYL2FYSK.js} +3 -3
- package/dist-cli/chunks/{report-issue-KNWXOE53.js → report-issue-TAI6DYZO.js} +2 -2
- package/dist-cli/chunks/reset-7YGYKQPQ.js +4 -0
- package/dist-cli/chunks/runtime-B4JO6QRI.js +4 -0
- package/dist-cli/chunks/{screenshot-command-QLALR6GY.js → screenshot-command-7ANLODZY.js} +7 -7
- package/dist-cli/chunks/{screenshot-layers-OQFMFVRL.js → screenshot-layers-A7FYXSVU.js} +3 -3
- package/dist-cli/chunks/{screenshots-capture-6TSRJFGB.js → screenshots-capture-WT2ZY6CB.js} +2 -2
- package/dist-cli/chunks/serve-TG5WKA4V.js +44 -0
- package/dist-cli/chunks/{setup-7V6UKX4U.js → setup-PCWLD22V.js} +2 -2
- package/dist-cli/chunks/{skills-QQBOSETH.js → skills-27ZHWCQS.js} +2 -2
- package/dist-cli/chunks/state-ZQVY46ZO.js +14 -0
- package/dist-cli/chunks/{storage-TAPMKK5O.js → storage-OYC57C4X.js} +6 -6
- package/dist-cli/chunks/store-32HSPZHI.js +4 -0
- package/dist-cli/chunks/telemetry-PBJR7XHR.js +4 -0
- package/dist-cli/chunks/{timeline-IAM564NA.js → timeline-RAJJFQT6.js} +2 -2
- package/dist-cli/chunks/{upgrade-CD55QXIX.js → upgrade-OEFNZRIP.js} +2 -2
- package/dist-cli/chunks/upload-LQJITLKK.js +4 -0
- package/dist-cli/chunks/version-NIXTY6PL.js +6 -0
- package/dist-cli/chunks/{web-6NW6YLPI.js → web-XBUTBVGR.js} +2 -2
- package/dist-cli/chunks/{what-happened-CZZDOOVG.js → what-happened-YNHRFUDX.js} +3 -3
- package/dist-lib/agent-daemon-client.cjs +1 -1
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/agent-identity.cjs +1 -1
- package/dist-lib/agent-sessions.cjs +1 -1
- package/dist-lib/attached-projects.cjs +1 -1
- package/dist-lib/auth/shared-session.cjs +1 -1
- package/dist-lib/backend-origin.cjs +1 -1
- package/dist-lib/beta.cjs +1 -1
- package/dist-lib/beta.mjs +1 -1
- package/dist-lib/bridge-constants.cjs +1 -1
- package/dist-lib/bridge-contract.cjs +20 -0
- 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 +67 -28
- package/dist-lib/host/bridge-host.cjs +140 -12
- 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 +2815 -40
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/menu.cjs +1 -1
- package/dist-lib/menu.mjs +1 -1
- package/dist-lib/metro.cjs +1 -1
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/public-brand.cjs +1 -1
- package/dist-lib/render-mode.cjs +1 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
- package/dist-lib/sdk.cjs +2549 -2061
- package/dist-lib/sdk.mjs +2543 -2061
- package/dist-lib/skills.cjs +493 -282
- package/dist-lib/vite.cjs +1 -1
- package/package.json +8 -2
- package/src/bridge-constants.ts +3 -4
- package/src/bridge-contract.ts +251 -0
- package/src/connect.ts +83 -0
- package/src/disk-cleanup.ts +30 -0
- package/src/home-paths.ts +81 -38
- package/src/host/bridge-host.ts +134 -6
- package/src/index.ts +27 -1
- package/src/sdk.ts +8 -0
- package/src/sim-client.ts +660 -0
- package/dist-cli/chunks/camera-6EAK7CUL.js +0 -22
- package/dist-cli/chunks/chunk-33FKQH54.js +0 -5
- package/dist-cli/chunks/chunk-66NF2KNG.js +0 -9
- package/dist-cli/chunks/chunk-6UKJB2VC.js +0 -125
- package/dist-cli/chunks/chunk-7INBNA3U.js +0 -26
- package/dist-cli/chunks/chunk-AY47WKS5.js +0 -23
- package/dist-cli/chunks/chunk-HMDOLCKG.js +0 -15
- package/dist-cli/chunks/chunk-KR5BGMBZ.js +0 -4
- package/dist-cli/chunks/chunk-NLEMQ4PY.js +0 -6
- package/dist-cli/chunks/chunk-PEPUGJVI.js +0 -4
- package/dist-cli/chunks/cli-version-3QPAJ3LC.js +0 -4
- package/dist-cli/chunks/daemon-JHF4BJ3U.js +0 -4
- package/dist-cli/chunks/drivers-HQ6AKZT6.js +0 -4
- package/dist-cli/chunks/flow-N27MRRNL.js +0 -4
- package/dist-cli/chunks/help-SF6FEGEE.js +0 -4
- package/dist-cli/chunks/home-paths-XOV44JEF.js +0 -4
- package/dist-cli/chunks/inspect-CWFRFQTY.js +0 -4
- package/dist-cli/chunks/install-desktop-KLQDNXZQ.js +0 -4
- package/dist-cli/chunks/runtime-AR3GJTGK.js +0 -4
- package/dist-cli/chunks/serve-F54J2NQO.js +0 -44
- package/dist-cli/chunks/store-7NX3S43H.js +0 -4
- package/dist-cli/chunks/telemetry-E7BUK6RA.js +0 -4
- package/dist-cli/chunks/upload-GZ6LIMYM.js +0 -4
- package/dist-cli/chunks/version-3DYJ5AS3.js +0 -6
package/dist-lib/sdk.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.314 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
|
@@ -29,8 +29,17 @@ __export(sdk_exports, {
|
|
|
29
29
|
READY_CONTENT_NODE_FLOOR: () => READY_CONTENT_NODE_FLOOR,
|
|
30
30
|
READY_NODE_STABLE_MS: () => READY_NODE_STABLE_MS,
|
|
31
31
|
READY_PROBE_POLL_MS: () => READY_PROBE_POLL_MS,
|
|
32
|
+
SimClient: () => SimClient,
|
|
33
|
+
SimClientError: () => SimClientError,
|
|
34
|
+
SimConnectError: () => SimConnectError,
|
|
35
|
+
SimKeyboardError: () => SimKeyboardError,
|
|
36
|
+
SimPerformError: () => SimPerformError,
|
|
37
|
+
SimReadyTimeoutError: () => SimReadyTimeoutError,
|
|
38
|
+
SimResetError: () => SimResetError,
|
|
39
|
+
SimTapError: () => SimTapError,
|
|
32
40
|
WAIT_READY_PROBE: () => WAIT_READY_PROBE,
|
|
33
41
|
WAIT_READY_PROGRESS_INTERVAL_MS: () => WAIT_READY_PROGRESS_INTERVAL_MS,
|
|
42
|
+
buildSimOpenUrl: () => buildSimOpenUrl,
|
|
34
43
|
clearConsole: () => clearConsole,
|
|
35
44
|
clearLogs: () => clearLogs,
|
|
36
45
|
clearRequests: () => clearRequests,
|
|
@@ -92,2175 +101,2645 @@ __export(sdk_exports, {
|
|
|
92
101
|
});
|
|
93
102
|
module.exports = __toCommonJS(sdk_exports);
|
|
94
103
|
|
|
95
|
-
// cli/commands/inspect/
|
|
96
|
-
function
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
function isInspectPickCommand(subcommand) {
|
|
116
|
-
return typeof subcommand === "string" && INSPECT_PICK_COMMANDS.has(subcommand);
|
|
117
|
-
}
|
|
118
|
-
async function isInspectModeActive(bridge) {
|
|
119
|
-
const active = await bridge.send({
|
|
120
|
-
type: "evaluate",
|
|
121
|
-
code: "window.__sootsimEngineState?.inspectActive === true"
|
|
122
|
-
});
|
|
123
|
-
return active === true;
|
|
124
|
-
}
|
|
125
|
-
async function shouldSkipAutoSettleForInspectPick(bridge, subcommand) {
|
|
126
|
-
if (!isInspectPickCommand(subcommand)) return false;
|
|
127
|
-
try {
|
|
128
|
-
return await isInspectModeActive(bridge);
|
|
129
|
-
} catch {
|
|
130
|
-
return false;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
async function inspectNodeCount(bridge) {
|
|
134
|
-
const count = await bridge.send({
|
|
135
|
-
type: "evaluate",
|
|
136
|
-
code: "(async () => await window.__sootsimTest.getNodeCount())()"
|
|
137
|
-
});
|
|
138
|
-
return { nodes: typeof count === "number" ? count : 0 };
|
|
139
|
-
}
|
|
140
|
-
async function inspectTree(bridge, depth = 5, opts) {
|
|
141
|
-
const optsArg = opts?.format ? `, ${JSON.stringify(opts)}` : "";
|
|
142
|
-
const tree = await bridge.send({
|
|
143
|
-
type: "evaluate",
|
|
144
|
-
code: `(async () => await window.__sootsimTest.dumpTree(${depth}${optsArg}))()`
|
|
145
|
-
});
|
|
146
|
-
if (opts?.format === "boxes" && typeof tree === "string") {
|
|
147
|
-
try {
|
|
148
|
-
return { depth, tree: JSON.parse(tree) };
|
|
149
|
-
} catch {
|
|
150
|
-
return { depth, tree };
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
return { depth, tree };
|
|
154
|
-
}
|
|
155
|
-
async function inspectUrl(bridge) {
|
|
156
|
-
const url = await bridge.send({ type: "evaluate", code: "window.location.href" });
|
|
157
|
-
return { url: typeof url === "string" ? url : "" };
|
|
158
|
-
}
|
|
159
|
-
async function inspectDescribe(bridge, dumpOpts) {
|
|
160
|
-
const code = `(async () => {
|
|
161
|
-
const t = window.__sootsimTest
|
|
162
|
-
const mainShell = window.SootSim?.bridges?.mainShell
|
|
163
|
-
const kb = window.__sootsimKeyboard
|
|
164
|
-
if (!t) return { error: 'no test bridge' }
|
|
104
|
+
// cli/commands/inspect/settling.ts
|
|
105
|
+
async function waitForSootsimIdle({
|
|
106
|
+
bridge,
|
|
107
|
+
simId,
|
|
108
|
+
maxMs,
|
|
109
|
+
pollMs = 50,
|
|
110
|
+
stablePolls = 3,
|
|
111
|
+
strict = false
|
|
112
|
+
}) {
|
|
113
|
+
const result = await bridge.send(
|
|
114
|
+
{
|
|
115
|
+
type: "evaluate",
|
|
116
|
+
simId,
|
|
117
|
+
code: `(async () => {
|
|
118
|
+
const start = Date.now()
|
|
119
|
+
const deadline = start + ${Math.max(0, Math.round(maxMs))}
|
|
120
|
+
const pollMs = ${Math.max(1, Math.round(pollMs))}
|
|
121
|
+
const requiredStablePolls = ${Math.max(1, Math.round(stablePolls))}
|
|
122
|
+
const strict = ${strict ? "true" : "false"}
|
|
123
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
165
124
|
|
|
166
|
-
|
|
125
|
+
// route-aware settle: a tap that pushes/pops a screen is async \u2014 the
|
|
126
|
+
// old screen still renders for a moment, then the new one mounts and
|
|
127
|
+
// its content loads. a pure layout-hash check reports "idle" on the
|
|
128
|
+
// outgoing screen or on the incoming skeleton, so a driver re-taps
|
|
129
|
+
// and stacks duplicate navigations. drain any in-flight screen
|
|
130
|
+
// transition FIRST, bounded by the overall budget. when no
|
|
131
|
+
// transition is happening this returns in ~80ms (startWindowMs), so
|
|
132
|
+
// a plain button tap barely pays for it.
|
|
167
133
|
try {
|
|
168
|
-
|
|
134
|
+
const wfst = window.__sootsimTest?.waitForScreenTransitions
|
|
135
|
+
if (typeof wfst === 'function') {
|
|
136
|
+
const remaining = deadline - Date.now()
|
|
137
|
+
if (remaining > 120) {
|
|
138
|
+
await wfst({
|
|
139
|
+
timeoutMs: Math.min(remaining - 80, 4000),
|
|
140
|
+
settleMs: 64,
|
|
141
|
+
startWindowMs: 80,
|
|
142
|
+
})
|
|
143
|
+
}
|
|
144
|
+
}
|
|
169
145
|
} catch {}
|
|
170
146
|
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
const filter = typeof opts.filter === 'string' ? opts.filter.toLowerCase() : ''
|
|
192
|
-
const testIdRegex = opts.testIdLike ? globToRegex(opts.testIdLike) : null
|
|
193
|
-
const onlyRegex = opts.onlyGlob ? globToRegex(opts.onlyGlob) : null
|
|
194
|
-
const lines = []
|
|
195
|
-
let matched = 0
|
|
196
|
-
for (const node of list) {
|
|
197
|
-
const testID = node?.testID || node?.testId || ''
|
|
198
|
-
const role = node?.role || ''
|
|
199
|
-
const label = node?.accessibilityLabel || node?.label || ''
|
|
200
|
-
const text = node?.text || ''
|
|
201
|
-
if (testIdRegex && !testIdRegex.test(testID)) continue
|
|
202
|
-
const joined = [role, label, text, testID].filter(Boolean).join(' ')
|
|
203
|
-
if (onlyRegex && !onlyRegex.test(joined)) continue
|
|
204
|
-
if (filter && !joined.toLowerCase().includes(filter)) continue
|
|
205
|
-
matched++
|
|
206
|
-
|
|
207
|
-
const type = String(node?.type || 'node').toLowerCase()
|
|
208
|
-
const handlers = Array.isArray(node?.handlers) ? node.handlers : []
|
|
209
|
-
const parts = []
|
|
210
|
-
if (role) parts.push('[' + role + ']')
|
|
211
|
-
else if (handlers.length > 0) parts.push('[tap]')
|
|
212
|
-
else parts.push('<' + type + '>')
|
|
213
|
-
const visibleText = clipForDisplay(text, 50)
|
|
214
|
-
if (visibleText) parts.push(JSON.stringify(visibleText))
|
|
215
|
-
if (testID) parts.push('#' + testID)
|
|
216
|
-
if (label && label !== text) {
|
|
217
|
-
parts.push('label=' + JSON.stringify(clipForDisplay(label, 50)))
|
|
147
|
+
const readSnapshot = async () => {
|
|
148
|
+
let animating = false
|
|
149
|
+
let pendingFetches = 0
|
|
150
|
+
let requestTotal = 0
|
|
151
|
+
let requestInFlight = 0
|
|
152
|
+
try {
|
|
153
|
+
const stats = await window.__sootsimRenderHost?.queryStats?.()
|
|
154
|
+
if (stats) {
|
|
155
|
+
animating =
|
|
156
|
+
stats.hasActiveAnims === true ||
|
|
157
|
+
stats.hasActiveNativeAnimations === true ||
|
|
158
|
+
stats.hasPendingAnimationFrames === true
|
|
159
|
+
// a freshly-pushed screen showing a skeleton is layout-stable
|
|
160
|
+
// but not actually settled \u2014 its data/images are still in
|
|
161
|
+
// flight. treat bounded image-loader fetches as not-idle so
|
|
162
|
+
// settle waits for real content, capped by the budget.
|
|
163
|
+
const pf = stats.memory && stats.memory.imageLoader
|
|
164
|
+
? stats.memory.imageLoader.pendingFetches
|
|
165
|
+
: 0
|
|
166
|
+
pendingFetches = typeof pf === 'number' ? pf : 0
|
|
218
167
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
168
|
+
} catch {}
|
|
169
|
+
try {
|
|
170
|
+
const counts = await window.__sootsimTest?.getRequestCounts?.()
|
|
171
|
+
requestTotal = typeof counts?.total === 'number' ? counts.total : 0
|
|
172
|
+
requestInFlight = typeof counts?.inFlight === 'number' ? counts.inFlight : 0
|
|
173
|
+
} catch {}
|
|
174
|
+
const root = window.__sootsimRoot
|
|
175
|
+
const nodes = []
|
|
176
|
+
if (root) {
|
|
177
|
+
const walk = (n) => {
|
|
178
|
+
if (n.layout && n.layout.width > 0) {
|
|
179
|
+
nodes.push(Math.round(n.layout.x) + ',' + Math.round(n.layout.y) + ',' + Math.round(n.layout.width))
|
|
180
|
+
}
|
|
181
|
+
for (const c of n.children || []) walk(c)
|
|
227
182
|
}
|
|
228
|
-
|
|
229
|
-
lines.push(' ' + parts.join(' '))
|
|
183
|
+
walk(root)
|
|
230
184
|
}
|
|
231
|
-
return {
|
|
185
|
+
return { layout: nodes.join(';'), animating, pendingFetches, requestTotal, requestInFlight }
|
|
232
186
|
}
|
|
233
187
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
188
|
+
// how long generic background network is allowed to keep us waiting AFTER
|
|
189
|
+
// the screen is otherwise visually settled. apps with continuous traffic
|
|
190
|
+
// (polling, realtime sockets, a tap that fires 20-50 fetches) never reach
|
|
191
|
+
// requestInFlight===0 / a stable requestTotal, so requiring full network
|
|
192
|
+
// quiet burns the ENTIRE budget on every command \u2014 the leading idle that
|
|
193
|
+
// failed PR-preview recordings for network-heavy apps (3pc fight-pulse).
|
|
194
|
+
// layout-stability + the image-loader already prove visible content
|
|
195
|
+
// loaded (a data load changes layout; an image load shows in the loader),
|
|
196
|
+
// so the generic request counters are a courtesy, not a gate: honor them
|
|
197
|
+
// only within this grace once the screen is visually still.
|
|
198
|
+
const networkQuietGraceMs = 1200
|
|
199
|
+
let lastLayout = ''
|
|
200
|
+
let lastRequestTotal = -1
|
|
201
|
+
let stable = 0
|
|
202
|
+
let visuallyStillSince = 0
|
|
203
|
+
while (Date.now() < deadline) {
|
|
204
|
+
const snapshot = await readSnapshot()
|
|
205
|
+
const strictOk = !strict || !snapshot.animating
|
|
206
|
+
// visually settled: no animations, no image content loading, layout
|
|
207
|
+
// unchanged since the last poll.
|
|
208
|
+
const visuallyStill =
|
|
209
|
+
strictOk && snapshot.pendingFetches === 0 && snapshot.layout === lastLayout
|
|
210
|
+
// generic network quiet: nothing in flight and no new requests issued.
|
|
211
|
+
const networkQuiet =
|
|
212
|
+
snapshot.requestInFlight === 0 && snapshot.requestTotal === lastRequestTotal
|
|
213
|
+
if (visuallyStill) {
|
|
214
|
+
stable++
|
|
215
|
+
if (visuallyStillSince === 0) visuallyStillSince = Date.now()
|
|
216
|
+
// settle once the screen has held still long enough AND either the
|
|
217
|
+
// network is genuinely quiet OR it has refused to quiet within the
|
|
218
|
+
// grace (continuous background traffic must not block forever).
|
|
219
|
+
if (
|
|
220
|
+
stable >= requiredStablePolls &&
|
|
221
|
+
(networkQuiet || Date.now() - visuallyStillSince >= networkQuietGraceMs)
|
|
222
|
+
) {
|
|
223
|
+
return { settled: true, elapsed: Date.now() - start }
|
|
224
|
+
}
|
|
225
|
+
} else {
|
|
226
|
+
stable = 0
|
|
227
|
+
visuallyStillSince = 0
|
|
228
|
+
}
|
|
229
|
+
lastLayout = snapshot.layout
|
|
230
|
+
lastRequestTotal = snapshot.requestTotal
|
|
231
|
+
await sleep(pollMs)
|
|
240
232
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
return result ?? {};
|
|
247
|
-
}
|
|
248
|
-
function scoreLayoutTextContrast(input) {
|
|
249
|
-
const required = input.fontSize >= 18 || input.fontSize >= 14 && (input.fontWeight ?? 400) >= 700 ? 3 : 4.5;
|
|
250
|
-
const { normal, background } = input;
|
|
251
|
-
if (normal.width <= 0 || normal.height <= 0 || normal.width !== background.width || normal.height !== background.height) {
|
|
252
|
-
return { ratio: null, required, reason: "capture dimensions do not match" };
|
|
253
|
-
}
|
|
254
|
-
const left = Math.max(0, Math.floor(input.box.x));
|
|
255
|
-
const top = Math.max(0, Math.floor(input.box.y));
|
|
256
|
-
const right = Math.min(normal.width, Math.ceil(input.box.x + input.box.w));
|
|
257
|
-
const bottom = Math.min(normal.height, Math.ceil(input.box.y + input.box.h));
|
|
258
|
-
if (right <= left || bottom <= top) {
|
|
259
|
-
return { ratio: null, required, reason: "text is outside the captured frame" };
|
|
260
|
-
}
|
|
261
|
-
const linear = (value) => {
|
|
262
|
-
const channel = value / 255;
|
|
263
|
-
return channel <= 0.04045 ? channel / 12.92 : Math.pow((channel + 0.055) / 1.055, 2.4);
|
|
264
|
-
};
|
|
265
|
-
const ratioOf = (foreground, behind) => {
|
|
266
|
-
const foregroundLuminance = 0.2126 * linear(foreground[0]) + 0.7152 * linear(foreground[1]) + 0.0722 * linear(foreground[2]);
|
|
267
|
-
const backgroundLuminance = 0.2126 * linear(behind[0]) + 0.7152 * linear(behind[1]) + 0.0722 * linear(behind[2]);
|
|
268
|
-
return (Math.max(foregroundLuminance, backgroundLuminance) + 0.05) / (Math.min(foregroundLuminance, backgroundLuminance) + 0.05);
|
|
269
|
-
};
|
|
270
|
-
const hex = (color) => `#${color.slice(0, 3).map((value) => Math.round(value).toString(16).padStart(2, "0")).join("")}`;
|
|
271
|
-
const area = (right - left) * (bottom - top);
|
|
272
|
-
const step = Math.max(1, Math.floor(Math.sqrt(area / 12e3)));
|
|
273
|
-
const offsetAt = (x, y) => (y * normal.width + x) * 4;
|
|
274
|
-
const deltaAt = (offset) => Math.max(
|
|
275
|
-
Math.abs(normal.data[offset] - background.data[offset]),
|
|
276
|
-
Math.abs(normal.data[offset + 1] - background.data[offset + 1]),
|
|
277
|
-
Math.abs(normal.data[offset + 2] - background.data[offset + 2])
|
|
278
|
-
);
|
|
279
|
-
let strongestDelta = 0;
|
|
280
|
-
let strongestForeground = null;
|
|
281
|
-
let opaqueBackground = null;
|
|
282
|
-
for (let y = top; y < bottom; y += step) {
|
|
283
|
-
for (let x = left; x < right; x += step) {
|
|
284
|
-
const offset = offsetAt(x, y);
|
|
285
|
-
if (background.data[offset + 3] < 250) continue;
|
|
286
|
-
const behind = [
|
|
287
|
-
background.data[offset],
|
|
288
|
-
background.data[offset + 1],
|
|
289
|
-
background.data[offset + 2]
|
|
290
|
-
];
|
|
291
|
-
opaqueBackground ??= behind;
|
|
292
|
-
const delta = deltaAt(offset);
|
|
293
|
-
if (delta <= strongestDelta) continue;
|
|
294
|
-
strongestDelta = delta;
|
|
295
|
-
strongestForeground = [
|
|
296
|
-
normal.data[offset],
|
|
297
|
-
normal.data[offset + 1],
|
|
298
|
-
normal.data[offset + 2]
|
|
299
|
-
];
|
|
233
|
+
return { settled: false, elapsed: Date.now() - start }
|
|
234
|
+
})()`
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
timeoutMs: maxMs + 1e3
|
|
300
238
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
const ratio = ratioOf(foreground, behind);
|
|
307
|
-
if (ratio >= bestRatio) return;
|
|
308
|
-
bestRatio = ratio;
|
|
309
|
-
bestForeground = foreground;
|
|
310
|
-
bestBackground = behind;
|
|
239
|
+
);
|
|
240
|
+
const { elapsed, settled } = result ?? {};
|
|
241
|
+
return {
|
|
242
|
+
elapsed: typeof elapsed === "number" ? elapsed : maxMs,
|
|
243
|
+
settled: settled === true
|
|
311
244
|
};
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// cli/commands/inspect/actions.ts
|
|
248
|
+
var SCREEN_EVAL = `(() => {
|
|
249
|
+
const spec = window.SootSim?.state?.engineSnapshot?.windowState?.deviceSpec
|
|
250
|
+
return spec && spec.width > 0 && spec.height > 0
|
|
251
|
+
? { width: spec.width, height: spec.height }
|
|
252
|
+
: null
|
|
253
|
+
})()`;
|
|
254
|
+
var DEFAULT_AGENT_TIMING = {
|
|
255
|
+
initialWaitMs: 3e3,
|
|
256
|
+
deadlineMs: 5e3,
|
|
257
|
+
retryWaitMs: 700
|
|
258
|
+
};
|
|
259
|
+
var DEFAULT_INTERACTIVE_TIMING = {
|
|
260
|
+
initialWaitMs: 1200,
|
|
261
|
+
deadlineMs: 2500,
|
|
262
|
+
retryWaitMs: 700
|
|
263
|
+
};
|
|
264
|
+
function tapTiming(agent, opts = {}) {
|
|
265
|
+
return {
|
|
266
|
+
...agent ? DEFAULT_AGENT_TIMING : DEFAULT_INTERACTIVE_TIMING,
|
|
267
|
+
...opts
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
function isTapSuccess(result) {
|
|
271
|
+
if (!result || result.hit === false || result.ok === false) return false;
|
|
272
|
+
if (result.requestedTargetMatched === false) return false;
|
|
273
|
+
if (result.pointerTapHandled === false && !result.keyboardOpened && !result.isTextInput) {
|
|
274
|
+
return false;
|
|
335
275
|
}
|
|
276
|
+
return true;
|
|
277
|
+
}
|
|
278
|
+
function tapTargetFromPayload(payload, textFallback) {
|
|
336
279
|
return {
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
280
|
+
nodeId: payload.target?.nodeId ?? payload.match?.nodeId ?? payload.node?.nodeId ?? null,
|
|
281
|
+
id: payload.target?.id ?? payload.match?.id ?? payload.node?.id ?? null,
|
|
282
|
+
testID: payload.target?.testID ?? payload.match?.testID ?? payload.node?.testID ?? null,
|
|
283
|
+
text: payload.target?.text ?? payload.target?.accessibilityLabel ?? payload.match?.text ?? payload.match?.accessibilityLabel ?? payload.node?.text ?? textFallback ?? null,
|
|
284
|
+
type: payload.target?.type ?? payload.match?.type ?? payload.node?.type ?? null
|
|
341
285
|
};
|
|
342
286
|
}
|
|
343
|
-
function
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
if (!style) return 0
|
|
379
|
-
const raw = style.fontSize
|
|
380
|
-
const n = typeof raw === 'number' ? raw : (typeof raw === 'string' ? parseFloat(raw) : NaN)
|
|
381
|
-
return Number.isFinite(n) ? Math.round(n) : 0
|
|
382
|
-
}
|
|
383
|
-
// styling extractors \u2014 omit boring defaults so the agent only sees notable
|
|
384
|
-
// styling (low opacity, struck text, clipping, a real border/color).
|
|
385
|
-
const colorStr = (v) => {
|
|
386
|
-
if (v == null) return undefined
|
|
387
|
-
const s = String(v).split(' ').join('')
|
|
388
|
-
return s && s !== 'transparent' && s !== 'rgba(0,0,0,0)' ? s : undefined
|
|
389
|
-
}
|
|
390
|
-
const opacityOf = (style) => {
|
|
391
|
-
const o = style && style.opacity
|
|
392
|
-
const n = typeof o === 'number' ? o : (typeof o === 'string' ? parseFloat(o) : NaN)
|
|
393
|
-
return Number.isFinite(n) && n < 1 ? Math.round(n * 100) / 100 : undefined
|
|
394
|
-
}
|
|
395
|
-
const weightOf = (style) => {
|
|
396
|
-
const w = style && style.fontWeight
|
|
397
|
-
const n = w === 'bold' ? 700 : (typeof w === 'number' ? w : (typeof w === 'string' ? parseInt(w, 10) : NaN))
|
|
398
|
-
return Number.isFinite(n) && n >= 600 ? n : undefined
|
|
399
|
-
}
|
|
400
|
-
const decoOf = (style) => {
|
|
401
|
-
const d = style && (style.textDecorationLine || style.textDecoration)
|
|
402
|
-
return d && d !== 'none' && d !== 'normal' ? String(d).split(' ')[0] : undefined
|
|
403
|
-
}
|
|
404
|
-
const overflowOf = (style) => (style && style.overflow === 'hidden' ? 'hidden' : undefined)
|
|
405
|
-
const borderWOf = (style) => {
|
|
406
|
-
if (!style) return undefined
|
|
407
|
-
const keys = ['borderWidth','borderTopWidth','borderBottomWidth','borderLeftWidth','borderRightWidth']
|
|
408
|
-
let max = 0
|
|
409
|
-
for (const k of keys) {
|
|
410
|
-
const r = style[k]
|
|
411
|
-
const n = typeof r === 'number' ? r : (typeof r === 'string' ? parseFloat(r) : NaN)
|
|
412
|
-
if (Number.isFinite(n)) max = Math.max(max, n)
|
|
287
|
+
async function tapResolvedTarget(bridge, args) {
|
|
288
|
+
const timing = tapTiming(!!args.agent, args.timing);
|
|
289
|
+
let attempts = 0;
|
|
290
|
+
let lastPayload = null;
|
|
291
|
+
let lastResult = null;
|
|
292
|
+
try {
|
|
293
|
+
await waitForSootsimIdle({
|
|
294
|
+
bridge,
|
|
295
|
+
maxMs: timing.initialWaitMs,
|
|
296
|
+
pollMs: 32,
|
|
297
|
+
stablePolls: 2
|
|
298
|
+
});
|
|
299
|
+
} catch {
|
|
300
|
+
}
|
|
301
|
+
const deadline = Date.now() + timing.deadlineMs;
|
|
302
|
+
let lastOffscreenAt = null;
|
|
303
|
+
while (Date.now() <= deadline || attempts === 0) {
|
|
304
|
+
attempts++;
|
|
305
|
+
const payload = await args.resolve();
|
|
306
|
+
lastPayload = payload;
|
|
307
|
+
if (payload?.error === "bridge-not-ready" || payload?.ambiguous || payload?.nthOutOfRange) {
|
|
308
|
+
return { payload, result: null, attempts, failure: "special" };
|
|
309
|
+
}
|
|
310
|
+
if (payload && typeof payload.cx === "number" && typeof payload.cy === "number") {
|
|
311
|
+
if (payload.offscreen) {
|
|
312
|
+
lastResult = {
|
|
313
|
+
hit: false,
|
|
314
|
+
reason: "offscreen",
|
|
315
|
+
x: payload.cx,
|
|
316
|
+
y: payload.cy,
|
|
317
|
+
screen: payload.screen
|
|
318
|
+
};
|
|
319
|
+
const at = `${Math.round(payload.cx)},${Math.round(payload.cy)}`;
|
|
320
|
+
if (lastOffscreenAt === at) {
|
|
321
|
+
return { payload, result: lastResult, attempts, failure: "missed" };
|
|
413
322
|
}
|
|
414
|
-
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
323
|
+
lastOffscreenAt = at;
|
|
324
|
+
} else {
|
|
325
|
+
lastOffscreenAt = null;
|
|
326
|
+
const requestedTarget = tapTargetFromPayload(payload, args.textFallback);
|
|
327
|
+
const verification = await bridge.send({
|
|
328
|
+
type: "evaluate",
|
|
329
|
+
code: `(async () => {
|
|
330
|
+
const t = window.__sootsimTest
|
|
331
|
+
if (
|
|
332
|
+
!t ||
|
|
333
|
+
typeof t.inspectAt !== 'function' ||
|
|
334
|
+
typeof t.resolveTapTarget !== 'function'
|
|
335
|
+
) {
|
|
336
|
+
return { error: 'tap-target-inspection-unavailable' }
|
|
337
|
+
}
|
|
338
|
+
const requestedTarget = ${JSON.stringify(requestedTarget)}
|
|
339
|
+
const inspected = await t.inspectAt(${payload.cx}, ${payload.cy})
|
|
340
|
+
const resolved =
|
|
341
|
+
inspected && typeof inspected.nodeId === 'number'
|
|
342
|
+
? await t.resolveTapTarget(inspected.nodeId)
|
|
343
|
+
: null
|
|
344
|
+
const node = (resolved && resolved.target) || inspected
|
|
345
|
+
const coordinateTarget = node
|
|
346
|
+
? {
|
|
347
|
+
nodeId: node.nodeId ?? null,
|
|
348
|
+
id: node.id ?? null,
|
|
349
|
+
testID: node.testID ?? null,
|
|
350
|
+
text: node.text ?? node.accessibilityLabel ?? null,
|
|
351
|
+
type: node.type ?? null,
|
|
352
|
+
}
|
|
353
|
+
: null
|
|
354
|
+
const coordinateAncestors = Array.isArray(inspected?.ancestors)
|
|
355
|
+
? inspected.ancestors
|
|
356
|
+
: []
|
|
357
|
+
const ancestorMatchesRequested = coordinateAncestors.some(
|
|
358
|
+
(ancestor) =>
|
|
359
|
+
(typeof requestedTarget.nodeId === 'number' &&
|
|
360
|
+
ancestor?.nodeId === requestedTarget.nodeId) ||
|
|
361
|
+
(requestedTarget.testID &&
|
|
362
|
+
ancestor?.testID === requestedTarget.testID) ||
|
|
363
|
+
(requestedTarget.id && ancestor?.id === requestedTarget.id),
|
|
364
|
+
)
|
|
365
|
+
const requestedTargetMatched =
|
|
366
|
+
coordinateTarget === null
|
|
367
|
+
? false
|
|
368
|
+
: typeof requestedTarget.nodeId === 'number' &&
|
|
369
|
+
typeof coordinateTarget.nodeId === 'number'
|
|
370
|
+
? requestedTarget.nodeId === coordinateTarget.nodeId ||
|
|
371
|
+
ancestorMatchesRequested
|
|
372
|
+
: requestedTarget.testID && coordinateTarget.testID
|
|
373
|
+
? requestedTarget.testID === coordinateTarget.testID
|
|
374
|
+
|| ancestorMatchesRequested
|
|
375
|
+
: requestedTarget.id && coordinateTarget.id
|
|
376
|
+
? requestedTarget.id === coordinateTarget.id
|
|
377
|
+
|| ancestorMatchesRequested
|
|
378
|
+
: ancestorMatchesRequested
|
|
379
|
+
if (!requestedTargetMatched) {
|
|
380
|
+
return {
|
|
381
|
+
requestedTargetMatched,
|
|
382
|
+
requestedTarget,
|
|
383
|
+
coordinateTarget,
|
|
454
384
|
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
385
|
+
}
|
|
386
|
+
if (typeof t.activatePressAt !== 'function') {
|
|
387
|
+
return { error: 'tap-target-activation-unavailable' }
|
|
388
|
+
}
|
|
389
|
+
const activation = await t.activatePressAt(
|
|
390
|
+
${payload.cx},
|
|
391
|
+
${payload.cy},
|
|
392
|
+
requestedTarget.nodeId,
|
|
393
|
+
)
|
|
394
|
+
if (activation?.ok) {
|
|
395
|
+
window.dispatchEvent(
|
|
396
|
+
new CustomEvent('sootsim:agentAction', {
|
|
397
|
+
detail: {
|
|
398
|
+
type: 'tap',
|
|
399
|
+
x: ${payload.cx},
|
|
400
|
+
y: ${payload.cy},
|
|
401
|
+
target: requestedTarget,
|
|
402
|
+
},
|
|
403
|
+
}),
|
|
404
|
+
)
|
|
405
|
+
}
|
|
406
|
+
return {
|
|
407
|
+
activation,
|
|
408
|
+
requestedTargetMatched,
|
|
409
|
+
requestedTarget,
|
|
410
|
+
coordinateTarget,
|
|
411
|
+
}
|
|
412
|
+
})()`
|
|
413
|
+
});
|
|
414
|
+
if (verification?.error) {
|
|
415
|
+
return {
|
|
416
|
+
payload,
|
|
417
|
+
result: {
|
|
418
|
+
hit: false,
|
|
419
|
+
reason: verification.error,
|
|
420
|
+
requestedTarget
|
|
421
|
+
},
|
|
422
|
+
attempts,
|
|
423
|
+
failure: "special"
|
|
424
|
+
};
|
|
464
425
|
}
|
|
465
|
-
if (
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
color: text ? colorStr(style.color) : undefined,
|
|
477
|
-
bg: colorStr(style.backgroundColor),
|
|
478
|
-
opacity: opacityOf(style),
|
|
479
|
-
fontWeight: weightOf(style),
|
|
480
|
-
textDecoration: decoOf(style),
|
|
481
|
-
overflow: overflowOf(style),
|
|
482
|
-
borderWidth,
|
|
483
|
-
borderColor: borderWidth ? borderColorOf(style) : undefined,
|
|
426
|
+
if (verification?.requestedTargetMatched === false) {
|
|
427
|
+
return {
|
|
428
|
+
payload,
|
|
429
|
+
result: {
|
|
430
|
+
hit: false,
|
|
431
|
+
reason: "target-covered",
|
|
432
|
+
...verification
|
|
433
|
+
},
|
|
434
|
+
attempts,
|
|
435
|
+
failure: "missed"
|
|
436
|
+
};
|
|
484
437
|
}
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
const screenshot = window.SootSim && window.SootSim.bridges && window.SootSim.bridges.screenshot
|
|
490
|
-
if (typeof screenshot !== 'function') {
|
|
491
|
-
throw new Error('styled native layout requires the rnx capture bridge')
|
|
492
|
-
}
|
|
493
|
-
const device = list.find((node) => node && node.device)?.device || {}
|
|
494
|
-
const width = Math.round(Number(device.width) || window.innerWidth || 0)
|
|
495
|
-
const height = Math.round(Number(device.height) || window.innerHeight || 0)
|
|
496
|
-
if (!width || !height) throw new Error('native layout has no capture dimensions')
|
|
497
|
-
const capture = async (suppressText) => {
|
|
498
|
-
const dataUrl = await screenshot({
|
|
499
|
-
format: 'png',
|
|
500
|
-
outputWidth: width,
|
|
501
|
-
outputHeight: height,
|
|
502
|
-
...(suppressText ? { suppressText: true } : {}),
|
|
503
|
-
})
|
|
504
|
-
const image = await new Promise((resolve, reject) => {
|
|
505
|
-
const next = new Image()
|
|
506
|
-
next.onload = () => resolve(next)
|
|
507
|
-
next.onerror = () => reject(new Error('could not decode native layout pixels'))
|
|
508
|
-
next.src = dataUrl
|
|
509
|
-
})
|
|
510
|
-
const canvas = document.createElement('canvas')
|
|
511
|
-
canvas.width = image.naturalWidth
|
|
512
|
-
canvas.height = image.naturalHeight
|
|
513
|
-
const context = canvas.getContext('2d', { willReadFrequently: true })
|
|
514
|
-
if (!context) throw new Error('could not read native layout pixels')
|
|
515
|
-
context.drawImage(image, 0, 0)
|
|
516
|
-
return context.getImageData(0, 0, canvas.width, canvas.height)
|
|
517
|
-
}
|
|
518
|
-
const normal = await capture(false)
|
|
519
|
-
const background = await capture(true)
|
|
520
|
-
for (const element of textElements) {
|
|
521
|
-
element.contrast = scoreContrast({
|
|
522
|
-
normal,
|
|
523
|
-
background,
|
|
524
|
-
box: element.box,
|
|
525
|
-
fontSize: element.fontSize,
|
|
526
|
-
fontWeight: element.fontWeight,
|
|
527
|
-
})
|
|
438
|
+
const dispatched = verification?.activation?.tap ?? verification?.activation;
|
|
439
|
+
const result = dispatched && typeof dispatched === "object" ? { ...dispatched, ...verification?.activation, ...verification } : { hit: !!dispatched, ...verification };
|
|
440
|
+
lastResult = result;
|
|
441
|
+
if (isTapSuccess(result)) return { payload, result, attempts };
|
|
528
442
|
}
|
|
529
|
-
|
|
530
|
-
|
|
443
|
+
}
|
|
444
|
+
const remaining = deadline - Date.now();
|
|
445
|
+
if (remaining <= 0) break;
|
|
446
|
+
try {
|
|
447
|
+
await waitForSootsimIdle({
|
|
448
|
+
bridge,
|
|
449
|
+
maxMs: Math.min(remaining, timing.retryWaitMs),
|
|
450
|
+
pollMs: 32,
|
|
451
|
+
stablePolls: 2
|
|
452
|
+
});
|
|
453
|
+
} catch {
|
|
454
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(120, remaining)));
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
return {
|
|
458
|
+
payload: lastPayload,
|
|
459
|
+
result: lastResult,
|
|
460
|
+
attempts,
|
|
461
|
+
failure: lastPayload && typeof lastPayload.cx === "number" ? "missed" : "not-found"
|
|
462
|
+
};
|
|
531
463
|
}
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
464
|
+
async function tapCoordinates(bridge, x, y, target) {
|
|
465
|
+
return bridge.send({
|
|
466
|
+
type: "tap",
|
|
467
|
+
x,
|
|
468
|
+
y,
|
|
469
|
+
...target ? { target } : {}
|
|
537
470
|
});
|
|
538
|
-
return Array.isArray(raw) ? raw : [];
|
|
539
471
|
}
|
|
540
|
-
function
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
472
|
+
async function tapById(bridge, query, opts = {}) {
|
|
473
|
+
const arg = JSON.stringify(query);
|
|
474
|
+
return tapResolvedTarget(bridge, {
|
|
475
|
+
agent: opts.agent,
|
|
476
|
+
timing: opts.timing,
|
|
477
|
+
resolve: () => bridge.send({
|
|
478
|
+
type: "evaluate",
|
|
479
|
+
code: `(async () => {
|
|
480
|
+
const t = window.__sootsimTest
|
|
481
|
+
if (!t) return null
|
|
482
|
+
const n = (await t.findByTestId(${arg})) || (await t.findById(${arg}))
|
|
483
|
+
if (!n || !n.absolutePosition || !n.layout) return { cx: null }
|
|
484
|
+
const resolved =
|
|
485
|
+
typeof n.nodeId === 'number' && typeof t.resolveTapTarget === 'function'
|
|
486
|
+
? await t.resolveTapTarget(n.nodeId)
|
|
487
|
+
: null
|
|
488
|
+
const target = (resolved && resolved.target) || n
|
|
489
|
+
const cx =
|
|
490
|
+
resolved && typeof resolved.cx === 'number'
|
|
491
|
+
? resolved.cx
|
|
492
|
+
: n.absolutePosition.x + (n.layout.width || 0) / 2
|
|
493
|
+
const cy =
|
|
494
|
+
resolved && typeof resolved.cy === 'number'
|
|
495
|
+
? resolved.cy
|
|
496
|
+
: n.absolutePosition.y + (n.layout.height || 0) / 2
|
|
497
|
+
const scr = ${SCREEN_EVAL}
|
|
498
|
+
const offscreen =
|
|
499
|
+
!!scr && (cx < 0 || cy < 0 || cx > scr.width || cy > scr.height)
|
|
500
|
+
return {
|
|
501
|
+
cx,
|
|
502
|
+
cy,
|
|
503
|
+
...(offscreen ? { offscreen: true, screen: scr } : {}),
|
|
504
|
+
match: {
|
|
505
|
+
nodeId: n.nodeId ?? null,
|
|
506
|
+
id: n.id,
|
|
507
|
+
testID: n.testID,
|
|
508
|
+
text: n.text ?? n.accessibilityLabel ?? null,
|
|
509
|
+
type: n.type,
|
|
510
|
+
},
|
|
511
|
+
target: {
|
|
512
|
+
nodeId: target.nodeId ?? null,
|
|
513
|
+
id: target.id,
|
|
514
|
+
testID: target.testID,
|
|
515
|
+
text:
|
|
516
|
+
target.text ??
|
|
517
|
+
target.accessibilityLabel ??
|
|
518
|
+
n.text ??
|
|
519
|
+
n.accessibilityLabel ??
|
|
520
|
+
null,
|
|
521
|
+
type: target.type,
|
|
522
|
+
},
|
|
523
|
+
strategy: (resolved && resolved.strategy) || 'matched-node',
|
|
524
|
+
}
|
|
525
|
+
})()`
|
|
526
|
+
})
|
|
575
527
|
});
|
|
576
|
-
return lines.join("\n");
|
|
577
528
|
}
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
529
|
+
async function tapByText(bridge, query, options = {}, opts = {}) {
|
|
530
|
+
const filterArg = JSON.stringify({
|
|
531
|
+
query,
|
|
532
|
+
exact: !!options.exact,
|
|
533
|
+
role: options.role ?? null,
|
|
534
|
+
within: options.within ?? null,
|
|
535
|
+
minX: options.minX ?? null,
|
|
536
|
+
maxX: options.maxX ?? null,
|
|
537
|
+
minY: options.minY ?? null,
|
|
538
|
+
maxY: options.maxY ?? null,
|
|
539
|
+
near: options.near ?? null,
|
|
540
|
+
nth: options.nth ?? null,
|
|
541
|
+
first: !!options.first
|
|
542
|
+
});
|
|
543
|
+
return tapResolvedTarget(bridge, {
|
|
544
|
+
agent: opts.agent,
|
|
545
|
+
timing: opts.timing,
|
|
546
|
+
textFallback: query,
|
|
547
|
+
resolve: () => bridge.send({
|
|
548
|
+
type: "evaluate",
|
|
549
|
+
code: `(async () => {
|
|
550
|
+
const t = window.__sootsimTest
|
|
551
|
+
if (!t) return { error: 'bridge-not-ready' }
|
|
552
|
+
const F = ${filterArg}
|
|
581
553
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
const readState = (n) => n?.accessibilityState || n?.state || null
|
|
588
|
-
const readTestId = (n) => n?.testID || n?.testId || null
|
|
589
|
-
const readType = (n) => typeof n?.type === 'string' ? n.type.toLowerCase() : n?.type
|
|
590
|
-
const readPressable = (n) => {
|
|
591
|
-
if (n?.pressable) return true
|
|
592
|
-
const handlers = Array.isArray(n?.handlers) ? n.handlers : []
|
|
593
|
-
const role = readRole(n)
|
|
594
|
-
return handlers.length > 0 || role === 'button' || role === 'link' || role === 'tab'
|
|
595
|
-
}
|
|
596
|
-
const isTextInput = (n) => n?.isTextInput === true
|
|
597
|
-
const isTextNode = (n) => readType(n) === 'text'
|
|
598
|
-
const isVisibleTarget = (n) => {
|
|
599
|
-
const layout = readLayout(n)
|
|
600
|
-
if (!layout || layout.width <= 0 || layout.height <= 0) return false
|
|
601
|
-
const abs = readAbs(n)
|
|
602
|
-
if (!abs) return true
|
|
603
|
-
const device = n?.device || {}
|
|
604
|
-
const screenW = Number(device.width) || window.innerWidth || 0
|
|
605
|
-
const screenH = Number(device.height) || window.innerHeight || 0
|
|
606
|
-
return (
|
|
607
|
-
abs.x + layout.width > 0 &&
|
|
608
|
-
abs.y + layout.height > 0 &&
|
|
609
|
-
abs.x < screenW &&
|
|
610
|
-
abs.y < screenH
|
|
611
|
-
)
|
|
612
|
-
}
|
|
613
|
-
const hasAccessibleSignal = (n) => {
|
|
614
|
-
const role = readRole(n)
|
|
615
|
-
const label = readLabel(n)
|
|
616
|
-
const hint = readHint(n)
|
|
617
|
-
if (role) return true
|
|
618
|
-
if (hint) return true
|
|
619
|
-
if (isTextInput(n)) return true
|
|
620
|
-
if (readPressable(n)) return true
|
|
621
|
-
if (isTextNode(n) && n?.text) return true
|
|
622
|
-
if (typeof label === 'string' && label.length <= 30 && label !== n?.text) return true
|
|
623
|
-
return false
|
|
624
|
-
}
|
|
625
|
-
const normalize = (n) => {
|
|
626
|
-
const layout = readLayout(n)
|
|
627
|
-
const abs = readAbs(n)
|
|
628
|
-
const role = readRole(n) || (readPressable(n) ? 'button' : isTextInput(n) ? 'textfield' : isTextNode(n) ? 'statictext' : 'none')
|
|
629
|
-
return {
|
|
630
|
-
role,
|
|
631
|
-
label: readLabel(n),
|
|
632
|
-
hint: readHint(n),
|
|
633
|
-
state: readState(n),
|
|
634
|
-
testID: readTestId(n),
|
|
635
|
-
position: abs ? { x: Math.round(abs.x), y: Math.round(abs.y) } : null,
|
|
636
|
-
size: layout ? { w: Math.round(layout.width), h: Math.round(layout.height) } : null,
|
|
637
|
-
}
|
|
638
|
-
}
|
|
554
|
+
const res = await t.queryTextCandidates({
|
|
555
|
+
query: F.query,
|
|
556
|
+
exact: !!F.exact,
|
|
557
|
+
...(F.role ? { role: F.role } : {}),
|
|
558
|
+
})
|
|
639
559
|
|
|
640
|
-
|
|
641
|
-
const list = await t.listInspectable({})
|
|
642
|
-
if (Array.isArray(list)) {
|
|
643
|
-
return list.filter(n => isVisibleTarget(n) && hasAccessibleSignal(n)).map(normalize)
|
|
644
|
-
}
|
|
645
|
-
}
|
|
560
|
+
let candidates = res.candidates || []
|
|
646
561
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
? {
|
|
670
|
-
x: Math.max(0, absolutePosition.x),
|
|
671
|
-
y: Math.max(0, absolutePosition.y),
|
|
672
|
-
width: Math.max(
|
|
673
|
-
0,
|
|
674
|
-
Math.min(screenW, absolutePosition.x + layout.width) -
|
|
675
|
-
Math.max(0, absolutePosition.x),
|
|
676
|
-
),
|
|
677
|
-
height: Math.max(
|
|
678
|
-
0,
|
|
679
|
-
Math.min(screenH, absolutePosition.y + layout.height) -
|
|
680
|
-
Math.max(0, absolutePosition.y),
|
|
681
|
-
),
|
|
682
|
-
}
|
|
683
|
-
: null
|
|
684
|
-
const handlers = Array.isArray(n.handlers) ? n.handlers : []
|
|
685
|
-
const pressable =
|
|
686
|
-
handlers.length > 0 ||
|
|
687
|
-
role === 'button' ||
|
|
688
|
-
role === 'link' ||
|
|
689
|
-
role === 'tab'
|
|
690
|
-
return {
|
|
691
|
-
type: typeof n.type === 'string' ? n.type.toLowerCase() : n.type,
|
|
692
|
-
id: n.id ?? n.nodeId,
|
|
693
|
-
nodeId: n.nodeId,
|
|
694
|
-
testID: n.testID ?? n.testId,
|
|
695
|
-
text: n.text,
|
|
696
|
-
layout,
|
|
697
|
-
absolutePosition,
|
|
698
|
-
visibleFrame,
|
|
699
|
-
style: n.computedStyle ?? n.style ?? {},
|
|
700
|
-
childCount: n.childCount ?? 0,
|
|
701
|
-
pressed: n.pressed ?? false,
|
|
702
|
-
pressable,
|
|
703
|
-
isTextInput: n.isTextInput ?? false,
|
|
704
|
-
accessible: n.accessible ?? true,
|
|
705
|
-
accessibilityLabel: label,
|
|
706
|
-
accessibilityRole: role,
|
|
707
|
-
accessibilityHint: n.accessibilityHint,
|
|
708
|
-
accessibilityState: n.accessibilityState,
|
|
709
|
-
accessibilityValue: n.accessibilityValue,
|
|
710
|
-
}
|
|
711
|
-
})
|
|
712
|
-
}
|
|
713
|
-
const isVisibleTarget = (n) => {
|
|
714
|
-
if (!n?.layout) return false
|
|
715
|
-
const frame = n.visibleFrame
|
|
716
|
-
if (frame) return frame.width > 0 && frame.height > 0
|
|
717
|
-
const abs = n.absolutePosition
|
|
718
|
-
if (!abs) return n.layout.width > 0 && n.layout.height > 0
|
|
719
|
-
const screenW = window.innerWidth || 0
|
|
720
|
-
const screenH = window.innerHeight || 0
|
|
721
|
-
return (
|
|
722
|
-
abs.x + n.layout.width > 0 &&
|
|
723
|
-
abs.y + n.layout.height > 0 &&
|
|
724
|
-
abs.x < screenW &&
|
|
725
|
-
abs.y < screenH
|
|
726
|
-
)
|
|
727
|
-
}
|
|
728
|
-
`;
|
|
729
|
-
function resolveFindMode(q) {
|
|
730
|
-
if (q.testId) {
|
|
731
|
-
return {
|
|
732
|
-
mode: "testid",
|
|
733
|
-
code: `(async () => {
|
|
734
|
-
const t = window.__sootsimTest
|
|
735
|
-
if (!t) return null
|
|
736
|
-
return (await t.findByTestId(${JSON.stringify(q.testId)})) || (await t.findById(${JSON.stringify(q.testId)}))
|
|
737
|
-
})()`
|
|
738
|
-
};
|
|
739
|
-
}
|
|
740
|
-
if (q.role) {
|
|
741
|
-
return {
|
|
742
|
-
mode: "role",
|
|
743
|
-
code: `(async () => {
|
|
744
|
-
const t = window.__sootsimTest
|
|
745
|
-
if (!t) return []
|
|
746
|
-
return await t.queryAll({ hasRole: ${JSON.stringify(q.role)}, pruneHidden: true })
|
|
747
|
-
})()`
|
|
748
|
-
};
|
|
749
|
-
}
|
|
750
|
-
if (q.type) {
|
|
751
|
-
return {
|
|
752
|
-
mode: "type",
|
|
753
|
-
code: `(async () => {
|
|
754
|
-
const t = window.__sootsimTest
|
|
755
|
-
if (!t) return []
|
|
756
|
-
return await t.queryAll({ type: ${JSON.stringify(q.type)}, pruneHidden: true })
|
|
757
|
-
})()`
|
|
758
|
-
};
|
|
759
|
-
}
|
|
760
|
-
if (q.pressable) {
|
|
761
|
-
return {
|
|
762
|
-
mode: "pressable",
|
|
763
|
-
code: `(async () => {
|
|
764
|
-
const t = window.__sootsimTest
|
|
765
|
-
if (!t) return []
|
|
766
|
-
${FIND_INSPECTABLE_TARGETS}
|
|
767
|
-
const inspectable = await fromInspectable()
|
|
768
|
-
if (inspectable) return inspectable.filter(n => n.pressable && isVisibleTarget(n))
|
|
769
|
-
const all = await t.queryAll({ pruneHidden: true })
|
|
770
|
-
return all.filter(n => n.pressable && isVisibleTarget(n))
|
|
771
|
-
})()`
|
|
772
|
-
};
|
|
773
|
-
}
|
|
774
|
-
if (q.interactive) {
|
|
775
|
-
return {
|
|
776
|
-
mode: "interactive-targets",
|
|
777
|
-
code: `(async () => {
|
|
778
|
-
const t = window.__sootsimTest
|
|
779
|
-
if (!t) return []
|
|
780
|
-
${FIND_INSPECTABLE_TARGETS}
|
|
781
|
-
const inspectable = await fromInspectable()
|
|
782
|
-
if (inspectable) {
|
|
783
|
-
return inspectable.filter(n => n.pressable && isVisibleTarget(n))
|
|
784
|
-
}
|
|
785
|
-
const all = await t.queryAll({ pruneHidden: true })
|
|
786
|
-
return all.filter(n => n.pressable && isVisibleTarget(n))
|
|
787
|
-
})()`
|
|
788
|
-
};
|
|
789
|
-
}
|
|
790
|
-
if (q.visible) {
|
|
791
|
-
return {
|
|
792
|
-
mode: "visible",
|
|
793
|
-
code: `(async () => {
|
|
794
|
-
const t = window.__sootsimTest
|
|
795
|
-
if (!t) return []
|
|
796
|
-
const all = await t.queryAll({ pruneHidden: true })
|
|
797
|
-
return all.filter(n => n.layout && n.layout.width > 0 && n.layout.height > 0)
|
|
798
|
-
})()`
|
|
799
|
-
};
|
|
800
|
-
}
|
|
801
|
-
if (q.text) {
|
|
802
|
-
return {
|
|
803
|
-
mode: "text",
|
|
804
|
-
code: `(async () => {
|
|
805
|
-
const t = window.__sootsimTest
|
|
806
|
-
if (!t) return null
|
|
807
|
-
return await t.findByText(${JSON.stringify(q.text)})
|
|
808
|
-
})()`
|
|
809
|
-
};
|
|
810
|
-
}
|
|
811
|
-
return null;
|
|
812
|
-
}
|
|
813
|
-
async function inspectFind(bridge, q) {
|
|
814
|
-
const resolved = resolveFindMode(q);
|
|
815
|
-
if (!resolved) return null;
|
|
816
|
-
const result = await bridge.send({ type: "evaluate", code: resolved.code });
|
|
817
|
-
return { mode: resolved.mode, result };
|
|
818
|
-
}
|
|
819
|
-
function rankInteractive(nodes) {
|
|
820
|
-
return [...nodes].sort((a, b) => scoreInteractive(b) - scoreInteractive(a));
|
|
821
|
-
}
|
|
822
|
-
function scoreInteractive(n) {
|
|
823
|
-
let score = 0;
|
|
824
|
-
if (n.testID) score += 100;
|
|
825
|
-
if (typeof n.text === "string" && n.text.trim().length > 0) score += 60;
|
|
826
|
-
if (typeof n.accessibilityLabel === "string" && n.accessibilityLabel.trim().length > 0) {
|
|
827
|
-
score += 30;
|
|
828
|
-
}
|
|
829
|
-
if (n.accessibilityRole) score += 15;
|
|
830
|
-
const w = n.layout?.width ?? 0;
|
|
831
|
-
const h = n.layout?.height ?? 0;
|
|
832
|
-
const area = w * h;
|
|
833
|
-
if (area >= 400 && area <= 6e4) score += 25;
|
|
834
|
-
else if (area > 6e4) score -= 20;
|
|
835
|
-
const y = n.absolutePosition?.y ?? 0;
|
|
836
|
-
if (y < 0) score -= 30;
|
|
837
|
-
return score;
|
|
838
|
-
}
|
|
839
|
-
function tapCommandForNode(n) {
|
|
840
|
-
if (n.testID) return `rnxsim do tap-id ${shellEscape(n.testID)}`;
|
|
841
|
-
const text = typeof n.text === "string" ? n.text.trim() : "";
|
|
842
|
-
if (text.length > 0 && text.length <= 80)
|
|
843
|
-
return `rnxsim do tap-text ${shellEscape(text)}`;
|
|
844
|
-
const x = Math.round(((n.absolutePosition?.x ?? 0) + (n.layout?.width ?? 0) / 2) * 10) / 10;
|
|
845
|
-
const y = Math.round(((n.absolutePosition?.y ?? 0) + (n.layout?.height ?? 0) / 2) * 10) / 10;
|
|
846
|
-
return `rnxsim do tap ${x} ${y}`;
|
|
847
|
-
}
|
|
848
|
-
function shellEscape(value) {
|
|
849
|
-
if (/^[A-Za-z0-9_./@:-]+$/.test(value)) return value;
|
|
850
|
-
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
851
|
-
}
|
|
852
|
-
var WAIT_READY_PROBE = `(async () => {
|
|
853
|
-
const t = window.__sootsimTest
|
|
854
|
-
let nodes = 0
|
|
855
|
-
try { nodes = (await t?.getNodeCount?.()) || 0 } catch {}
|
|
856
|
-
let targets = 0
|
|
857
|
-
let liveFrameActive = false
|
|
858
|
-
let liveFrameChannels = 0
|
|
859
|
-
let liveFramePublishes = 0
|
|
860
|
-
let loadingText = ''
|
|
861
|
-
const cleanText = (value) =>
|
|
862
|
-
typeof value === 'string' ? value.replace(/\\s+/g, ' ').trim() : ''
|
|
863
|
-
const loadingSignal = (value) => {
|
|
864
|
-
const text = cleanText(value)
|
|
865
|
-
if (!text) return ''
|
|
866
|
-
return /^(opening app\\b|looking for your app\\b|building bundle\\b|preparing rnxsim runtime\\b|loading native bundle\\b|loading rnx stubs\\b|loading bundle loader\\b|loading native stubs\\b|loading compat bridge\\b|loading react-native shim\\b|loading react bridge\\b|loading reconciler\\b|loading replacement modules\\b|injecting rnx modules\\b|warming sqlite engine\\b|capturing root component\\b|reloading app\\b|loading(?:[.!\u2026]*)?$|connecting(?:[.!\u2026]*)?$)/i.test(text)
|
|
867
|
-
? text
|
|
868
|
-
: ''
|
|
869
|
-
}
|
|
870
|
-
try {
|
|
871
|
-
const loading = window.__sootsimConnectRNLoading
|
|
872
|
-
if (loading && loading.active === true) {
|
|
873
|
-
loadingText = [loading.title, loading.detail].map(cleanText).filter(Boolean).join(' ')
|
|
874
|
-
}
|
|
875
|
-
} catch {}
|
|
876
|
-
try {
|
|
877
|
-
if (typeof t?.listInspectable === 'function') {
|
|
878
|
-
const list = await t.listInspectable({})
|
|
879
|
-
if (Array.isArray(list)) {
|
|
880
|
-
const screenW = window.innerWidth || 0
|
|
881
|
-
const screenH = window.innerHeight || 0
|
|
882
|
-
const isVisible = (n) => {
|
|
883
|
-
const layout = n?.layout
|
|
884
|
-
if (!layout || layout.width <= 0 || layout.height <= 0) return false
|
|
885
|
-
const abs = n?.absolutePosition || n?.absolute
|
|
886
|
-
if (!abs) return true
|
|
887
|
-
const device = n?.device || {}
|
|
888
|
-
const w = Number(device.width) || screenW
|
|
889
|
-
const h = Number(device.height) || screenH
|
|
890
|
-
return (
|
|
891
|
-
abs.x + layout.width > 0 &&
|
|
892
|
-
abs.y + layout.height > 0 &&
|
|
893
|
-
abs.x < w &&
|
|
894
|
-
abs.y < h
|
|
895
|
-
)
|
|
896
|
-
}
|
|
897
|
-
const hasContentSignal = (n) => {
|
|
898
|
-
const role = n?.accessibilityRole || n?.role
|
|
899
|
-
const label = cleanText(n?.accessibilityLabel || n?.label)
|
|
900
|
-
const handlers = Array.isArray(n?.handlers) ? n.handlers : []
|
|
901
|
-
const text = cleanText(n?.text)
|
|
902
|
-
const testID = cleanText(n?.testID || n?.testId)
|
|
903
|
-
const loading = loadingSignal(text) || loadingSignal(label) || loadingSignal(testID)
|
|
904
|
-
if (!loadingText && loading) {
|
|
905
|
-
loadingText = loading
|
|
906
|
-
}
|
|
907
|
-
if (handlers.length > 0) return true
|
|
908
|
-
if (role) return true
|
|
909
|
-
if (n?.isTextInput) return true
|
|
910
|
-
if (text) return true
|
|
911
|
-
if (label) return true
|
|
912
|
-
if (testID && !/^splash|loading/i.test(testID)) return true
|
|
913
|
-
return false
|
|
914
|
-
}
|
|
915
|
-
targets = list.filter(n => isVisible(n) && hasContentSignal(n)).length
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
} catch {}
|
|
919
|
-
try {
|
|
920
|
-
const stats = await t?.getImageLoaderStats?.()
|
|
921
|
-
const presentation = stats?.liveFramePresentation
|
|
922
|
-
liveFrameChannels = Number(presentation?.activeChannels) || 0
|
|
923
|
-
liveFramePublishes = Number(presentation?.publishes) || 0
|
|
924
|
-
const previous = window.__sootsimWaitReadyLiveFramePublishes
|
|
925
|
-
liveFrameActive =
|
|
926
|
-
liveFrameChannels > 0 &&
|
|
927
|
-
typeof previous === 'number' &&
|
|
928
|
-
liveFramePublishes > previous
|
|
929
|
-
window.__sootsimWaitReadyLiveFramePublishes = liveFramePublishes
|
|
930
|
-
} catch {}
|
|
931
|
-
let externalReady = null
|
|
932
|
-
let externalStatus = ''
|
|
933
|
-
let externalError = ''
|
|
934
|
-
let suppressedEntryError = ''
|
|
935
|
-
try {
|
|
936
|
-
const getExternalAppState = t?.getExternalAppState
|
|
937
|
-
const external = typeof getExternalAppState === 'function'
|
|
938
|
-
? await Promise.race([
|
|
939
|
-
getExternalAppState(),
|
|
940
|
-
new Promise((resolve) => setTimeout(
|
|
941
|
-
() => resolve({
|
|
942
|
-
state: {
|
|
943
|
-
ready: false,
|
|
944
|
-
loading: true,
|
|
945
|
-
status: 'waiting for tenant worker...',
|
|
946
|
-
},
|
|
947
|
-
}),
|
|
948
|
-
1000,
|
|
949
|
-
)),
|
|
950
|
-
])
|
|
951
|
-
: null
|
|
952
|
-
const state = external?.state
|
|
953
|
-
if (state && typeof state === 'object') {
|
|
954
|
-
externalReady = state.ready === true
|
|
955
|
-
if (typeof state.status === 'string') externalStatus = state.status
|
|
956
|
-
if (typeof state.error === 'string') externalError = state.error
|
|
957
|
-
}
|
|
958
|
-
if (!externalError && Array.isArray(external?.entryErrors)) {
|
|
959
|
-
// suppressed-and-continued entry errors (bundle-loader's patched __r
|
|
960
|
-
// swallows entry throws and moves on) must NOT gate readiness \u2014 the
|
|
961
|
-
// app frequently runs fine after one (e.g. metro's "Unknown named
|
|
962
|
-
// module" probing noise). they surface separately as a diagnostic
|
|
963
|
-
// for the timeout path.
|
|
964
|
-
const isReal = (item) => item && typeof item.message === 'string' && item.message.trim()
|
|
965
|
-
const fatal = external.entryErrors.find((item) => isReal(item) && item.suppressed !== true)
|
|
966
|
-
if (fatal) externalError = fatal.message
|
|
967
|
-
const suppressed = external.entryErrors.find((item) => isReal(item) && item.suppressed === true)
|
|
968
|
-
if (suppressed) suppressedEntryError = suppressed.message
|
|
969
|
-
}
|
|
970
|
-
// once the app has errored its last status ("injecting sootsim
|
|
971
|
-
// modules...") is stale \u2014 reporting it makes a dead app read as one that
|
|
972
|
-
// is still booting.
|
|
973
|
-
if (externalError) loadingText = ''
|
|
974
|
-
else if (externalReady === false && externalStatus) loadingText = externalStatus
|
|
975
|
-
} catch {}
|
|
976
|
-
let errors = 0
|
|
977
|
-
try { errors = window.__sootsimConsole?.count?.()?.errors ?? 0 } catch {}
|
|
978
|
-
return {
|
|
979
|
-
flag: (window).__sootsimExternalAppReady,
|
|
980
|
-
at: (window).__sootsimExternalAppReadyAt || 0,
|
|
981
|
-
nodes,
|
|
982
|
-
targets,
|
|
983
|
-
liveFrameActive,
|
|
984
|
-
liveFrameChannels,
|
|
985
|
-
liveFramePublishes,
|
|
986
|
-
errors,
|
|
987
|
-
loadingText,
|
|
988
|
-
externalReady,
|
|
989
|
-
externalStatus,
|
|
990
|
-
externalError,
|
|
991
|
-
suppressedEntryError,
|
|
992
|
-
}
|
|
993
|
-
})()`;
|
|
994
|
-
var READY_CONTENT_NODE_FLOOR = 100;
|
|
995
|
-
var READY_NODE_STABLE_MS = 750;
|
|
996
|
-
var WAIT_READY_PROGRESS_INTERVAL_MS = 2e3;
|
|
997
|
-
var READY_PROBE_POLL_MS = 150;
|
|
998
|
-
function readyProbeHasContent(probe) {
|
|
999
|
-
return probe.liveFrameActive || probe.targets > 0 || probe.nodes >= READY_CONTENT_NODE_FLOOR;
|
|
1000
|
-
}
|
|
1001
|
-
function readyProbeHasTargetContent(probe) {
|
|
1002
|
-
return probe.liveFrameActive || probe.targets > 0;
|
|
1003
|
-
}
|
|
1004
|
-
function waitReadyReason(status) {
|
|
1005
|
-
if (status.bridgeError) {
|
|
1006
|
-
return `sim unreachable: ${status.bridgeError} \u2014 the target tab is closed or no sim is connected (check \`rnxsim list\`, reopen with \`rnxsim open\`)`;
|
|
1007
|
-
}
|
|
1008
|
-
const base = status.externalError ? `guest app errored: ${status.externalError}` : status.loadingText ? `still showing "${status.loadingText}"` : status.externalReady === false ? "guest app is still loading" : status.flag !== true && status.targets > 0 ? "native content is rendered but the ready signal has not settled" : status.flag !== true ? "guest app has not emitted sootsim:externalAppReady" : status.targets <= 0 ? "ready flag emitted but no visible app content is inspectable yet" : "node tree is still changing";
|
|
1009
|
-
return !status.externalError && status.suppressedEntryError ? `${base} (suppressed entry error: ${status.suppressedEntryError})` : base;
|
|
1010
|
-
}
|
|
1011
|
-
async function inspectWaitReady(bridge, timeoutMs = 2e4, options = {}) {
|
|
1012
|
-
const start = Date.now();
|
|
1013
|
-
const deadline = start + timeoutMs;
|
|
1014
|
-
const progressIntervalMs = options.progressIntervalMs ?? WAIT_READY_PROGRESS_INTERVAL_MS;
|
|
1015
|
-
let nextProgressAt = start + progressIntervalMs;
|
|
1016
|
-
let lastNodes = -1;
|
|
1017
|
-
let nodeStableSince = start;
|
|
1018
|
-
let bridgeError = "";
|
|
1019
|
-
let last = {
|
|
1020
|
-
flag: void 0,
|
|
1021
|
-
at: 0,
|
|
1022
|
-
nodes: 0,
|
|
1023
|
-
targets: 0,
|
|
1024
|
-
liveFrameActive: false,
|
|
1025
|
-
liveFrameChannels: 0,
|
|
1026
|
-
liveFramePublishes: 0,
|
|
1027
|
-
errors: 0,
|
|
1028
|
-
loadingText: "",
|
|
1029
|
-
externalReady: null,
|
|
1030
|
-
externalStatus: "",
|
|
1031
|
-
externalError: "",
|
|
1032
|
-
suppressedEntryError: ""
|
|
1033
|
-
};
|
|
1034
|
-
while (Date.now() < deadline) {
|
|
1035
|
-
try {
|
|
1036
|
-
last = await bridge.send({ type: "evaluate", code: WAIT_READY_PROBE }) ?? last;
|
|
1037
|
-
bridgeError = "";
|
|
1038
|
-
} catch (error) {
|
|
1039
|
-
bridgeError = error instanceof Error ? error.message : String(error);
|
|
1040
|
-
if (bridgeError.startsWith("multiple sims are connected:") || bridgeError.startsWith("saved sim ")) {
|
|
1041
|
-
throw error;
|
|
1042
|
-
}
|
|
1043
|
-
}
|
|
1044
|
-
const now = Date.now();
|
|
1045
|
-
const status2 = {
|
|
1046
|
-
ready: false,
|
|
1047
|
-
elapsedMs: now - start,
|
|
1048
|
-
nodes: last.nodes,
|
|
1049
|
-
targets: last.targets,
|
|
1050
|
-
liveFrameActive: last.liveFrameActive,
|
|
1051
|
-
liveFrameChannels: last.liveFrameChannels,
|
|
1052
|
-
liveFramePublishes: last.liveFramePublishes,
|
|
1053
|
-
flag: last.flag,
|
|
1054
|
-
loadingText: last.loadingText,
|
|
1055
|
-
externalReady: last.externalReady,
|
|
1056
|
-
externalStatus: last.externalStatus,
|
|
1057
|
-
externalError: last.externalError,
|
|
1058
|
-
suppressedEntryError: last.suppressedEntryError,
|
|
1059
|
-
errors: last.errors,
|
|
1060
|
-
bridgeError
|
|
1061
|
-
};
|
|
1062
|
-
if (last.nodes !== lastNodes) {
|
|
1063
|
-
lastNodes = last.nodes;
|
|
1064
|
-
nodeStableSince = now;
|
|
1065
|
-
}
|
|
1066
|
-
const hasReadySignal = last.flag === true && readyProbeHasContent(last);
|
|
1067
|
-
const hasRenderedContentWithoutReadySignal = last.flag !== true && readyProbeHasTargetContent(last);
|
|
1068
|
-
if ((hasReadySignal || hasRenderedContentWithoutReadySignal) && last.externalReady !== false && !last.externalError && !last.loadingText) {
|
|
1069
|
-
if (now - nodeStableSince >= READY_NODE_STABLE_MS) {
|
|
1070
|
-
return {
|
|
1071
|
-
...status2,
|
|
1072
|
-
ready: true
|
|
1073
|
-
};
|
|
1074
|
-
}
|
|
1075
|
-
}
|
|
1076
|
-
if (last.externalError) return status2;
|
|
1077
|
-
if (options.onProgress && progressIntervalMs > 0 && now >= nextProgressAt) {
|
|
1078
|
-
options.onProgress(status2);
|
|
1079
|
-
do {
|
|
1080
|
-
nextProgressAt += progressIntervalMs;
|
|
1081
|
-
} while (now >= nextProgressAt);
|
|
1082
|
-
}
|
|
1083
|
-
await new Promise((r) => setTimeout(r, READY_PROBE_POLL_MS));
|
|
1084
|
-
}
|
|
1085
|
-
const status = {
|
|
1086
|
-
ready: false,
|
|
1087
|
-
elapsedMs: Date.now() - start,
|
|
1088
|
-
nodes: last.nodes,
|
|
1089
|
-
targets: last.targets,
|
|
1090
|
-
liveFrameActive: last.liveFrameActive,
|
|
1091
|
-
liveFrameChannels: last.liveFrameChannels,
|
|
1092
|
-
liveFramePublishes: last.liveFramePublishes,
|
|
1093
|
-
flag: last.flag,
|
|
1094
|
-
loadingText: last.loadingText,
|
|
1095
|
-
externalReady: last.externalReady,
|
|
1096
|
-
externalStatus: last.externalStatus,
|
|
1097
|
-
externalError: last.externalError,
|
|
1098
|
-
suppressedEntryError: last.suppressedEntryError,
|
|
1099
|
-
errors: last.errors,
|
|
1100
|
-
bridgeError
|
|
1101
|
-
};
|
|
1102
|
-
if (options.onProgress && progressIntervalMs > 0 && Date.now() >= nextProgressAt) {
|
|
1103
|
-
options.onProgress(status);
|
|
1104
|
-
}
|
|
1105
|
-
return status;
|
|
1106
|
-
}
|
|
1107
|
-
async function inspectWaitSelector(bridge, testId, timeoutMs = 5e3, opts = {}) {
|
|
1108
|
-
const gone = opts.gone === true;
|
|
1109
|
-
const result = await bridge.send(
|
|
1110
|
-
{
|
|
1111
|
-
type: "evaluate",
|
|
1112
|
-
code: `(async () => {
|
|
1113
|
-
const start = Date.now()
|
|
1114
|
-
const deadline = start + ${timeoutMs}
|
|
1115
|
-
const gone = ${gone}
|
|
1116
|
-
const ID = ${JSON.stringify(testId)}
|
|
1117
|
-
const present = (node) => !!(node && node.layout && node.layout.width > 0 && node.layout.height > 0)
|
|
1118
|
-
const find = async () => {
|
|
1119
|
-
const t = window.__sootsimTest
|
|
1120
|
-
if (!t) return undefined // bridge not ready \u2014 indeterminate, not "gone"
|
|
1121
|
-
try {
|
|
1122
|
-
// gone-mode confirms a gate/screen LEFT \u2014 it only needs structural
|
|
1123
|
-
// existence + layout, NOT the full-tree occlusion pass (pruneHidden).
|
|
1124
|
-
// that pass (computeOccludedInspectNodeIds) walks the whole tree and
|
|
1125
|
-
// does O(n\xB2)-ish rect-occlusion testing EVERY poll; on a large
|
|
1126
|
-
// post-login app tree it costs seconds per poll, so --gone can never
|
|
1127
|
-
// get two clean "absent" polls inside budget and falsely reports the
|
|
1128
|
-
// gate as still blocked \u2014 the root cause of the 3pc PR-preview outage.
|
|
1129
|
-
// presence-mode keeps pruneHidden so "appeared" still means
|
|
1130
|
-
// visibly-reachable. a departed gate is unmounted or zero-sized, so the
|
|
1131
|
-
// cheap structural+layout check is correct here and ~O(n) cheap.
|
|
1132
|
-
const matches = await t.queryAll?.(gone ? { hasId: ID } : { hasId: ID, pruneHidden: true })
|
|
1133
|
-
if (!Array.isArray(matches)) return undefined
|
|
1134
|
-
return matches.find(present) || null
|
|
1135
|
-
} catch {
|
|
1136
|
-
return undefined
|
|
1137
|
-
}
|
|
1138
|
-
}
|
|
1139
|
-
let goneStreak = 0
|
|
1140
|
-
while (Date.now() < deadline) {
|
|
1141
|
-
const node = await find()
|
|
1142
|
-
if (gone) {
|
|
1143
|
-
if (node === null) {
|
|
1144
|
-
goneStreak += 1
|
|
1145
|
-
if (goneStreak >= 2) return { found: true, elapsed: Date.now() - start }
|
|
562
|
+
candidates = candidates.filter((c) => {
|
|
563
|
+
const ax = c.info.absolutePosition && c.info.absolutePosition.x
|
|
564
|
+
const ay = c.info.absolutePosition && c.info.absolutePosition.y
|
|
565
|
+
if (F.minX !== null && !(ax >= F.minX)) return false
|
|
566
|
+
if (F.maxX !== null && !(ax <= F.maxX)) return false
|
|
567
|
+
if (F.minY !== null && !(ay >= F.minY)) return false
|
|
568
|
+
if (F.maxY !== null && !(ay <= F.maxY)) return false
|
|
569
|
+
if (F.within && !c.ancestorTestIDs.includes(F.within)) return false
|
|
570
|
+
return true
|
|
571
|
+
})
|
|
572
|
+
|
|
573
|
+
if (F.near) {
|
|
574
|
+
candidates.sort((a, b) => {
|
|
575
|
+
const ax = (a.info.absolutePosition && a.info.absolutePosition.x) || 0
|
|
576
|
+
const ay = (a.info.absolutePosition && a.info.absolutePosition.y) || 0
|
|
577
|
+
const bx = (b.info.absolutePosition && b.info.absolutePosition.x) || 0
|
|
578
|
+
const by = (b.info.absolutePosition && b.info.absolutePosition.y) || 0
|
|
579
|
+
return (
|
|
580
|
+
Math.hypot(ax - F.near.x, ay - F.near.y) -
|
|
581
|
+
Math.hypot(bx - F.near.x, by - F.near.y)
|
|
582
|
+
)
|
|
583
|
+
})
|
|
1146
584
|
} else {
|
|
1147
|
-
|
|
585
|
+
candidates.sort((a, b) => {
|
|
586
|
+
const ay = (a.info.absolutePosition && a.info.absolutePosition.y) || 0
|
|
587
|
+
const by = (b.info.absolutePosition && b.info.absolutePosition.y) || 0
|
|
588
|
+
if (Math.abs(ay - by) > 2) return ay - by
|
|
589
|
+
const ax = (a.info.absolutePosition && a.info.absolutePosition.x) || 0
|
|
590
|
+
const bx = (b.info.absolutePosition && b.info.absolutePosition.x) || 0
|
|
591
|
+
return ax - bx
|
|
592
|
+
})
|
|
1148
593
|
}
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
594
|
+
|
|
595
|
+
const total = candidates.length
|
|
596
|
+
if (total === 0) return { matched: 0, total: 0 }
|
|
597
|
+
|
|
598
|
+
let idx = 0
|
|
599
|
+
if (F.nth !== null) {
|
|
600
|
+
idx = F.nth < 0 ? total + F.nth : F.nth
|
|
601
|
+
if (idx < 0 || idx >= total) {
|
|
602
|
+
return { matched: 0, total, nthOutOfRange: true, nth: F.nth }
|
|
603
|
+
}
|
|
604
|
+
} else if (total > 1 && !F.first && !F.near) {
|
|
605
|
+
return {
|
|
606
|
+
ambiguous: true,
|
|
607
|
+
total,
|
|
608
|
+
candidates: candidates.slice(0, 10).map((c, i) => ({
|
|
609
|
+
idx: i,
|
|
610
|
+
nodeId: c.info.nodeId,
|
|
611
|
+
type: c.info.type,
|
|
612
|
+
testID: c.info.testID,
|
|
613
|
+
text: (c.info.text || '').slice(0, 60),
|
|
614
|
+
abs: c.info.absolutePosition,
|
|
615
|
+
layout: c.info.layout
|
|
616
|
+
? {
|
|
617
|
+
width: Math.round(c.info.layout.width || 0),
|
|
618
|
+
height: Math.round(c.info.layout.height || 0),
|
|
619
|
+
}
|
|
620
|
+
: null,
|
|
621
|
+
ancestorTestIDs: (c.ancestorTestIDs || []).slice(0, 5),
|
|
622
|
+
})),
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
const picked = candidates[idx]
|
|
627
|
+
const n = picked.info
|
|
628
|
+
if (!n.absolutePosition || !n.layout) return { matched: 0, total }
|
|
629
|
+
|
|
630
|
+
const resolved =
|
|
631
|
+
typeof n.nodeId === 'number' &&
|
|
632
|
+
typeof t.resolveTapTarget === 'function'
|
|
633
|
+
? await t.resolveTapTarget(n.nodeId)
|
|
634
|
+
: null
|
|
635
|
+
const target = (resolved && resolved.target) || n
|
|
636
|
+
const cx =
|
|
637
|
+
resolved && typeof resolved.cx === 'number'
|
|
638
|
+
? resolved.cx
|
|
639
|
+
: n.absolutePosition.x + (n.layout.width || 0) / 2
|
|
640
|
+
const cy =
|
|
641
|
+
resolved && typeof resolved.cy === 'number'
|
|
642
|
+
? resolved.cy
|
|
643
|
+
: n.absolutePosition.y + (n.layout.height || 0) / 2
|
|
644
|
+
const scr = ${SCREEN_EVAL}
|
|
645
|
+
const offscreen =
|
|
646
|
+
!!scr && (cx < 0 || cy < 0 || cx > scr.width || cy > scr.height)
|
|
647
|
+
return {
|
|
648
|
+
cx,
|
|
649
|
+
cy,
|
|
650
|
+
...(offscreen ? { offscreen: true, screen: scr } : {}),
|
|
651
|
+
match: {
|
|
652
|
+
nodeId: n.nodeId ?? null,
|
|
653
|
+
id: n.id,
|
|
654
|
+
testID: n.testID,
|
|
655
|
+
type: n.type,
|
|
656
|
+
},
|
|
657
|
+
target: {
|
|
658
|
+
nodeId: target.nodeId ?? null,
|
|
659
|
+
id: target.id,
|
|
660
|
+
testID: target.testID,
|
|
661
|
+
text:
|
|
662
|
+
target.text ??
|
|
663
|
+
target.accessibilityLabel ??
|
|
664
|
+
n.text ??
|
|
665
|
+
n.accessibilityLabel ??
|
|
666
|
+
null,
|
|
667
|
+
type: target.type,
|
|
668
|
+
},
|
|
669
|
+
strategy: (resolved && resolved.strategy) || 'matched-node',
|
|
670
|
+
total,
|
|
671
|
+
idx,
|
|
672
|
+
}
|
|
673
|
+
})()`
|
|
674
|
+
})
|
|
1200
675
|
});
|
|
1201
|
-
return Array.isArray(result) ? result : [];
|
|
1202
676
|
}
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
677
|
+
async function tapBest(bridge, query, opts = {}) {
|
|
678
|
+
const arg = JSON.stringify(query);
|
|
679
|
+
return tapResolvedTarget(bridge, {
|
|
680
|
+
agent: opts.agent,
|
|
681
|
+
timing: opts.timing,
|
|
682
|
+
textFallback: query,
|
|
683
|
+
resolve: async () => {
|
|
684
|
+
const payload = await bridge.send({
|
|
685
|
+
type: "evaluate",
|
|
686
|
+
code: `(async () => {
|
|
687
|
+
const t = window.__sootsimTest
|
|
688
|
+
if (!t) return { error: 'bridge-not-ready' }
|
|
689
|
+
const byTestId =
|
|
690
|
+
(await t.findByTestId(${arg})) || (await t.findById(${arg}))
|
|
691
|
+
if (byTestId && byTestId.absolutePosition && byTestId.layout) {
|
|
692
|
+
return {
|
|
693
|
+
strategy: 'testid',
|
|
694
|
+
node: {
|
|
695
|
+
nodeId: byTestId.nodeId ?? null,
|
|
696
|
+
id: byTestId.id,
|
|
697
|
+
testID: byTestId.testID,
|
|
698
|
+
type: byTestId.type,
|
|
699
|
+
text: byTestId.text,
|
|
700
|
+
absolutePosition: byTestId.absolutePosition,
|
|
701
|
+
layout: byTestId.layout,
|
|
702
|
+
},
|
|
703
|
+
screen: ${SCREEN_EVAL},
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
const byText = await t.findByText(${arg})
|
|
707
|
+
if (byText && byText.absolutePosition && byText.layout) {
|
|
708
|
+
return {
|
|
709
|
+
strategy: 'text',
|
|
710
|
+
node: {
|
|
711
|
+
nodeId: byText.nodeId ?? null,
|
|
712
|
+
id: byText.id,
|
|
713
|
+
testID: byText.testID,
|
|
714
|
+
type: byText.type,
|
|
715
|
+
text: byText.text,
|
|
716
|
+
absolutePosition: byText.absolutePosition,
|
|
717
|
+
layout: byText.layout,
|
|
718
|
+
},
|
|
719
|
+
screen: ${SCREEN_EVAL},
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
return { strategy: 'none' }
|
|
723
|
+
})()`
|
|
724
|
+
});
|
|
725
|
+
if (!payload || !("node" in payload)) return payload;
|
|
726
|
+
const node = payload.node;
|
|
727
|
+
const cx = node.absolutePosition.x + node.layout.width / 2;
|
|
728
|
+
const cy = node.absolutePosition.y + node.layout.height / 2;
|
|
729
|
+
const screen = payload.screen ?? null;
|
|
730
|
+
const offscreen = !!screen && (cx < 0 || cy < 0 || cx > screen.width || cy > screen.height);
|
|
731
|
+
return {
|
|
732
|
+
...payload,
|
|
733
|
+
cx,
|
|
734
|
+
cy,
|
|
735
|
+
...offscreen ? { offscreen: true, screen } : {},
|
|
736
|
+
target: {
|
|
737
|
+
id: node.id,
|
|
738
|
+
testID: node.testID,
|
|
739
|
+
text: payload.strategy === "text" ? query : node.text,
|
|
740
|
+
type: node.type
|
|
741
|
+
}
|
|
742
|
+
};
|
|
1216
743
|
}
|
|
1217
|
-
try {
|
|
1218
|
-
const c = window.__sootsimConsole
|
|
1219
|
-
for (const e of (c?.getErrors?.(200) || [])) add('error', e?.timestamp ?? e?.ts, e?.args)
|
|
1220
|
-
for (const e of (c?.getWarnings?.(200) || [])) add('warn', e?.timestamp ?? e?.ts, e?.args)
|
|
1221
|
-
} catch {}
|
|
1222
|
-
try {
|
|
1223
|
-
const obs = window.__sootsimObservability
|
|
1224
|
-
const snap = obs && obs.logs && typeof obs.logs.getSnapshot === 'function' ? obs.logs.getSnapshot() : []
|
|
1225
|
-
for (const e of snap) if (e) add(e.level, e.ts, e.args)
|
|
1226
|
-
} catch {}
|
|
1227
|
-
return { errors, warnings, total: errors + warnings }
|
|
1228
|
-
})()`;
|
|
1229
|
-
async function clearConsole(bridge) {
|
|
1230
|
-
await bridge.send({
|
|
1231
|
-
type: "evaluate",
|
|
1232
|
-
code: 'window.__sootsimConsole?.clear(); window.__sootsimObservability?.logs?.clear?.(); "cleared"'
|
|
1233
744
|
});
|
|
1234
745
|
}
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
const
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
args
|
|
1242
|
-
|
|
1243
|
-
|
|
746
|
+
|
|
747
|
+
// cli/commands/inspect/core.ts
|
|
748
|
+
function resolveMaxMsFlag(args, fallbackMs) {
|
|
749
|
+
const aliases = ["--max-ms", "--maxMs", "--maxms", "--max_ms"];
|
|
750
|
+
for (const flag of aliases) {
|
|
751
|
+
const i = args.indexOf(flag);
|
|
752
|
+
if (i >= 0 && args[i + 1]) {
|
|
753
|
+
const n = Number(args[i + 1]);
|
|
754
|
+
if (Number.isFinite(n)) return Math.max(100, n);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
return fallbackMs;
|
|
1244
758
|
}
|
|
1245
|
-
|
|
1246
|
-
|
|
759
|
+
var INSPECT_PICK_COMMANDS = /* @__PURE__ */ new Set([
|
|
760
|
+
"tap",
|
|
761
|
+
"double-tap",
|
|
762
|
+
"tap-text",
|
|
763
|
+
"tap-id",
|
|
764
|
+
"long-press",
|
|
765
|
+
"touch"
|
|
766
|
+
]);
|
|
767
|
+
function isInspectPickCommand(subcommand) {
|
|
768
|
+
return typeof subcommand === "string" && INSPECT_PICK_COMMANDS.has(subcommand);
|
|
1247
769
|
}
|
|
1248
|
-
async function
|
|
1249
|
-
const
|
|
1250
|
-
type: "
|
|
1251
|
-
|
|
1252
|
-
args: [x, y]
|
|
770
|
+
async function isInspectModeActive(bridge) {
|
|
771
|
+
const active = await bridge.send({
|
|
772
|
+
type: "evaluate",
|
|
773
|
+
code: "window.__sootsimEngineState?.inspectActive === true"
|
|
1253
774
|
});
|
|
1254
|
-
|
|
1255
|
-
const state = result;
|
|
1256
|
-
return {
|
|
1257
|
-
...state,
|
|
1258
|
-
scrollOffsetX: typeof state.scrollOffsetX === "number" ? state.scrollOffsetX : state.offsetX,
|
|
1259
|
-
scrollOffsetY: typeof state.scrollOffsetY === "number" ? state.scrollOffsetY : state.offsetY
|
|
1260
|
-
};
|
|
775
|
+
return active === true;
|
|
1261
776
|
}
|
|
1262
|
-
async function
|
|
1263
|
-
|
|
777
|
+
async function shouldSkipAutoSettleForInspectPick(bridge, subcommand) {
|
|
778
|
+
if (!isInspectPickCommand(subcommand)) return false;
|
|
779
|
+
try {
|
|
780
|
+
return await isInspectModeActive(bridge);
|
|
781
|
+
} catch {
|
|
782
|
+
return false;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
async function inspectNodeCount(bridge) {
|
|
786
|
+
const count = await bridge.send({
|
|
1264
787
|
type: "evaluate",
|
|
1265
|
-
code:
|
|
1266
|
-
const obs = window.__sootsimObservability;
|
|
1267
|
-
if (!obs) return { ok: false };
|
|
1268
|
-
return { ok: true, entries: obs.logs.getSnapshot() };
|
|
1269
|
-
})()`
|
|
788
|
+
code: "(async () => await window.__sootsimTest.getNodeCount())()"
|
|
1270
789
|
});
|
|
1271
|
-
|
|
1272
|
-
return res.entries ?? [];
|
|
790
|
+
return { nodes: typeof count === "number" ? count : 0 };
|
|
1273
791
|
}
|
|
1274
|
-
async function
|
|
1275
|
-
|
|
792
|
+
async function inspectTree(bridge, depth = 5, opts) {
|
|
793
|
+
const optsArg = opts?.format ? `, ${JSON.stringify(opts)}` : "";
|
|
794
|
+
const tree = await bridge.send({
|
|
1276
795
|
type: "evaluate",
|
|
1277
|
-
code:
|
|
796
|
+
code: `(async () => await window.__sootsimTest.dumpTree(${depth}${optsArg}))()`
|
|
1278
797
|
});
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
const sources = /* @__PURE__ */ new Set([a.source, b.source]);
|
|
1286
|
-
if (!sources.has("sootsim-worker")) return false;
|
|
1287
|
-
if (!sources.has("render-worker") && !sources.has("forwarded-render-worker")) {
|
|
1288
|
-
return false;
|
|
798
|
+
if (opts?.format === "boxes" && typeof tree === "string") {
|
|
799
|
+
try {
|
|
800
|
+
return { depth, tree: JSON.parse(tree) };
|
|
801
|
+
} catch {
|
|
802
|
+
return { depth, tree };
|
|
803
|
+
}
|
|
1289
804
|
}
|
|
1290
|
-
return
|
|
805
|
+
return { depth, tree };
|
|
1291
806
|
}
|
|
1292
|
-
function
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
807
|
+
async function inspectUrl(bridge) {
|
|
808
|
+
const url = await bridge.send({ type: "evaluate", code: "window.location.href" });
|
|
809
|
+
return { url: typeof url === "string" ? url : "" };
|
|
810
|
+
}
|
|
811
|
+
async function inspectDescribe(bridge, dumpOpts) {
|
|
812
|
+
const dumpDepth = dumpOpts.maxDepth ?? 12;
|
|
813
|
+
const code = `(async () => {
|
|
814
|
+
const t = window.__sootsimTest
|
|
815
|
+
const mainShell = window.SootSim?.bridges?.mainShell
|
|
816
|
+
const kb = window.__sootsimKeyboard
|
|
817
|
+
if (!t) return { error: 'no test bridge' }
|
|
818
|
+
|
|
819
|
+
let shell = null
|
|
820
|
+
try {
|
|
821
|
+
shell = typeof mainShell?.getState === 'function' ? await mainShell.getState() : null
|
|
822
|
+
} catch {}
|
|
823
|
+
|
|
824
|
+
const opts = ${JSON.stringify(dumpOpts)}
|
|
825
|
+
const needsRecursiveDump =
|
|
826
|
+
opts.verbose || opts.subtreeRoot || opts.includeOccluded || opts.maxDepth != null
|
|
827
|
+
const globToRegex = (glob) => {
|
|
828
|
+
const escaped = String(glob).replace(/[.+^$(){}|[\\]\\\\]/g, '\\\\$&')
|
|
829
|
+
const body = escaped.replace(/\\*/g, '.*').replace(/\\?/g, '.')
|
|
830
|
+
return new RegExp('^' + body + '$', 'i')
|
|
831
|
+
}
|
|
832
|
+
// clipping a describe line is a DISPLAY decision. opts.fullText (set by
|
|
833
|
+
// \`describe --json\`) keeps the real string, so a bundle-eval stack or a
|
|
834
|
+
// red-box message reaches the caller whole instead of ending in an
|
|
835
|
+
// ellipsis nothing downstream can undo.
|
|
836
|
+
const clipForDisplay = (value, max) => {
|
|
837
|
+
const text = typeof value === 'string' ? value.replace(/\\s+/g, ' ').trim() : ''
|
|
838
|
+
if (opts.fullText || text.length <= max) return text
|
|
839
|
+
return text.slice(0, max - 1).trimEnd() + '\u2026'
|
|
840
|
+
}
|
|
841
|
+
const formatInspectableTree = async () => {
|
|
842
|
+
if (typeof t.listInspectable !== 'function') return null
|
|
843
|
+
const list = await t.listInspectable({})
|
|
844
|
+
if (!Array.isArray(list)) return null
|
|
845
|
+
const filter = typeof opts.filter === 'string' ? opts.filter.toLowerCase() : ''
|
|
846
|
+
const testIdRegex = opts.testIdLike ? globToRegex(opts.testIdLike) : null
|
|
847
|
+
const onlyRegex = opts.onlyGlob ? globToRegex(opts.onlyGlob) : null
|
|
848
|
+
const lines = []
|
|
849
|
+
let matched = 0
|
|
850
|
+
for (const node of list) {
|
|
851
|
+
const testID = node?.testID || node?.testId || ''
|
|
852
|
+
const role = node?.role || ''
|
|
853
|
+
const label = node?.accessibilityLabel || node?.label || ''
|
|
854
|
+
const text = node?.text || ''
|
|
855
|
+
if (testIdRegex && !testIdRegex.test(testID)) continue
|
|
856
|
+
const joined = [role, label, text, testID].filter(Boolean).join(' ')
|
|
857
|
+
if (onlyRegex && !onlyRegex.test(joined)) continue
|
|
858
|
+
if (filter && !joined.toLowerCase().includes(filter)) continue
|
|
859
|
+
matched++
|
|
860
|
+
|
|
861
|
+
const type = String(node?.type || 'node').toLowerCase()
|
|
862
|
+
const handlers = Array.isArray(node?.handlers) ? node.handlers : []
|
|
863
|
+
const parts = []
|
|
864
|
+
if (role) parts.push('[' + role + ']')
|
|
865
|
+
else if (handlers.length > 0) parts.push('[tap]')
|
|
866
|
+
else parts.push('<' + type + '>')
|
|
867
|
+
const visibleText = clipForDisplay(text, 50)
|
|
868
|
+
if (visibleText) parts.push(JSON.stringify(visibleText))
|
|
869
|
+
if (testID) parts.push('#' + testID)
|
|
870
|
+
if (label && label !== text) {
|
|
871
|
+
parts.push('label=' + JSON.stringify(clipForDisplay(label, 50)))
|
|
872
|
+
}
|
|
873
|
+
if (!opts.hideXy) {
|
|
874
|
+
const abs = node?.absolute || {}
|
|
875
|
+
const layout = node?.layout || {}
|
|
876
|
+
const x = Math.round(Number(abs.x) || 0)
|
|
877
|
+
const y = Math.round(Number(abs.y) || 0)
|
|
878
|
+
const w = Math.round(Number(layout.width) || 0)
|
|
879
|
+
const h = Math.round(Number(layout.height) || 0)
|
|
880
|
+
parts.push('@(' + x + ',' + y + ') ' + w + 'x' + h)
|
|
881
|
+
}
|
|
882
|
+
if (handlers.length > 0) parts.push('(tap)')
|
|
883
|
+
lines.push(' ' + parts.join(' '))
|
|
884
|
+
}
|
|
885
|
+
return { tree: lines.join('\\n'), matched }
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
let tree = ''
|
|
889
|
+
const compactResult = needsRecursiveDump ? null : await formatInspectableTree()
|
|
890
|
+
if (compactResult) {
|
|
891
|
+
tree = compactResult.tree
|
|
892
|
+
} else {
|
|
893
|
+
tree = await t.dumpTree(${dumpDepth}, opts)
|
|
894
|
+
}
|
|
895
|
+
const nodeCount = (await t.getNodeCount?.()) || 0
|
|
896
|
+
const keyboard = kb && typeof kb.getLayout === 'function' ? kb.getLayout() : null
|
|
897
|
+
return { tree, shell, nodeCount, keyboard }
|
|
898
|
+
})()`;
|
|
899
|
+
const result = await bridge.send({ type: "evaluate", code });
|
|
900
|
+
return result ?? {};
|
|
901
|
+
}
|
|
902
|
+
function scoreLayoutTextContrast(input) {
|
|
903
|
+
const required = input.fontSize >= 18 || input.fontSize >= 14 && (input.fontWeight ?? 400) >= 700 ? 3 : 4.5;
|
|
904
|
+
const { normal, background } = input;
|
|
905
|
+
if (normal.width <= 0 || normal.height <= 0 || normal.width !== background.width || normal.height !== background.height) {
|
|
906
|
+
return { ratio: null, required, reason: "capture dimensions do not match" };
|
|
907
|
+
}
|
|
908
|
+
const left = Math.max(0, Math.floor(input.box.x));
|
|
909
|
+
const top = Math.max(0, Math.floor(input.box.y));
|
|
910
|
+
const right = Math.min(normal.width, Math.ceil(input.box.x + input.box.w));
|
|
911
|
+
const bottom = Math.min(normal.height, Math.ceil(input.box.y + input.box.h));
|
|
912
|
+
if (right <= left || bottom <= top) {
|
|
913
|
+
return { ratio: null, required, reason: "text is outside the captured frame" };
|
|
914
|
+
}
|
|
915
|
+
const linear = (value) => {
|
|
916
|
+
const channel = value / 255;
|
|
917
|
+
return channel <= 0.04045 ? channel / 12.92 : Math.pow((channel + 0.055) / 1.055, 2.4);
|
|
918
|
+
};
|
|
919
|
+
const ratioOf = (foreground, behind) => {
|
|
920
|
+
const foregroundLuminance = 0.2126 * linear(foreground[0]) + 0.7152 * linear(foreground[1]) + 0.0722 * linear(foreground[2]);
|
|
921
|
+
const backgroundLuminance = 0.2126 * linear(behind[0]) + 0.7152 * linear(behind[1]) + 0.0722 * linear(behind[2]);
|
|
922
|
+
return (Math.max(foregroundLuminance, backgroundLuminance) + 0.05) / (Math.min(foregroundLuminance, backgroundLuminance) + 0.05);
|
|
923
|
+
};
|
|
924
|
+
const hex = (color) => `#${color.slice(0, 3).map((value) => Math.round(value).toString(16).padStart(2, "0")).join("")}`;
|
|
925
|
+
const area = (right - left) * (bottom - top);
|
|
926
|
+
const step = Math.max(1, Math.floor(Math.sqrt(area / 12e3)));
|
|
927
|
+
const offsetAt = (x, y) => (y * normal.width + x) * 4;
|
|
928
|
+
const deltaAt = (offset) => Math.max(
|
|
929
|
+
Math.abs(normal.data[offset] - background.data[offset]),
|
|
930
|
+
Math.abs(normal.data[offset + 1] - background.data[offset + 1]),
|
|
931
|
+
Math.abs(normal.data[offset + 2] - background.data[offset + 2])
|
|
932
|
+
);
|
|
933
|
+
let strongestDelta = 0;
|
|
934
|
+
let strongestForeground = null;
|
|
935
|
+
let opaqueBackground = null;
|
|
936
|
+
for (let y = top; y < bottom; y += step) {
|
|
937
|
+
for (let x = left; x < right; x += step) {
|
|
938
|
+
const offset = offsetAt(x, y);
|
|
939
|
+
if (background.data[offset + 3] < 250) continue;
|
|
940
|
+
const behind = [
|
|
941
|
+
background.data[offset],
|
|
942
|
+
background.data[offset + 1],
|
|
943
|
+
background.data[offset + 2]
|
|
944
|
+
];
|
|
945
|
+
opaqueBackground ??= behind;
|
|
946
|
+
const delta = deltaAt(offset);
|
|
947
|
+
if (delta <= strongestDelta) continue;
|
|
948
|
+
strongestDelta = delta;
|
|
949
|
+
strongestForeground = [
|
|
950
|
+
normal.data[offset],
|
|
951
|
+
normal.data[offset + 1],
|
|
952
|
+
normal.data[offset + 2]
|
|
953
|
+
];
|
|
1297
954
|
}
|
|
1298
|
-
out.push(entry);
|
|
1299
955
|
}
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
956
|
+
let bestRatio = Infinity;
|
|
957
|
+
let bestForeground = null;
|
|
958
|
+
let bestBackground = null;
|
|
959
|
+
const consider = (foreground, behind) => {
|
|
960
|
+
const ratio = ratioOf(foreground, behind);
|
|
961
|
+
if (ratio >= bestRatio) return;
|
|
962
|
+
bestRatio = ratio;
|
|
963
|
+
bestForeground = foreground;
|
|
964
|
+
bestBackground = behind;
|
|
965
|
+
};
|
|
966
|
+
if (strongestDelta >= 2 && strongestForeground) {
|
|
967
|
+
for (let y = top; y < bottom; y += step) {
|
|
968
|
+
for (let x = left; x < right; x += step) {
|
|
969
|
+
const offset = offsetAt(x, y);
|
|
970
|
+
if (background.data[offset + 3] < 250 || deltaAt(offset) < 2) {
|
|
971
|
+
continue;
|
|
972
|
+
}
|
|
973
|
+
consider(strongestForeground, [
|
|
974
|
+
background.data[offset],
|
|
975
|
+
background.data[offset + 1],
|
|
976
|
+
background.data[offset + 2]
|
|
977
|
+
]);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
} else if (opaqueBackground) {
|
|
981
|
+
consider(opaqueBackground, opaqueBackground);
|
|
1305
982
|
}
|
|
1306
|
-
if (
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
983
|
+
if (!bestForeground || !bestBackground || !Number.isFinite(bestRatio)) {
|
|
984
|
+
return {
|
|
985
|
+
ratio: null,
|
|
986
|
+
required,
|
|
987
|
+
reason: "could not isolate opaque rendered text pixels"
|
|
988
|
+
};
|
|
1310
989
|
}
|
|
1311
|
-
return
|
|
990
|
+
return {
|
|
991
|
+
ratio: Math.round(bestRatio * 100) / 100,
|
|
992
|
+
required,
|
|
993
|
+
foreground: hex(bestForeground),
|
|
994
|
+
background: hex(bestBackground)
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
function layoutGetNativeEval(opts = {}) {
|
|
998
|
+
const includeStyle = opts.styling === true;
|
|
999
|
+
const filter = opts.filter?.trim().toLowerCase() ?? "";
|
|
1000
|
+
return `(async () => {
|
|
1001
|
+
const t = window.__sootsimTest
|
|
1002
|
+
if (!t || typeof t.listInspectable !== 'function') return []
|
|
1003
|
+
const list = await t.listInspectable({})
|
|
1004
|
+
if (!Array.isArray(list)) return []
|
|
1005
|
+
const includeStyle = ${JSON.stringify(includeStyle)}
|
|
1006
|
+
const scoreContrast = ${scoreLayoutTextContrast.toString()}
|
|
1007
|
+
// narrows by ELEMENT, not by style field: fewer rows, full detail on each.
|
|
1008
|
+
const filter = ${JSON.stringify(filter)}
|
|
1009
|
+
const matchesFilter = (n, text) => {
|
|
1010
|
+
if (!filter) return true
|
|
1011
|
+
const fields = [text, n.testID || n.testId, n.accessibilityRole || n.role, n.type]
|
|
1012
|
+
return fields.some((f) => typeof f === 'string' && f.toLowerCase().includes(filter))
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : 0)
|
|
1016
|
+
const round = (v) => Math.round(num(v))
|
|
1017
|
+
|
|
1018
|
+
// borderRadius can be authored as a string ("12px") or split per-corner;
|
|
1019
|
+
// prefer the uniform borderRadius, else the largest corner radius present.
|
|
1020
|
+
const radiusOf = (style) => {
|
|
1021
|
+
if (!style) return 0
|
|
1022
|
+
const keys = ['borderRadius','borderTopLeftRadius','borderTopRightRadius','borderBottomLeftRadius','borderBottomRightRadius']
|
|
1023
|
+
let max = 0
|
|
1024
|
+
for (const k of keys) {
|
|
1025
|
+
const raw = style[k]
|
|
1026
|
+
const n = typeof raw === 'number' ? raw : (typeof raw === 'string' ? parseFloat(raw) : NaN)
|
|
1027
|
+
if (Number.isFinite(n)) max = Math.max(max, n)
|
|
1028
|
+
}
|
|
1029
|
+
return Math.round(max)
|
|
1030
|
+
}
|
|
1031
|
+
const fontSizeOf = (style) => {
|
|
1032
|
+
if (!style) return 0
|
|
1033
|
+
const raw = style.fontSize
|
|
1034
|
+
const n = typeof raw === 'number' ? raw : (typeof raw === 'string' ? parseFloat(raw) : NaN)
|
|
1035
|
+
return Number.isFinite(n) ? Math.round(n) : 0
|
|
1036
|
+
}
|
|
1037
|
+
// styling extractors \u2014 omit boring defaults so the agent only sees notable
|
|
1038
|
+
// styling (low opacity, struck text, clipping, a real border/color).
|
|
1039
|
+
const colorStr = (v) => {
|
|
1040
|
+
if (v == null) return undefined
|
|
1041
|
+
const s = String(v).split(' ').join('')
|
|
1042
|
+
return s && s !== 'transparent' && s !== 'rgba(0,0,0,0)' ? s : undefined
|
|
1043
|
+
}
|
|
1044
|
+
const opacityOf = (style) => {
|
|
1045
|
+
const o = style && style.opacity
|
|
1046
|
+
const n = typeof o === 'number' ? o : (typeof o === 'string' ? parseFloat(o) : NaN)
|
|
1047
|
+
return Number.isFinite(n) && n < 1 ? Math.round(n * 100) / 100 : undefined
|
|
1048
|
+
}
|
|
1049
|
+
const weightOf = (style) => {
|
|
1050
|
+
const w = style && style.fontWeight
|
|
1051
|
+
const n = w === 'bold' ? 700 : (typeof w === 'number' ? w : (typeof w === 'string' ? parseInt(w, 10) : NaN))
|
|
1052
|
+
return Number.isFinite(n) && n >= 600 ? n : undefined
|
|
1053
|
+
}
|
|
1054
|
+
const decoOf = (style) => {
|
|
1055
|
+
const d = style && (style.textDecorationLine || style.textDecoration)
|
|
1056
|
+
return d && d !== 'none' && d !== 'normal' ? String(d).split(' ')[0] : undefined
|
|
1057
|
+
}
|
|
1058
|
+
const overflowOf = (style) => (style && style.overflow === 'hidden' ? 'hidden' : undefined)
|
|
1059
|
+
const borderWOf = (style) => {
|
|
1060
|
+
if (!style) return undefined
|
|
1061
|
+
const keys = ['borderWidth','borderTopWidth','borderBottomWidth','borderLeftWidth','borderRightWidth']
|
|
1062
|
+
let max = 0
|
|
1063
|
+
for (const k of keys) {
|
|
1064
|
+
const r = style[k]
|
|
1065
|
+
const n = typeof r === 'number' ? r : (typeof r === 'string' ? parseFloat(r) : NaN)
|
|
1066
|
+
if (Number.isFinite(n)) max = Math.max(max, n)
|
|
1067
|
+
}
|
|
1068
|
+
return max > 0 ? Math.round(max) : undefined
|
|
1069
|
+
}
|
|
1070
|
+
// only meaningful alongside a real border width, so it is resolved by the
|
|
1071
|
+
// caller below rather than on its own.
|
|
1072
|
+
const borderColorOf = (style) => {
|
|
1073
|
+
if (!style) return undefined
|
|
1074
|
+
const keys = ['borderColor','borderTopColor','borderBottomColor','borderLeftColor','borderRightColor']
|
|
1075
|
+
for (const k of keys) {
|
|
1076
|
+
const c = colorStr(style[k])
|
|
1077
|
+
if (c) return c
|
|
1078
|
+
}
|
|
1079
|
+
return undefined
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
const isVisible = (n) => {
|
|
1083
|
+
const layout = n && n.layout
|
|
1084
|
+
if (!layout || layout.width <= 0 || layout.height <= 0) return false
|
|
1085
|
+
const abs = (n && (n.absolute || n.absolutePosition)) || null
|
|
1086
|
+
if (!abs) return true
|
|
1087
|
+
const device = (n && n.device) || {}
|
|
1088
|
+
const screenW = Number(device.width) || window.innerWidth || 0
|
|
1089
|
+
const screenH = Number(device.height) || window.innerHeight || 0
|
|
1090
|
+
if (!screenW || !screenH) return true
|
|
1091
|
+
return abs.x + layout.width > 0 && abs.y + layout.height > 0 && abs.x < screenW && abs.y < screenH
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
const elements = list.filter(isVisible).filter((n) => {
|
|
1095
|
+
const t = typeof n.text === 'string' && n.text.trim() ? n.text.trim() : undefined
|
|
1096
|
+
return matchesFilter(n, t)
|
|
1097
|
+
}).map((n) => {
|
|
1098
|
+
const layout = n.layout || { x: 0, y: 0, width: 0, height: 0 }
|
|
1099
|
+
const abs = n.absolute || n.absolutePosition || { x: layout.x, y: layout.y }
|
|
1100
|
+
const text = typeof n.text === 'string' && n.text.trim() ? n.text.trim() : undefined
|
|
1101
|
+
const base = {
|
|
1102
|
+
...(typeof n.nodeId === 'number'
|
|
1103
|
+
? {
|
|
1104
|
+
nodeId: n.nodeId,
|
|
1105
|
+
ancestorIds: Array.isArray(n.ancestors)
|
|
1106
|
+
? n.ancestors.map((ancestor) => ancestor && ancestor.nodeId).filter((id) => typeof id === 'number')
|
|
1107
|
+
: [],
|
|
1108
|
+
}
|
|
1109
|
+
: {}),
|
|
1110
|
+
testID: n.testID || n.testId || undefined,
|
|
1111
|
+
role: n.accessibilityRole || n.role || undefined,
|
|
1112
|
+
type: typeof n.type === 'string' ? n.type : 'node',
|
|
1113
|
+
text,
|
|
1114
|
+
box: { x: round(abs.x), y: round(abs.y), w: round(layout.width), h: round(layout.height) },
|
|
1115
|
+
borderRadius: 0,
|
|
1116
|
+
padding: { t: 0, r: 0, b: 0, l: 0 },
|
|
1117
|
+
fontSize: 0,
|
|
1118
|
+
}
|
|
1119
|
+
if (!includeStyle) return base
|
|
1120
|
+
// computedStyle has the resolved values; style is the curated subset \u2014
|
|
1121
|
+
// fall back across both so radius/fontSize survive either shape.
|
|
1122
|
+
const style = Object.assign({}, n.style || {}, n.computedStyle || {})
|
|
1123
|
+
const pad = (n.boxModel && n.boxModel.padding) || {}
|
|
1124
|
+
const borderWidth = borderWOf(style)
|
|
1125
|
+
return {
|
|
1126
|
+
...base,
|
|
1127
|
+
borderRadius: radiusOf(style),
|
|
1128
|
+
padding: { t: round(pad.top), r: round(pad.right), b: round(pad.bottom), l: round(pad.left) },
|
|
1129
|
+
fontSize: text ? (fontSizeOf(style) || 14) : 0,
|
|
1130
|
+
color: text ? colorStr(style.color) : undefined,
|
|
1131
|
+
bg: colorStr(style.backgroundColor),
|
|
1132
|
+
opacity: opacityOf(style),
|
|
1133
|
+
fontWeight: weightOf(style),
|
|
1134
|
+
textDecoration: decoOf(style),
|
|
1135
|
+
overflow: overflowOf(style),
|
|
1136
|
+
borderWidth,
|
|
1137
|
+
borderColor: borderWidth ? borderColorOf(style) : undefined,
|
|
1138
|
+
}
|
|
1139
|
+
})
|
|
1140
|
+
const textElements = elements.filter((element) => element.text && element.fontSize > 0)
|
|
1141
|
+
if (!includeStyle || textElements.length === 0) return elements
|
|
1142
|
+
|
|
1143
|
+
const screenshot = window.SootSim && window.SootSim.bridges && window.SootSim.bridges.screenshot
|
|
1144
|
+
if (typeof screenshot !== 'function') {
|
|
1145
|
+
throw new Error('styled native layout requires the rnx capture bridge')
|
|
1146
|
+
}
|
|
1147
|
+
const device = list.find((node) => node && node.device)?.device || {}
|
|
1148
|
+
const width = Math.round(Number(device.width) || window.innerWidth || 0)
|
|
1149
|
+
const height = Math.round(Number(device.height) || window.innerHeight || 0)
|
|
1150
|
+
if (!width || !height) throw new Error('native layout has no capture dimensions')
|
|
1151
|
+
const capture = async (suppressText) => {
|
|
1152
|
+
const dataUrl = await screenshot({
|
|
1153
|
+
format: 'png',
|
|
1154
|
+
outputWidth: width,
|
|
1155
|
+
outputHeight: height,
|
|
1156
|
+
...(suppressText ? { suppressText: true } : {}),
|
|
1157
|
+
})
|
|
1158
|
+
const image = await new Promise((resolve, reject) => {
|
|
1159
|
+
const next = new Image()
|
|
1160
|
+
next.onload = () => resolve(next)
|
|
1161
|
+
next.onerror = () => reject(new Error('could not decode native layout pixels'))
|
|
1162
|
+
next.src = dataUrl
|
|
1163
|
+
})
|
|
1164
|
+
const canvas = document.createElement('canvas')
|
|
1165
|
+
canvas.width = image.naturalWidth
|
|
1166
|
+
canvas.height = image.naturalHeight
|
|
1167
|
+
const context = canvas.getContext('2d', { willReadFrequently: true })
|
|
1168
|
+
if (!context) throw new Error('could not read native layout pixels')
|
|
1169
|
+
context.drawImage(image, 0, 0)
|
|
1170
|
+
return context.getImageData(0, 0, canvas.width, canvas.height)
|
|
1171
|
+
}
|
|
1172
|
+
const normal = await capture(false)
|
|
1173
|
+
const background = await capture(true)
|
|
1174
|
+
for (const element of textElements) {
|
|
1175
|
+
element.contrast = scoreContrast({
|
|
1176
|
+
normal,
|
|
1177
|
+
background,
|
|
1178
|
+
box: element.box,
|
|
1179
|
+
fontSize: element.fontSize,
|
|
1180
|
+
fontWeight: element.fontWeight,
|
|
1181
|
+
})
|
|
1182
|
+
}
|
|
1183
|
+
return elements
|
|
1184
|
+
})()`;
|
|
1312
1185
|
}
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1186
|
+
var LAYOUT_GET_NATIVE_EVAL = layoutGetNativeEval();
|
|
1187
|
+
async function inspectGetLayout(bridge, opts = {}) {
|
|
1188
|
+
const raw = await bridge.send({
|
|
1189
|
+
type: "evaluate",
|
|
1190
|
+
code: layoutGetNativeEval(opts)
|
|
1318
1191
|
});
|
|
1192
|
+
return Array.isArray(raw) ? raw : [];
|
|
1319
1193
|
}
|
|
1320
|
-
|
|
1321
|
-
return
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1194
|
+
function formatLayoutElements(elements, opts) {
|
|
1195
|
+
if (elements.length === 0) return " no visible elements found";
|
|
1196
|
+
const lines = elements.map((el) => {
|
|
1197
|
+
const ident = el.testID ? `#${el.testID}` : el.selector ? el.selector : el.role ? `[${el.role}]` : `<${el.type}>`;
|
|
1198
|
+
const parts = [ident, `@(${el.box.x},${el.box.y})`, `${el.box.w}x${el.box.h}`];
|
|
1199
|
+
if (el.borderRadius > 0) parts.push(`radius:${el.borderRadius}`);
|
|
1200
|
+
const p = el.padding;
|
|
1201
|
+
if (p.t || p.r || p.b || p.l) {
|
|
1202
|
+
const uniform = p.t === p.r && p.r === p.b && p.b === p.l;
|
|
1203
|
+
parts.push(uniform ? `pad:${p.t}` : `pad:${p.t},${p.r},${p.b},${p.l}`);
|
|
1204
|
+
}
|
|
1205
|
+
if (el.fontSize > 0) parts.push(`font:${el.fontSize}`);
|
|
1206
|
+
if (opts?.styling) {
|
|
1207
|
+
if (el.fontWeight) parts.push(`weight:${el.fontWeight}`);
|
|
1208
|
+
if (el.color) parts.push(`color:${el.color}`);
|
|
1209
|
+
if (el.bg) parts.push(`bg:${el.bg}`);
|
|
1210
|
+
if (el.contrast) {
|
|
1211
|
+
const score = el.contrast.ratio == null ? `?/${el.contrast.required}` : `${el.contrast.ratio}/${el.contrast.required}${el.contrast.ratio < el.contrast.required ? " FAIL" : ""}`;
|
|
1212
|
+
parts.push(`contrast:${score}`);
|
|
1213
|
+
if (el.contrast.background) parts.push(`on:${el.contrast.background}`);
|
|
1214
|
+
}
|
|
1215
|
+
if (el.opacity != null) parts.push(`opacity:${el.opacity}`);
|
|
1216
|
+
if (el.textDecoration) parts.push(`deco:${el.textDecoration}`);
|
|
1217
|
+
if (el.overflow) parts.push(`overflow:${el.overflow}`);
|
|
1218
|
+
if (el.borderWidth) {
|
|
1219
|
+
parts.push(
|
|
1220
|
+
el.borderColor ? `border:${el.borderWidth}/${el.borderColor}` : `border:${el.borderWidth}`
|
|
1221
|
+
);
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
if (el.text) {
|
|
1225
|
+
const text = el.text.length > 40 ? `${el.text.slice(0, 39)}\u2026` : el.text;
|
|
1226
|
+
parts.push(`"${text}"`);
|
|
1227
|
+
}
|
|
1228
|
+
return ` ${parts.join(" ")}`;
|
|
1325
1229
|
});
|
|
1230
|
+
return lines.join("\n");
|
|
1326
1231
|
}
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1232
|
+
var INSPECT_A11Y_CODE = `(async () => {
|
|
1233
|
+
const t = window.__sootsimTest
|
|
1234
|
+
if (!t) return []
|
|
1235
|
+
|
|
1236
|
+
const readLayout = (n) => n?.layout || null
|
|
1237
|
+
const readAbs = (n) => n?.absolutePosition || n?.absolute || null
|
|
1238
|
+
const readRole = (n) => n?.accessibilityRole || n?.role || null
|
|
1239
|
+
const readLabel = (n) => n?.accessibilityLabel || n?.label || n?.text || null
|
|
1240
|
+
const readHint = (n) => n?.accessibilityHint || n?.hint || null
|
|
1241
|
+
const readState = (n) => n?.accessibilityState || n?.state || null
|
|
1242
|
+
const readTestId = (n) => n?.testID || n?.testId || null
|
|
1243
|
+
const readType = (n) => typeof n?.type === 'string' ? n.type.toLowerCase() : n?.type
|
|
1244
|
+
const readPressable = (n) => {
|
|
1245
|
+
if (n?.pressable) return true
|
|
1246
|
+
const handlers = Array.isArray(n?.handlers) ? n.handlers : []
|
|
1247
|
+
const role = readRole(n)
|
|
1248
|
+
return handlers.length > 0 || role === 'button' || role === 'link' || role === 'tab'
|
|
1249
|
+
}
|
|
1250
|
+
const isTextInput = (n) => n?.isTextInput === true
|
|
1251
|
+
const isTextNode = (n) => readType(n) === 'text'
|
|
1252
|
+
const isVisibleTarget = (n) => {
|
|
1253
|
+
const layout = readLayout(n)
|
|
1254
|
+
if (!layout || layout.width <= 0 || layout.height <= 0) return false
|
|
1255
|
+
const abs = readAbs(n)
|
|
1256
|
+
if (!abs) return true
|
|
1257
|
+
const device = n?.device || {}
|
|
1258
|
+
const screenW = Number(device.width) || window.innerWidth || 0
|
|
1259
|
+
const screenH = Number(device.height) || window.innerHeight || 0
|
|
1260
|
+
return (
|
|
1261
|
+
abs.x + layout.width > 0 &&
|
|
1262
|
+
abs.y + layout.height > 0 &&
|
|
1263
|
+
abs.x < screenW &&
|
|
1264
|
+
abs.y < screenH
|
|
1265
|
+
)
|
|
1266
|
+
}
|
|
1267
|
+
const hasAccessibleSignal = (n) => {
|
|
1268
|
+
const role = readRole(n)
|
|
1269
|
+
const label = readLabel(n)
|
|
1270
|
+
const hint = readHint(n)
|
|
1271
|
+
if (role) return true
|
|
1272
|
+
if (hint) return true
|
|
1273
|
+
if (isTextInput(n)) return true
|
|
1274
|
+
if (readPressable(n)) return true
|
|
1275
|
+
if (isTextNode(n) && n?.text) return true
|
|
1276
|
+
if (typeof label === 'string' && label.length <= 30 && label !== n?.text) return true
|
|
1277
|
+
return false
|
|
1278
|
+
}
|
|
1279
|
+
const normalize = (n) => {
|
|
1280
|
+
const layout = readLayout(n)
|
|
1281
|
+
const abs = readAbs(n)
|
|
1282
|
+
const role = readRole(n) || (readPressable(n) ? 'button' : isTextInput(n) ? 'textfield' : isTextNode(n) ? 'statictext' : 'none')
|
|
1283
|
+
return {
|
|
1284
|
+
role,
|
|
1285
|
+
label: readLabel(n),
|
|
1286
|
+
hint: readHint(n),
|
|
1287
|
+
state: readState(n),
|
|
1288
|
+
testID: readTestId(n),
|
|
1289
|
+
position: abs ? { x: Math.round(abs.x), y: Math.round(abs.y) } : null,
|
|
1290
|
+
size: layout ? { w: Math.round(layout.width), h: Math.round(layout.height) } : null,
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
if (typeof t.listInspectable === 'function') {
|
|
1295
|
+
const list = await t.listInspectable({})
|
|
1296
|
+
if (Array.isArray(list)) {
|
|
1297
|
+
return list.filter(n => isVisibleTarget(n) && hasAccessibleSignal(n)).map(normalize)
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
const all = await t.queryAll({ pruneHidden: true })
|
|
1302
|
+
return all
|
|
1303
|
+
.filter(n => isVisibleTarget(n) && hasAccessibleSignal(n))
|
|
1304
|
+
.map(normalize)
|
|
1305
|
+
})()`;
|
|
1306
|
+
async function inspectAccessibilityTree(bridge) {
|
|
1307
|
+
const nodes = await bridge.send({ type: "evaluate", code: INSPECT_A11Y_CODE });
|
|
1308
|
+
return Array.isArray(nodes) ? nodes : [];
|
|
1358
1309
|
}
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1310
|
+
var FIND_INSPECTABLE_TARGETS = `
|
|
1311
|
+
const fromInspectable = async () => {
|
|
1312
|
+
if (typeof t.listInspectable !== 'function') return null
|
|
1313
|
+
const list = await t.listInspectable({})
|
|
1314
|
+
if (!Array.isArray(list)) return null
|
|
1315
|
+
return list.map((n) => {
|
|
1316
|
+
const role = n.accessibilityRole ?? n.role
|
|
1317
|
+
const label = n.accessibilityLabel ?? n.label
|
|
1318
|
+
const layout = n.layout
|
|
1319
|
+
const absolutePosition = n.absolutePosition ?? n.absolute
|
|
1320
|
+
const screenW = Number(n.device?.width) || window.innerWidth || 0
|
|
1321
|
+
const screenH = Number(n.device?.height) || window.innerHeight || 0
|
|
1322
|
+
const visibleFrame = layout && absolutePosition
|
|
1323
|
+
? {
|
|
1324
|
+
x: Math.max(0, absolutePosition.x),
|
|
1325
|
+
y: Math.max(0, absolutePosition.y),
|
|
1326
|
+
width: Math.max(
|
|
1327
|
+
0,
|
|
1328
|
+
Math.min(screenW, absolutePosition.x + layout.width) -
|
|
1329
|
+
Math.max(0, absolutePosition.x),
|
|
1330
|
+
),
|
|
1331
|
+
height: Math.max(
|
|
1332
|
+
0,
|
|
1333
|
+
Math.min(screenH, absolutePosition.y + layout.height) -
|
|
1334
|
+
Math.max(0, absolutePosition.y),
|
|
1335
|
+
),
|
|
1336
|
+
}
|
|
1337
|
+
: null
|
|
1338
|
+
const handlers = Array.isArray(n.handlers) ? n.handlers : []
|
|
1339
|
+
const pressable =
|
|
1340
|
+
handlers.length > 0 ||
|
|
1341
|
+
role === 'button' ||
|
|
1342
|
+
role === 'link' ||
|
|
1343
|
+
role === 'tab'
|
|
1344
|
+
return {
|
|
1345
|
+
type: typeof n.type === 'string' ? n.type.toLowerCase() : n.type,
|
|
1346
|
+
id: n.id ?? n.nodeId,
|
|
1347
|
+
nodeId: n.nodeId,
|
|
1348
|
+
testID: n.testID ?? n.testId,
|
|
1349
|
+
text: n.text,
|
|
1350
|
+
layout,
|
|
1351
|
+
absolutePosition,
|
|
1352
|
+
visibleFrame,
|
|
1353
|
+
style: n.computedStyle ?? n.style ?? {},
|
|
1354
|
+
childCount: n.childCount ?? 0,
|
|
1355
|
+
pressed: n.pressed ?? false,
|
|
1356
|
+
pressable,
|
|
1357
|
+
isTextInput: n.isTextInput ?? false,
|
|
1358
|
+
accessible: n.accessible ?? true,
|
|
1359
|
+
accessibilityLabel: label,
|
|
1360
|
+
accessibilityRole: role,
|
|
1361
|
+
accessibilityHint: n.accessibilityHint,
|
|
1362
|
+
accessibilityState: n.accessibilityState,
|
|
1363
|
+
accessibilityValue: n.accessibilityValue,
|
|
1364
|
+
}
|
|
1365
|
+
})
|
|
1366
1366
|
}
|
|
1367
|
-
|
|
1367
|
+
const isVisibleTarget = (n) => {
|
|
1368
|
+
if (!n?.layout) return false
|
|
1369
|
+
const frame = n.visibleFrame
|
|
1370
|
+
if (frame) return frame.width > 0 && frame.height > 0
|
|
1371
|
+
const abs = n.absolutePosition
|
|
1372
|
+
if (!abs) return n.layout.width > 0 && n.layout.height > 0
|
|
1373
|
+
const screenW = window.innerWidth || 0
|
|
1374
|
+
const screenH = window.innerHeight || 0
|
|
1375
|
+
return (
|
|
1376
|
+
abs.x + n.layout.width > 0 &&
|
|
1377
|
+
abs.y + n.layout.height > 0 &&
|
|
1378
|
+
abs.x < screenW &&
|
|
1379
|
+
abs.y < screenH
|
|
1380
|
+
)
|
|
1381
|
+
}
|
|
1382
|
+
`;
|
|
1383
|
+
function resolveFindMode(q) {
|
|
1384
|
+
if (q.testId) {
|
|
1385
|
+
return {
|
|
1386
|
+
mode: "testid",
|
|
1387
|
+
code: `(async () => {
|
|
1388
|
+
const t = window.__sootsimTest
|
|
1389
|
+
if (!t) return null
|
|
1390
|
+
return (await t.findByTestId(${JSON.stringify(q.testId)})) || (await t.findById(${JSON.stringify(q.testId)}))
|
|
1368
1391
|
})()`
|
|
1369
|
-
|
|
1370
|
-
return result ?? { error: "keyboard bridge returned no result" };
|
|
1371
|
-
}
|
|
1372
|
-
function isShellCommandUnavailable(error) {
|
|
1373
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1374
|
-
return message.includes("call target not found: SootSim.bridges.mainShell") || message.includes("test bridge unavailable before app-in-worker boot");
|
|
1375
|
-
}
|
|
1376
|
-
async function getShellState(bridge, readyTimeoutMs = 0) {
|
|
1377
|
-
const deadline = Date.now() + Math.max(0, readyTimeoutMs);
|
|
1378
|
-
while (true) {
|
|
1379
|
-
try {
|
|
1380
|
-
return await bridge.send({
|
|
1381
|
-
type: "call",
|
|
1382
|
-
path: "SootSim.bridges.mainShell.getState",
|
|
1383
|
-
args: []
|
|
1384
|
-
});
|
|
1385
|
-
} catch (error) {
|
|
1386
|
-
if (!isShellCommandUnavailable(error) || Date.now() >= deadline) throw error;
|
|
1387
|
-
await new Promise((r) => setTimeout(r, 50));
|
|
1388
|
-
}
|
|
1392
|
+
};
|
|
1389
1393
|
}
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
const navSnap =
|
|
1398
|
-
test && typeof test.getNavigationSnapshot === 'function'
|
|
1399
|
-
? await test.getNavigationSnapshot()
|
|
1400
|
-
: null
|
|
1401
|
-
const route =
|
|
1402
|
-
test && typeof test.getRouteInfo === 'function'
|
|
1403
|
-
? await test.getRouteInfo()
|
|
1404
|
-
: null
|
|
1405
|
-
const keyboard =
|
|
1406
|
-
kb && typeof kb.getLayout === 'function'
|
|
1407
|
-
? (() => {
|
|
1408
|
-
const layout = kb.getLayout()
|
|
1409
|
-
return layout ? {
|
|
1410
|
-
visible: layout.visible,
|
|
1411
|
-
mode: layout.mode,
|
|
1412
|
-
spec: layout.spec
|
|
1413
|
-
? {
|
|
1414
|
-
keyboardType: layout.spec.keyboardType,
|
|
1415
|
-
returnKeyType: layout.spec.returnKeyType,
|
|
1416
|
-
}
|
|
1417
|
-
: null,
|
|
1418
|
-
} : null
|
|
1419
|
-
})()
|
|
1420
|
-
: null
|
|
1421
|
-
return { nav: navSnap, route, keyboard }
|
|
1394
|
+
if (q.role) {
|
|
1395
|
+
return {
|
|
1396
|
+
mode: "role",
|
|
1397
|
+
code: `(async () => {
|
|
1398
|
+
const t = window.__sootsimTest
|
|
1399
|
+
if (!t) return []
|
|
1400
|
+
return await t.queryAll({ hasRole: ${JSON.stringify(q.role)}, pruneHidden: true })
|
|
1422
1401
|
})()`
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
}
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
}
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
}
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
const args = channels.length > 0 ? channels.map((c) => JSON.stringify(c)).join(", ") : action === "disable" ? "'all'" : "";
|
|
1460
|
-
return bridge.send({
|
|
1461
|
-
type: "evaluate",
|
|
1462
|
-
code: `window.__sootsimDebug.${action}(${args})`
|
|
1463
|
-
});
|
|
1464
|
-
}
|
|
1465
|
-
async function inspectMemory(bridge) {
|
|
1466
|
-
const result = await bridge.send({
|
|
1467
|
-
type: "evaluate",
|
|
1468
|
-
code: `(async () => {
|
|
1469
|
-
const host = window.__sootsimRenderHost
|
|
1470
|
-
const stats = host?.queryStats ? await host.queryStats() : null
|
|
1471
|
-
const shellRaw = window.__sootsimShellMemory
|
|
1472
|
-
? await window.__sootsimShellMemory()
|
|
1473
|
-
: null
|
|
1474
|
-
const shell = shellRaw && !shellRaw.error ? shellRaw : null
|
|
1475
|
-
// the compositor paints home + app:one + app:two, so every app-surface
|
|
1476
|
-
// picture and raster image is counted here and nowhere else.
|
|
1477
|
-
let compositor = null
|
|
1478
|
-
try {
|
|
1479
|
-
const cs = await window.__sootsimCompositor?.getStats?.(false, true)
|
|
1480
|
-
compositor = cs?.memory ?? null
|
|
1481
|
-
} catch {}
|
|
1482
|
-
const normalize = (m) =>
|
|
1483
|
-
m
|
|
1484
|
-
? {
|
|
1485
|
-
nodesRegistered: m.nodesRegistered ?? null,
|
|
1486
|
-
nodesDetached: m.nodesDetached ?? null,
|
|
1487
|
-
detachedTypes: m.detachedTypes ?? null,
|
|
1488
|
-
objects: m.objects ?? null,
|
|
1489
|
-
activeNativeAnimations: m.activeNativeAnimations ?? null,
|
|
1490
|
-
imageLoader: m.imageLoader ?? null,
|
|
1491
|
-
workerHeap: m.workerHeap ?? null,
|
|
1492
|
-
wasmHeapBytes: m.wasmHeapBytes ?? null,
|
|
1493
|
-
}
|
|
1494
|
-
: null
|
|
1495
|
-
const hostMem = performance.memory
|
|
1496
|
-
? {
|
|
1497
|
-
usedJSHeapSize: performance.memory.usedJSHeapSize,
|
|
1498
|
-
totalJSHeapSize: performance.memory.totalJSHeapSize,
|
|
1499
|
-
jsHeapSizeLimit: performance.memory.jsHeapSizeLimit,
|
|
1500
|
-
}
|
|
1501
|
-
: null
|
|
1502
|
-
return {
|
|
1503
|
-
tenant: normalize(stats?.memory),
|
|
1504
|
-
shell: normalize(shell),
|
|
1505
|
-
compositor: normalize(compositor),
|
|
1506
|
-
hostHeap: hostMem,
|
|
1402
|
+
};
|
|
1403
|
+
}
|
|
1404
|
+
if (q.type) {
|
|
1405
|
+
return {
|
|
1406
|
+
mode: "type",
|
|
1407
|
+
code: `(async () => {
|
|
1408
|
+
const t = window.__sootsimTest
|
|
1409
|
+
if (!t) return []
|
|
1410
|
+
return await t.queryAll({ type: ${JSON.stringify(q.type)}, pruneHidden: true })
|
|
1411
|
+
})()`
|
|
1412
|
+
};
|
|
1413
|
+
}
|
|
1414
|
+
if (q.pressable) {
|
|
1415
|
+
return {
|
|
1416
|
+
mode: "pressable",
|
|
1417
|
+
code: `(async () => {
|
|
1418
|
+
const t = window.__sootsimTest
|
|
1419
|
+
if (!t) return []
|
|
1420
|
+
${FIND_INSPECTABLE_TARGETS}
|
|
1421
|
+
const inspectable = await fromInspectable()
|
|
1422
|
+
if (inspectable) return inspectable.filter(n => n.pressable && isVisibleTarget(n))
|
|
1423
|
+
const all = await t.queryAll({ pruneHidden: true })
|
|
1424
|
+
return all.filter(n => n.pressable && isVisibleTarget(n))
|
|
1425
|
+
})()`
|
|
1426
|
+
};
|
|
1427
|
+
}
|
|
1428
|
+
if (q.interactive) {
|
|
1429
|
+
return {
|
|
1430
|
+
mode: "interactive-targets",
|
|
1431
|
+
code: `(async () => {
|
|
1432
|
+
const t = window.__sootsimTest
|
|
1433
|
+
if (!t) return []
|
|
1434
|
+
${FIND_INSPECTABLE_TARGETS}
|
|
1435
|
+
const inspectable = await fromInspectable()
|
|
1436
|
+
if (inspectable) {
|
|
1437
|
+
return inspectable.filter(n => n.pressable && isVisibleTarget(n))
|
|
1507
1438
|
}
|
|
1439
|
+
const all = await t.queryAll({ pruneHidden: true })
|
|
1440
|
+
return all.filter(n => n.pressable && isVisibleTarget(n))
|
|
1508
1441
|
})()`
|
|
1509
|
-
|
|
1510
|
-
return result ?? { tenant: null, shell: null, compositor: null, hostHeap: null };
|
|
1511
|
-
}
|
|
1512
|
-
function formatTimelineSummary(summary) {
|
|
1513
|
-
if (summary.total === 0) return "nothing recorded";
|
|
1514
|
-
const parts = [];
|
|
1515
|
-
const ORDER = [
|
|
1516
|
-
"error",
|
|
1517
|
-
"warning",
|
|
1518
|
-
"console",
|
|
1519
|
-
"fetch",
|
|
1520
|
-
"toast",
|
|
1521
|
-
"alert",
|
|
1522
|
-
"actionsheet",
|
|
1523
|
-
"picker",
|
|
1524
|
-
"notification",
|
|
1525
|
-
"screen",
|
|
1526
|
-
"route",
|
|
1527
|
-
"keyboard",
|
|
1528
|
-
"app-launch",
|
|
1529
|
-
"shell",
|
|
1530
|
-
"scroll",
|
|
1531
|
-
"gesture",
|
|
1532
|
-
"text-input",
|
|
1533
|
-
"react-commit",
|
|
1534
|
-
"animation",
|
|
1535
|
-
"reanimated"
|
|
1536
|
-
];
|
|
1537
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1538
|
-
for (const k of ORDER) {
|
|
1539
|
-
const n = summary.byKind[k];
|
|
1540
|
-
if (n) {
|
|
1541
|
-
parts.push(`${n} ${k}${n === 1 ? "" : "s"}`);
|
|
1542
|
-
seen.add(k);
|
|
1543
|
-
}
|
|
1442
|
+
};
|
|
1544
1443
|
}
|
|
1545
|
-
|
|
1546
|
-
|
|
1444
|
+
if (q.visible) {
|
|
1445
|
+
return {
|
|
1446
|
+
mode: "visible",
|
|
1447
|
+
code: `(async () => {
|
|
1448
|
+
const t = window.__sootsimTest
|
|
1449
|
+
if (!t) return []
|
|
1450
|
+
const all = await t.queryAll({ pruneHidden: true })
|
|
1451
|
+
return all.filter(n => n.layout && n.layout.width > 0 && n.layout.height > 0)
|
|
1452
|
+
})()`
|
|
1453
|
+
};
|
|
1547
1454
|
}
|
|
1548
|
-
|
|
1455
|
+
if (q.text) {
|
|
1456
|
+
return {
|
|
1457
|
+
mode: "text",
|
|
1458
|
+
code: `(async () => {
|
|
1459
|
+
const t = window.__sootsimTest
|
|
1460
|
+
if (!t) return null
|
|
1461
|
+
return await t.findByText(${JSON.stringify(q.text)})
|
|
1462
|
+
})()`
|
|
1463
|
+
};
|
|
1464
|
+
}
|
|
1465
|
+
return null;
|
|
1549
1466
|
}
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
"
|
|
1554
|
-
|
|
1555
|
-
function formatTimelineRelativeTime(t, anchor) {
|
|
1556
|
-
if (anchor === null) return new Date(t).toLocaleTimeString();
|
|
1557
|
-
const dt = (t - anchor) / 1e3;
|
|
1558
|
-
const sign = dt >= 0 ? "+" : "";
|
|
1559
|
-
return `${sign}${dt.toFixed(2)}s`;
|
|
1467
|
+
async function inspectFind(bridge, q) {
|
|
1468
|
+
const resolved = resolveFindMode(q);
|
|
1469
|
+
if (!resolved) return null;
|
|
1470
|
+
const result = await bridge.send({ type: "evaluate", code: resolved.code });
|
|
1471
|
+
return { mode: resolved.mode, result };
|
|
1560
1472
|
}
|
|
1561
|
-
function
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
return `${d.phase ?? "?"} ${d.name ?? d.activeName ?? ""}`;
|
|
1571
|
-
case "flow-step":
|
|
1572
|
-
return `${d.status ?? "?"} ${d.stepName ?? "step"}${d.durationMs ? ` ${d.durationMs}ms` : ""}`;
|
|
1573
|
-
case "route":
|
|
1574
|
-
return `${d.phase ?? "?"} ${d.path ?? d.pathname ?? ""}`;
|
|
1575
|
-
case "alert":
|
|
1576
|
-
case "actionsheet":
|
|
1577
|
-
case "picker":
|
|
1578
|
-
return `${d.phase ?? "?"} ${d.title ?? d.message ?? ""}`;
|
|
1579
|
-
case "notification":
|
|
1580
|
-
return `${d.title ?? ""}${d.body ? ` \u2014 ${d.body}` : ""}`;
|
|
1581
|
-
case "fetch":
|
|
1582
|
-
return `${d.method ?? "GET"} ${d.url ?? ""}${d.status ? ` -> ${d.status}` : ""}`;
|
|
1583
|
-
case "console":
|
|
1584
|
-
case "console-log":
|
|
1585
|
-
return `${d.level ?? "log"}: ${(d.message ?? "").toString().slice(0, 120)}`;
|
|
1586
|
-
case "shell":
|
|
1587
|
-
return `${d.event ?? d.type ?? d.phase ?? ""}`;
|
|
1588
|
-
case "scroll":
|
|
1589
|
-
return `${d.phase ?? "?"} ${d.target ?? ""}`;
|
|
1590
|
-
case "gesture":
|
|
1591
|
-
return `${d.phase ?? "?"} ${d.type ?? ""}`;
|
|
1592
|
-
case "text-input":
|
|
1593
|
-
return `${d.phase ?? "?"}${d.value !== void 0 ? ` "${String(d.value).slice(0, 40)}"` : ""}`;
|
|
1594
|
-
case "layout":
|
|
1595
|
-
return `${d.kind ?? "?"} ${d.testID ?? d.type ?? ""}${d.skipped ? ` skipped:${d.reason ?? "unknown"}` : ""}`;
|
|
1596
|
-
case "react-commit": {
|
|
1597
|
-
const slowest = d.slowest;
|
|
1598
|
-
return `${d.fiberCount ?? "?"} fibers ${d.durationMs ?? "?"}ms${slowest?.displayName ? ` \xB7 ${slowest.displayName} ${slowest.durationMs ?? "?"}ms` : ""}`;
|
|
1599
|
-
}
|
|
1600
|
-
case "frame":
|
|
1601
|
-
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` : ""}`;
|
|
1602
|
-
case "reanimated":
|
|
1603
|
-
case "animation":
|
|
1604
|
-
return `${d.kind ?? ""} ${d.target ?? ""}${d.durationMs ? ` ${d.durationMs}ms` : ""}`;
|
|
1473
|
+
function rankInteractive(nodes) {
|
|
1474
|
+
return [...nodes].sort((a, b) => scoreInteractive(b) - scoreInteractive(a));
|
|
1475
|
+
}
|
|
1476
|
+
function scoreInteractive(n) {
|
|
1477
|
+
let score = 0;
|
|
1478
|
+
if (n.testID) score += 100;
|
|
1479
|
+
if (typeof n.text === "string" && n.text.trim().length > 0) score += 60;
|
|
1480
|
+
if (typeof n.accessibilityLabel === "string" && n.accessibilityLabel.trim().length > 0) {
|
|
1481
|
+
score += 30;
|
|
1605
1482
|
}
|
|
1606
|
-
|
|
1483
|
+
if (n.accessibilityRole) score += 15;
|
|
1484
|
+
const w = n.layout?.width ?? 0;
|
|
1485
|
+
const h = n.layout?.height ?? 0;
|
|
1486
|
+
const area = w * h;
|
|
1487
|
+
if (area >= 400 && area <= 6e4) score += 25;
|
|
1488
|
+
else if (area > 6e4) score -= 20;
|
|
1489
|
+
const y = n.absolutePosition?.y ?? 0;
|
|
1490
|
+
if (y < 0) score -= 30;
|
|
1491
|
+
return score;
|
|
1607
1492
|
}
|
|
1608
|
-
function
|
|
1609
|
-
|
|
1610
|
-
const
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
const
|
|
1614
|
-
|
|
1493
|
+
function tapCommandForNode(n) {
|
|
1494
|
+
if (n.testID) return `rnxsim do tap-id ${shellEscape(n.testID)}`;
|
|
1495
|
+
const text = typeof n.text === "string" ? n.text.trim() : "";
|
|
1496
|
+
if (text.length > 0 && text.length <= 80)
|
|
1497
|
+
return `rnxsim do tap-text ${shellEscape(text)}`;
|
|
1498
|
+
const x = Math.round(((n.absolutePosition?.x ?? 0) + (n.layout?.width ?? 0) / 2) * 10) / 10;
|
|
1499
|
+
const y = Math.round(((n.absolutePosition?.y ?? 0) + (n.layout?.height ?? 0) / 2) * 10) / 10;
|
|
1500
|
+
return `rnxsim do tap ${x} ${y}`;
|
|
1615
1501
|
}
|
|
1616
|
-
function
|
|
1617
|
-
if (
|
|
1618
|
-
|
|
1619
|
-
return events.map((e) => formatTimelineEvent(e, anchor)).join("\n");
|
|
1502
|
+
function shellEscape(value) {
|
|
1503
|
+
if (/^[A-Za-z0-9_./@:-]+$/.test(value)) return value;
|
|
1504
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
1620
1505
|
}
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
{
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
const
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
// route-aware settle: a tap that pushes/pops a screen is async \u2014 the
|
|
1644
|
-
// old screen still renders for a moment, then the new one mounts and
|
|
1645
|
-
// its content loads. a pure layout-hash check reports "idle" on the
|
|
1646
|
-
// outgoing screen or on the incoming skeleton, so a driver re-taps
|
|
1647
|
-
// and stacks duplicate navigations. drain any in-flight screen
|
|
1648
|
-
// transition FIRST, bounded by the overall budget. when no
|
|
1649
|
-
// transition is happening this returns in ~80ms (startWindowMs), so
|
|
1650
|
-
// a plain button tap barely pays for it.
|
|
1651
|
-
try {
|
|
1652
|
-
const wfst = window.__sootsimTest?.waitForScreenTransitions
|
|
1653
|
-
if (typeof wfst === 'function') {
|
|
1654
|
-
const remaining = deadline - Date.now()
|
|
1655
|
-
if (remaining > 120) {
|
|
1656
|
-
await wfst({
|
|
1657
|
-
timeoutMs: Math.min(remaining - 80, 4000),
|
|
1658
|
-
settleMs: 64,
|
|
1659
|
-
startWindowMs: 80,
|
|
1660
|
-
})
|
|
1661
|
-
}
|
|
1662
|
-
}
|
|
1663
|
-
} catch {}
|
|
1664
|
-
|
|
1665
|
-
const readSnapshot = async () => {
|
|
1666
|
-
let animating = false
|
|
1667
|
-
let pendingFetches = 0
|
|
1668
|
-
let requestTotal = 0
|
|
1669
|
-
let requestInFlight = 0
|
|
1670
|
-
try {
|
|
1671
|
-
const stats = await window.__sootsimRenderHost?.queryStats?.()
|
|
1672
|
-
if (stats) {
|
|
1673
|
-
animating =
|
|
1674
|
-
stats.hasActiveAnims === true ||
|
|
1675
|
-
stats.hasActiveNativeAnimations === true ||
|
|
1676
|
-
stats.hasPendingAnimationFrames === true
|
|
1677
|
-
// a freshly-pushed screen showing a skeleton is layout-stable
|
|
1678
|
-
// but not actually settled \u2014 its data/images are still in
|
|
1679
|
-
// flight. treat bounded image-loader fetches as not-idle so
|
|
1680
|
-
// settle waits for real content, capped by the budget.
|
|
1681
|
-
const pf = stats.memory && stats.memory.imageLoader
|
|
1682
|
-
? stats.memory.imageLoader.pendingFetches
|
|
1683
|
-
: 0
|
|
1684
|
-
pendingFetches = typeof pf === 'number' ? pf : 0
|
|
1685
|
-
}
|
|
1686
|
-
} catch {}
|
|
1687
|
-
try {
|
|
1688
|
-
const counts = await window.__sootsimTest?.getRequestCounts?.()
|
|
1689
|
-
requestTotal = typeof counts?.total === 'number' ? counts.total : 0
|
|
1690
|
-
requestInFlight = typeof counts?.inFlight === 'number' ? counts.inFlight : 0
|
|
1691
|
-
} catch {}
|
|
1692
|
-
const root = window.__sootsimRoot
|
|
1693
|
-
const nodes = []
|
|
1694
|
-
if (root) {
|
|
1695
|
-
const walk = (n) => {
|
|
1696
|
-
if (n.layout && n.layout.width > 0) {
|
|
1697
|
-
nodes.push(Math.round(n.layout.x) + ',' + Math.round(n.layout.y) + ',' + Math.round(n.layout.width))
|
|
1698
|
-
}
|
|
1699
|
-
for (const c of n.children || []) walk(c)
|
|
1700
|
-
}
|
|
1701
|
-
walk(root)
|
|
1702
|
-
}
|
|
1703
|
-
return { layout: nodes.join(';'), animating, pendingFetches, requestTotal, requestInFlight }
|
|
1506
|
+
var WAIT_READY_PROBE = `(async () => {
|
|
1507
|
+
const t = window.__sootsimTest
|
|
1508
|
+
let nodes = 0
|
|
1509
|
+
try { nodes = (await t?.getNodeCount?.()) || 0 } catch {}
|
|
1510
|
+
let targets = 0
|
|
1511
|
+
let liveFrameActive = false
|
|
1512
|
+
let liveFrameChannels = 0
|
|
1513
|
+
let liveFramePublishes = 0
|
|
1514
|
+
let loadingText = ''
|
|
1515
|
+
const cleanText = (value) =>
|
|
1516
|
+
typeof value === 'string' ? value.replace(/\\s+/g, ' ').trim() : ''
|
|
1517
|
+
const loadingSignal = (value) => {
|
|
1518
|
+
const text = cleanText(value)
|
|
1519
|
+
if (!text) return ''
|
|
1520
|
+
return /^(opening app\\b|looking for your app\\b|building bundle\\b|preparing rnxsim runtime\\b|loading native bundle\\b|loading rnx stubs\\b|loading bundle loader\\b|loading native stubs\\b|loading compat bridge\\b|loading react-native shim\\b|loading react bridge\\b|loading reconciler\\b|loading replacement modules\\b|injecting rnx modules\\b|warming sqlite engine\\b|capturing root component\\b|reloading app\\b|loading(?:[.!\u2026]*)?$|connecting(?:[.!\u2026]*)?$)/i.test(text)
|
|
1521
|
+
? text
|
|
1522
|
+
: ''
|
|
1523
|
+
}
|
|
1524
|
+
try {
|
|
1525
|
+
const loading = window.__sootsimConnectRNLoading
|
|
1526
|
+
if (loading && loading.active === true) {
|
|
1527
|
+
loadingText = [loading.title, loading.detail].map(cleanText).filter(Boolean).join(' ')
|
|
1704
1528
|
}
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1529
|
+
} catch {}
|
|
1530
|
+
try {
|
|
1531
|
+
if (typeof t?.listInspectable === 'function') {
|
|
1532
|
+
const list = await t.listInspectable({})
|
|
1533
|
+
if (Array.isArray(list)) {
|
|
1534
|
+
const screenW = window.innerWidth || 0
|
|
1535
|
+
const screenH = window.innerHeight || 0
|
|
1536
|
+
const isVisible = (n) => {
|
|
1537
|
+
const layout = n?.layout
|
|
1538
|
+
if (!layout || layout.width <= 0 || layout.height <= 0) return false
|
|
1539
|
+
const abs = n?.absolutePosition || n?.absolute
|
|
1540
|
+
if (!abs) return true
|
|
1541
|
+
const device = n?.device || {}
|
|
1542
|
+
const w = Number(device.width) || screenW
|
|
1543
|
+
const h = Number(device.height) || screenH
|
|
1544
|
+
return (
|
|
1545
|
+
abs.x + layout.width > 0 &&
|
|
1546
|
+
abs.y + layout.height > 0 &&
|
|
1547
|
+
abs.x < w &&
|
|
1548
|
+
abs.y < h
|
|
1549
|
+
)
|
|
1550
|
+
}
|
|
1551
|
+
const hasContentSignal = (n) => {
|
|
1552
|
+
const role = n?.accessibilityRole || n?.role
|
|
1553
|
+
const label = cleanText(n?.accessibilityLabel || n?.label)
|
|
1554
|
+
const handlers = Array.isArray(n?.handlers) ? n.handlers : []
|
|
1555
|
+
const text = cleanText(n?.text)
|
|
1556
|
+
const testID = cleanText(n?.testID || n?.testId)
|
|
1557
|
+
const loading = loadingSignal(text) || loadingSignal(label) || loadingSignal(testID)
|
|
1558
|
+
if (!loadingText && loading) {
|
|
1559
|
+
loadingText = loading
|
|
1560
|
+
}
|
|
1561
|
+
if (handlers.length > 0) return true
|
|
1562
|
+
if (role) return true
|
|
1563
|
+
if (n?.isTextInput) return true
|
|
1564
|
+
if (text) return true
|
|
1565
|
+
if (label) return true
|
|
1566
|
+
if (testID && !/^splash|loading/i.test(testID)) return true
|
|
1567
|
+
return false
|
|
1742
1568
|
}
|
|
1743
|
-
|
|
1744
|
-
stable = 0
|
|
1745
|
-
visuallyStillSince = 0
|
|
1569
|
+
targets = list.filter(n => isVisible(n) && hasContentSignal(n)).length
|
|
1746
1570
|
}
|
|
1747
|
-
lastLayout = snapshot.layout
|
|
1748
|
-
lastRequestTotal = snapshot.requestTotal
|
|
1749
|
-
await sleep(pollMs)
|
|
1750
1571
|
}
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1572
|
+
} catch {}
|
|
1573
|
+
try {
|
|
1574
|
+
const stats = await t?.getImageLoaderStats?.()
|
|
1575
|
+
const presentation = stats?.liveFramePresentation
|
|
1576
|
+
liveFrameChannels = Number(presentation?.activeChannels) || 0
|
|
1577
|
+
liveFramePublishes = Number(presentation?.publishes) || 0
|
|
1578
|
+
const previous = window.__sootsimWaitReadyLiveFramePublishes
|
|
1579
|
+
liveFrameActive =
|
|
1580
|
+
liveFrameChannels > 0 &&
|
|
1581
|
+
typeof previous === 'number' &&
|
|
1582
|
+
liveFramePublishes > previous
|
|
1583
|
+
window.__sootsimWaitReadyLiveFramePublishes = liveFramePublishes
|
|
1584
|
+
} catch {}
|
|
1585
|
+
let externalReady = null
|
|
1586
|
+
let externalStatus = ''
|
|
1587
|
+
let externalError = ''
|
|
1588
|
+
let suppressedEntryError = ''
|
|
1589
|
+
try {
|
|
1590
|
+
const getExternalAppState = t?.getExternalAppState
|
|
1591
|
+
const external = typeof getExternalAppState === 'function'
|
|
1592
|
+
? await Promise.race([
|
|
1593
|
+
getExternalAppState(),
|
|
1594
|
+
new Promise((resolve) => setTimeout(
|
|
1595
|
+
() => resolve({
|
|
1596
|
+
state: {
|
|
1597
|
+
ready: false,
|
|
1598
|
+
loading: true,
|
|
1599
|
+
status: 'waiting for tenant worker...',
|
|
1600
|
+
},
|
|
1601
|
+
}),
|
|
1602
|
+
1000,
|
|
1603
|
+
)),
|
|
1604
|
+
])
|
|
1605
|
+
: null
|
|
1606
|
+
const state = external?.state
|
|
1607
|
+
if (state && typeof state === 'object') {
|
|
1608
|
+
externalReady = state.ready === true
|
|
1609
|
+
if (typeof state.status === 'string') externalStatus = state.status
|
|
1610
|
+
if (typeof state.error === 'string') externalError = state.error
|
|
1611
|
+
}
|
|
1612
|
+
if (!externalError && Array.isArray(external?.entryErrors)) {
|
|
1613
|
+
// suppressed-and-continued entry errors (bundle-loader's patched __r
|
|
1614
|
+
// swallows entry throws and moves on) must NOT gate readiness \u2014 the
|
|
1615
|
+
// app frequently runs fine after one (e.g. metro's "Unknown named
|
|
1616
|
+
// module" probing noise). they surface separately as a diagnostic
|
|
1617
|
+
// for the timeout path.
|
|
1618
|
+
const isReal = (item) => item && typeof item.message === 'string' && item.message.trim()
|
|
1619
|
+
const fatal = external.entryErrors.find((item) => isReal(item) && item.suppressed !== true)
|
|
1620
|
+
if (fatal) externalError = fatal.message
|
|
1621
|
+
const suppressed = external.entryErrors.find((item) => isReal(item) && item.suppressed === true)
|
|
1622
|
+
if (suppressed) suppressedEntryError = suppressed.message
|
|
1623
|
+
}
|
|
1624
|
+
// once the app has errored its last status ("injecting sootsim
|
|
1625
|
+
// modules...") is stale \u2014 reporting it makes a dead app read as one that
|
|
1626
|
+
// is still booting.
|
|
1627
|
+
if (externalError) loadingText = ''
|
|
1628
|
+
else if (externalReady === false && externalStatus) loadingText = externalStatus
|
|
1629
|
+
} catch {}
|
|
1630
|
+
let errors = 0
|
|
1631
|
+
try { errors = window.__sootsimConsole?.count?.()?.errors ?? 0 } catch {}
|
|
1632
|
+
return {
|
|
1633
|
+
flag: (window).__sootsimExternalAppReady,
|
|
1634
|
+
at: (window).__sootsimExternalAppReadyAt || 0,
|
|
1635
|
+
nodes,
|
|
1636
|
+
targets,
|
|
1637
|
+
liveFrameActive,
|
|
1638
|
+
liveFrameChannels,
|
|
1639
|
+
liveFramePublishes,
|
|
1640
|
+
errors,
|
|
1641
|
+
loadingText,
|
|
1642
|
+
externalReady,
|
|
1643
|
+
externalStatus,
|
|
1644
|
+
externalError,
|
|
1645
|
+
suppressedEntryError,
|
|
1756
1646
|
}
|
|
1757
|
-
)
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1647
|
+
})()`;
|
|
1648
|
+
var READY_CONTENT_NODE_FLOOR = 100;
|
|
1649
|
+
var READY_NODE_STABLE_MS = 750;
|
|
1650
|
+
var WAIT_READY_PROGRESS_INTERVAL_MS = 2e3;
|
|
1651
|
+
var READY_PROBE_POLL_MS = 150;
|
|
1652
|
+
function readyProbeHasContent(probe) {
|
|
1653
|
+
return probe.liveFrameActive || probe.targets > 0 || probe.nodes >= READY_CONTENT_NODE_FLOOR;
|
|
1763
1654
|
}
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
var SCREEN_EVAL = `(() => {
|
|
1767
|
-
const spec = window.SootSim?.state?.engineSnapshot?.windowState?.deviceSpec
|
|
1768
|
-
return spec && spec.width > 0 && spec.height > 0
|
|
1769
|
-
? { width: spec.width, height: spec.height }
|
|
1770
|
-
: null
|
|
1771
|
-
})()`;
|
|
1772
|
-
var DEFAULT_AGENT_TIMING = {
|
|
1773
|
-
initialWaitMs: 3e3,
|
|
1774
|
-
deadlineMs: 5e3,
|
|
1775
|
-
retryWaitMs: 700
|
|
1776
|
-
};
|
|
1777
|
-
var DEFAULT_INTERACTIVE_TIMING = {
|
|
1778
|
-
initialWaitMs: 1200,
|
|
1779
|
-
deadlineMs: 2500,
|
|
1780
|
-
retryWaitMs: 700
|
|
1781
|
-
};
|
|
1782
|
-
function tapTiming(agent, opts = {}) {
|
|
1783
|
-
return {
|
|
1784
|
-
...agent ? DEFAULT_AGENT_TIMING : DEFAULT_INTERACTIVE_TIMING,
|
|
1785
|
-
...opts
|
|
1786
|
-
};
|
|
1655
|
+
function readyProbeHasTargetContent(probe) {
|
|
1656
|
+
return probe.liveFrameActive || probe.targets > 0;
|
|
1787
1657
|
}
|
|
1788
|
-
function
|
|
1789
|
-
if (
|
|
1790
|
-
|
|
1791
|
-
if (result.pointerTapHandled === false && !result.keyboardOpened && !result.isTextInput) {
|
|
1792
|
-
return false;
|
|
1658
|
+
function waitReadyReason(status) {
|
|
1659
|
+
if (status.bridgeError) {
|
|
1660
|
+
return `sim unreachable: ${status.bridgeError} \u2014 the target tab is closed or no sim is connected (check \`rnxsim list\`, reopen with \`rnxsim open\`)`;
|
|
1793
1661
|
}
|
|
1794
|
-
|
|
1662
|
+
const base = status.externalError ? `guest app errored: ${status.externalError}` : status.loadingText ? `still showing "${status.loadingText}"` : status.externalReady === false ? "guest app is still loading" : status.flag !== true && status.targets > 0 ? "native content is rendered but the ready signal has not settled" : status.flag !== true ? "guest app has not emitted sootsim:externalAppReady" : status.targets <= 0 ? "ready flag emitted but no visible app content is inspectable yet" : "node tree is still changing";
|
|
1663
|
+
return !status.externalError && status.suppressedEntryError ? `${base} (suppressed entry error: ${status.suppressedEntryError})` : base;
|
|
1795
1664
|
}
|
|
1796
|
-
function
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1665
|
+
async function inspectWaitReady(bridge, timeoutMs = 2e4, options = {}) {
|
|
1666
|
+
const start = Date.now();
|
|
1667
|
+
const deadline = start + timeoutMs;
|
|
1668
|
+
const progressIntervalMs = options.progressIntervalMs ?? WAIT_READY_PROGRESS_INTERVAL_MS;
|
|
1669
|
+
let nextProgressAt = start + progressIntervalMs;
|
|
1670
|
+
let lastNodes = -1;
|
|
1671
|
+
let nodeStableSince = start;
|
|
1672
|
+
let bridgeError = "";
|
|
1673
|
+
let last = {
|
|
1674
|
+
flag: void 0,
|
|
1675
|
+
at: 0,
|
|
1676
|
+
nodes: 0,
|
|
1677
|
+
targets: 0,
|
|
1678
|
+
liveFrameActive: false,
|
|
1679
|
+
liveFrameChannels: 0,
|
|
1680
|
+
liveFramePublishes: 0,
|
|
1681
|
+
errors: 0,
|
|
1682
|
+
loadingText: "",
|
|
1683
|
+
externalReady: null,
|
|
1684
|
+
externalStatus: "",
|
|
1685
|
+
externalError: "",
|
|
1686
|
+
suppressedEntryError: ""
|
|
1803
1687
|
};
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1688
|
+
while (Date.now() < deadline) {
|
|
1689
|
+
try {
|
|
1690
|
+
last = await bridge.send({ type: "evaluate", code: WAIT_READY_PROBE }) ?? last;
|
|
1691
|
+
bridgeError = "";
|
|
1692
|
+
} catch (error) {
|
|
1693
|
+
bridgeError = error instanceof Error ? error.message : String(error);
|
|
1694
|
+
if (bridgeError.startsWith("multiple sims are connected:") || bridgeError.startsWith("saved sim ")) {
|
|
1695
|
+
throw error;
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
const now = Date.now();
|
|
1699
|
+
const status2 = {
|
|
1700
|
+
ready: false,
|
|
1701
|
+
elapsedMs: now - start,
|
|
1702
|
+
nodes: last.nodes,
|
|
1703
|
+
targets: last.targets,
|
|
1704
|
+
liveFrameActive: last.liveFrameActive,
|
|
1705
|
+
liveFrameChannels: last.liveFrameChannels,
|
|
1706
|
+
liveFramePublishes: last.liveFramePublishes,
|
|
1707
|
+
flag: last.flag,
|
|
1708
|
+
loadingText: last.loadingText,
|
|
1709
|
+
externalReady: last.externalReady,
|
|
1710
|
+
externalStatus: last.externalStatus,
|
|
1711
|
+
externalError: last.externalError,
|
|
1712
|
+
suppressedEntryError: last.suppressedEntryError,
|
|
1713
|
+
errors: last.errors,
|
|
1714
|
+
bridgeError
|
|
1715
|
+
};
|
|
1716
|
+
if (last.nodes !== lastNodes) {
|
|
1717
|
+
lastNodes = last.nodes;
|
|
1718
|
+
nodeStableSince = now;
|
|
1719
|
+
}
|
|
1720
|
+
const hasReadySignal = last.flag === true && readyProbeHasContent(last);
|
|
1721
|
+
const hasRenderedContentWithoutReadySignal = last.flag !== true && readyProbeHasTargetContent(last);
|
|
1722
|
+
if ((hasReadySignal || hasRenderedContentWithoutReadySignal) && last.externalReady !== false && !last.externalError && !last.loadingText) {
|
|
1723
|
+
if (now - nodeStableSince >= READY_NODE_STABLE_MS) {
|
|
1724
|
+
return {
|
|
1725
|
+
...status2,
|
|
1726
|
+
ready: true
|
|
1727
|
+
};
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
if (last.externalError) return status2;
|
|
1731
|
+
if (options.onProgress && progressIntervalMs > 0 && now >= nextProgressAt) {
|
|
1732
|
+
options.onProgress(status2);
|
|
1733
|
+
do {
|
|
1734
|
+
nextProgressAt += progressIntervalMs;
|
|
1735
|
+
} while (now >= nextProgressAt);
|
|
1736
|
+
}
|
|
1737
|
+
await new Promise((r) => setTimeout(r, READY_PROBE_POLL_MS));
|
|
1738
|
+
}
|
|
1739
|
+
const status = {
|
|
1740
|
+
ready: false,
|
|
1741
|
+
elapsedMs: Date.now() - start,
|
|
1742
|
+
nodes: last.nodes,
|
|
1743
|
+
targets: last.targets,
|
|
1744
|
+
liveFrameActive: last.liveFrameActive,
|
|
1745
|
+
liveFrameChannels: last.liveFrameChannels,
|
|
1746
|
+
liveFramePublishes: last.liveFramePublishes,
|
|
1747
|
+
flag: last.flag,
|
|
1748
|
+
loadingText: last.loadingText,
|
|
1749
|
+
externalReady: last.externalReady,
|
|
1750
|
+
externalStatus: last.externalStatus,
|
|
1751
|
+
externalError: last.externalError,
|
|
1752
|
+
suppressedEntryError: last.suppressedEntryError,
|
|
1753
|
+
errors: last.errors,
|
|
1754
|
+
bridgeError
|
|
1755
|
+
};
|
|
1756
|
+
if (options.onProgress && progressIntervalMs > 0 && Date.now() >= nextProgressAt) {
|
|
1757
|
+
options.onProgress(status);
|
|
1818
1758
|
}
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
typeof t.inspectAt !== 'function' ||
|
|
1852
|
-
typeof t.resolveTapTarget !== 'function'
|
|
1853
|
-
) {
|
|
1854
|
-
return { error: 'tap-target-inspection-unavailable' }
|
|
1855
|
-
}
|
|
1856
|
-
const requestedTarget = ${JSON.stringify(requestedTarget)}
|
|
1857
|
-
const inspected = await t.inspectAt(${payload.cx}, ${payload.cy})
|
|
1858
|
-
const resolved =
|
|
1859
|
-
inspected && typeof inspected.nodeId === 'number'
|
|
1860
|
-
? await t.resolveTapTarget(inspected.nodeId)
|
|
1861
|
-
: null
|
|
1862
|
-
const node = (resolved && resolved.target) || inspected
|
|
1863
|
-
const coordinateTarget = node
|
|
1864
|
-
? {
|
|
1865
|
-
nodeId: node.nodeId ?? null,
|
|
1866
|
-
id: node.id ?? null,
|
|
1867
|
-
testID: node.testID ?? null,
|
|
1868
|
-
text: node.text ?? node.accessibilityLabel ?? null,
|
|
1869
|
-
type: node.type ?? null,
|
|
1870
|
-
}
|
|
1871
|
-
: null
|
|
1872
|
-
const coordinateAncestors = Array.isArray(inspected?.ancestors)
|
|
1873
|
-
? inspected.ancestors
|
|
1874
|
-
: []
|
|
1875
|
-
const ancestorMatchesRequested = coordinateAncestors.some(
|
|
1876
|
-
(ancestor) =>
|
|
1877
|
-
(typeof requestedTarget.nodeId === 'number' &&
|
|
1878
|
-
ancestor?.nodeId === requestedTarget.nodeId) ||
|
|
1879
|
-
(requestedTarget.testID &&
|
|
1880
|
-
ancestor?.testID === requestedTarget.testID) ||
|
|
1881
|
-
(requestedTarget.id && ancestor?.id === requestedTarget.id),
|
|
1882
|
-
)
|
|
1883
|
-
const requestedTargetMatched =
|
|
1884
|
-
coordinateTarget === null
|
|
1885
|
-
? false
|
|
1886
|
-
: typeof requestedTarget.nodeId === 'number' &&
|
|
1887
|
-
typeof coordinateTarget.nodeId === 'number'
|
|
1888
|
-
? requestedTarget.nodeId === coordinateTarget.nodeId ||
|
|
1889
|
-
ancestorMatchesRequested
|
|
1890
|
-
: requestedTarget.testID && coordinateTarget.testID
|
|
1891
|
-
? requestedTarget.testID === coordinateTarget.testID
|
|
1892
|
-
|| ancestorMatchesRequested
|
|
1893
|
-
: requestedTarget.id && coordinateTarget.id
|
|
1894
|
-
? requestedTarget.id === coordinateTarget.id
|
|
1895
|
-
|| ancestorMatchesRequested
|
|
1896
|
-
: ancestorMatchesRequested
|
|
1897
|
-
if (!requestedTargetMatched) {
|
|
1898
|
-
return {
|
|
1899
|
-
requestedTargetMatched,
|
|
1900
|
-
requestedTarget,
|
|
1901
|
-
coordinateTarget,
|
|
1902
|
-
}
|
|
1903
|
-
}
|
|
1904
|
-
if (typeof t.activatePressAt !== 'function') {
|
|
1905
|
-
return { error: 'tap-target-activation-unavailable' }
|
|
1906
|
-
}
|
|
1907
|
-
const activation = await t.activatePressAt(
|
|
1908
|
-
${payload.cx},
|
|
1909
|
-
${payload.cy},
|
|
1910
|
-
requestedTarget.nodeId,
|
|
1911
|
-
)
|
|
1912
|
-
if (activation?.ok) {
|
|
1913
|
-
window.dispatchEvent(
|
|
1914
|
-
new CustomEvent('sootsim:agentAction', {
|
|
1915
|
-
detail: {
|
|
1916
|
-
type: 'tap',
|
|
1917
|
-
x: ${payload.cx},
|
|
1918
|
-
y: ${payload.cy},
|
|
1919
|
-
target: requestedTarget,
|
|
1920
|
-
},
|
|
1921
|
-
}),
|
|
1922
|
-
)
|
|
1923
|
-
}
|
|
1924
|
-
return {
|
|
1925
|
-
activation,
|
|
1926
|
-
requestedTargetMatched,
|
|
1927
|
-
requestedTarget,
|
|
1928
|
-
coordinateTarget,
|
|
1929
|
-
}
|
|
1930
|
-
})()`
|
|
1931
|
-
});
|
|
1932
|
-
if (verification?.error) {
|
|
1933
|
-
return {
|
|
1934
|
-
payload,
|
|
1935
|
-
result: {
|
|
1936
|
-
hit: false,
|
|
1937
|
-
reason: verification.error,
|
|
1938
|
-
requestedTarget
|
|
1939
|
-
},
|
|
1940
|
-
attempts,
|
|
1941
|
-
failure: "special"
|
|
1942
|
-
};
|
|
1759
|
+
return status;
|
|
1760
|
+
}
|
|
1761
|
+
async function inspectWaitSelector(bridge, testId, timeoutMs = 5e3, opts = {}) {
|
|
1762
|
+
const gone = opts.gone === true;
|
|
1763
|
+
const result = await bridge.send(
|
|
1764
|
+
{
|
|
1765
|
+
type: "evaluate",
|
|
1766
|
+
code: `(async () => {
|
|
1767
|
+
const start = Date.now()
|
|
1768
|
+
const deadline = start + ${timeoutMs}
|
|
1769
|
+
const gone = ${gone}
|
|
1770
|
+
const ID = ${JSON.stringify(testId)}
|
|
1771
|
+
const present = (node) => !!(node && node.layout && node.layout.width > 0 && node.layout.height > 0)
|
|
1772
|
+
const find = async () => {
|
|
1773
|
+
const t = window.__sootsimTest
|
|
1774
|
+
if (!t) return undefined // bridge not ready \u2014 indeterminate, not "gone"
|
|
1775
|
+
try {
|
|
1776
|
+
// gone-mode confirms a gate/screen LEFT \u2014 it only needs structural
|
|
1777
|
+
// existence + layout, NOT the full-tree occlusion pass (pruneHidden).
|
|
1778
|
+
// that pass (computeOccludedInspectNodeIds) walks the whole tree and
|
|
1779
|
+
// does O(n\xB2)-ish rect-occlusion testing EVERY poll; on a large
|
|
1780
|
+
// post-login app tree it costs seconds per poll, so --gone can never
|
|
1781
|
+
// get two clean "absent" polls inside budget and falsely reports the
|
|
1782
|
+
// gate as still blocked \u2014 the root cause of the 3pc PR-preview outage.
|
|
1783
|
+
// presence-mode keeps pruneHidden so "appeared" still means
|
|
1784
|
+
// visibly-reachable. a departed gate is unmounted or zero-sized, so the
|
|
1785
|
+
// cheap structural+layout check is correct here and ~O(n) cheap.
|
|
1786
|
+
const matches = await t.queryAll?.(gone ? { hasId: ID } : { hasId: ID, pruneHidden: true })
|
|
1787
|
+
if (!Array.isArray(matches)) return undefined
|
|
1788
|
+
return matches.find(present) || null
|
|
1789
|
+
} catch {
|
|
1790
|
+
return undefined
|
|
1943
1791
|
}
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
}
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
}
|
|
1792
|
+
}
|
|
1793
|
+
let goneStreak = 0
|
|
1794
|
+
while (Date.now() < deadline) {
|
|
1795
|
+
const node = await find()
|
|
1796
|
+
if (gone) {
|
|
1797
|
+
if (node === null) {
|
|
1798
|
+
goneStreak += 1
|
|
1799
|
+
if (goneStreak >= 2) return { found: true, elapsed: Date.now() - start }
|
|
1800
|
+
} else {
|
|
1801
|
+
goneStreak = 0
|
|
1802
|
+
}
|
|
1803
|
+
} else if (present(node)) {
|
|
1804
|
+
return { found: true, node, elapsed: Date.now() - start }
|
|
1955
1805
|
}
|
|
1956
|
-
|
|
1957
|
-
const result = dispatched && typeof dispatched === "object" ? { ...dispatched, ...verification?.activation, ...verification } : { hit: !!dispatched, ...verification };
|
|
1958
|
-
lastResult = result;
|
|
1959
|
-
if (isTapSuccess(result)) return { payload, result, attempts };
|
|
1806
|
+
await new Promise((r) => setTimeout(r, 80))
|
|
1960
1807
|
}
|
|
1808
|
+
return { found: false, elapsed: Date.now() - start }
|
|
1809
|
+
})()`
|
|
1810
|
+
},
|
|
1811
|
+
{
|
|
1812
|
+
timeoutMs: timeoutMs + 1e3
|
|
1961
1813
|
}
|
|
1962
|
-
|
|
1963
|
-
|
|
1814
|
+
);
|
|
1815
|
+
return result ?? { found: false, elapsed: timeoutMs };
|
|
1816
|
+
}
|
|
1817
|
+
function consoleEntriesEval(level, limit) {
|
|
1818
|
+
const getter = level === "error" ? "getErrors" : "getWarnings";
|
|
1819
|
+
return `(() => {
|
|
1820
|
+
const out = []
|
|
1821
|
+
const seen = new Set()
|
|
1822
|
+
const norm = (s) => (typeof s === 'string' ? s : (() => { try { return JSON.stringify(s) } catch { return String(s) } })())
|
|
1823
|
+
const key = (ts, args) => Math.round((ts || 0) / 250) + '|' + (Array.isArray(args) ? args.map(norm).join(' ') : norm(args))
|
|
1824
|
+
const push = (e) => {
|
|
1825
|
+
if (!e) return
|
|
1826
|
+
const ts = typeof e.timestamp === 'number' ? e.timestamp : (typeof e.ts === 'number' ? e.ts : 0)
|
|
1827
|
+
const k = key(ts, e.args)
|
|
1828
|
+
if (seen.has(k)) return
|
|
1829
|
+
seen.add(k)
|
|
1830
|
+
out.push({ timestamp: ts, args: Array.isArray(e.args) ? e.args : [e.args], stack: e.stack || undefined, source: e.source || undefined })
|
|
1831
|
+
}
|
|
1832
|
+
// page-realm ws-bridge buffer first (richest stacks for page-origin errors)
|
|
1833
|
+
try { for (const e of (window.__sootsimConsole?.${getter}(${limit}) || [])) push(e) } catch {}
|
|
1834
|
+
// engine observability store \u2014 the always-on render-worker / forwarded path
|
|
1964
1835
|
try {
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1836
|
+
const obs = window.__sootsimObservability
|
|
1837
|
+
const snap = obs && obs.logs && typeof obs.logs.getSnapshot === 'function' ? obs.logs.getSnapshot() : []
|
|
1838
|
+
for (const e of snap) if (e && e.level === ${JSON.stringify(level)}) push(e)
|
|
1839
|
+
} catch {}
|
|
1840
|
+
return out.sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0)).slice(-${limit})
|
|
1841
|
+
})()`;
|
|
1842
|
+
}
|
|
1843
|
+
async function inspectErrors(bridge, limit = 20) {
|
|
1844
|
+
const result = await bridge.send({
|
|
1845
|
+
type: "evaluate",
|
|
1846
|
+
code: consoleEntriesEval("error", limit)
|
|
1847
|
+
});
|
|
1848
|
+
return Array.isArray(result) ? result : [];
|
|
1849
|
+
}
|
|
1850
|
+
async function inspectWarnings(bridge, limit = 20) {
|
|
1851
|
+
const result = await bridge.send({
|
|
1852
|
+
type: "evaluate",
|
|
1853
|
+
code: consoleEntriesEval("warn", limit)
|
|
1854
|
+
});
|
|
1855
|
+
return Array.isArray(result) ? result : [];
|
|
1856
|
+
}
|
|
1857
|
+
var MERGED_CONSOLE_COUNT_EVAL = `(() => {
|
|
1858
|
+
const norm = (s) => (typeof s === 'string' ? s : (() => { try { return JSON.stringify(s) } catch { return String(s) } })())
|
|
1859
|
+
const key = (lvl, ts, args) => lvl + '|' + Math.round((ts || 0) / 250) + '|' + (Array.isArray(args) ? args.map(norm).join(' ') : norm(args))
|
|
1860
|
+
const seen = new Set()
|
|
1861
|
+
let errors = 0
|
|
1862
|
+
let warnings = 0
|
|
1863
|
+
const add = (lvl, ts, args) => {
|
|
1864
|
+
if (lvl !== 'error' && lvl !== 'warn') return
|
|
1865
|
+
const k = key(lvl, ts, args)
|
|
1866
|
+
if (seen.has(k)) return
|
|
1867
|
+
seen.add(k)
|
|
1868
|
+
if (lvl === 'error') errors++
|
|
1869
|
+
else warnings++
|
|
1973
1870
|
}
|
|
1974
|
-
|
|
1871
|
+
try {
|
|
1872
|
+
const c = window.__sootsimConsole
|
|
1873
|
+
for (const e of (c?.getErrors?.(200) || [])) add('error', e?.timestamp ?? e?.ts, e?.args)
|
|
1874
|
+
for (const e of (c?.getWarnings?.(200) || [])) add('warn', e?.timestamp ?? e?.ts, e?.args)
|
|
1875
|
+
} catch {}
|
|
1876
|
+
try {
|
|
1877
|
+
const obs = window.__sootsimObservability
|
|
1878
|
+
const snap = obs && obs.logs && typeof obs.logs.getSnapshot === 'function' ? obs.logs.getSnapshot() : []
|
|
1879
|
+
for (const e of snap) if (e) add(e.level, e.ts, e.args)
|
|
1880
|
+
} catch {}
|
|
1881
|
+
return { errors, warnings, total: errors + warnings }
|
|
1882
|
+
})()`;
|
|
1883
|
+
async function clearConsole(bridge) {
|
|
1884
|
+
await bridge.send({
|
|
1885
|
+
type: "evaluate",
|
|
1886
|
+
code: 'window.__sootsimConsole?.clear(); window.__sootsimObservability?.logs?.clear?.(); "cleared"'
|
|
1887
|
+
});
|
|
1888
|
+
}
|
|
1889
|
+
async function inspectRequests(bridge, opts = {}) {
|
|
1890
|
+
const limit = opts.limit ?? 20;
|
|
1891
|
+
const method = opts.failed === false ? "getRequests" : "getFailedRequests";
|
|
1892
|
+
const result = await bridge.send({
|
|
1893
|
+
type: "call",
|
|
1894
|
+
path: `__sootsimTest.${method}`,
|
|
1895
|
+
args: [limit]
|
|
1896
|
+
});
|
|
1897
|
+
return Array.isArray(result) ? result : [];
|
|
1898
|
+
}
|
|
1899
|
+
async function clearRequests(bridge) {
|
|
1900
|
+
await bridge.send({ type: "call", path: "__sootsimTest.clearRequests", args: [] });
|
|
1901
|
+
}
|
|
1902
|
+
async function inspectScrollStateAt(bridge, x, y) {
|
|
1903
|
+
const result = await bridge.send({
|
|
1904
|
+
type: "call",
|
|
1905
|
+
path: "__sootsimTest.getScrollStateAt",
|
|
1906
|
+
args: [x, y]
|
|
1907
|
+
});
|
|
1908
|
+
if (!result || typeof result !== "object") return null;
|
|
1909
|
+
const state = result;
|
|
1975
1910
|
return {
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
failure: lastPayload && typeof lastPayload.cx === "number" ? "missed" : "not-found"
|
|
1911
|
+
...state,
|
|
1912
|
+
scrollOffsetX: typeof state.scrollOffsetX === "number" ? state.scrollOffsetX : state.offsetX,
|
|
1913
|
+
scrollOffsetY: typeof state.scrollOffsetY === "number" ? state.scrollOffsetY : state.offsetY
|
|
1980
1914
|
};
|
|
1981
1915
|
}
|
|
1982
|
-
async function
|
|
1983
|
-
|
|
1984
|
-
type: "
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1916
|
+
async function inspectLogs(bridge) {
|
|
1917
|
+
const res = await bridge.send({
|
|
1918
|
+
type: "evaluate",
|
|
1919
|
+
code: `(() => {
|
|
1920
|
+
const obs = window.__sootsimObservability;
|
|
1921
|
+
if (!obs) return { ok: false };
|
|
1922
|
+
return { ok: true, entries: obs.logs.getSnapshot() };
|
|
1923
|
+
})()`
|
|
1988
1924
|
});
|
|
1925
|
+
if (!res || !res.ok) return [];
|
|
1926
|
+
return res.entries ?? [];
|
|
1989
1927
|
}
|
|
1990
|
-
async function
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
timing: opts.timing,
|
|
1995
|
-
resolve: () => bridge.send({
|
|
1996
|
-
type: "evaluate",
|
|
1997
|
-
code: `(async () => {
|
|
1998
|
-
const t = window.__sootsimTest
|
|
1999
|
-
if (!t) return null
|
|
2000
|
-
const n = (await t.findByTestId(${arg})) || (await t.findById(${arg}))
|
|
2001
|
-
if (!n || !n.absolutePosition || !n.layout) return { cx: null }
|
|
2002
|
-
const resolved =
|
|
2003
|
-
typeof n.nodeId === 'number' && typeof t.resolveTapTarget === 'function'
|
|
2004
|
-
? await t.resolveTapTarget(n.nodeId)
|
|
2005
|
-
: null
|
|
2006
|
-
const target = (resolved && resolved.target) || n
|
|
2007
|
-
const cx =
|
|
2008
|
-
resolved && typeof resolved.cx === 'number'
|
|
2009
|
-
? resolved.cx
|
|
2010
|
-
: n.absolutePosition.x + (n.layout.width || 0) / 2
|
|
2011
|
-
const cy =
|
|
2012
|
-
resolved && typeof resolved.cy === 'number'
|
|
2013
|
-
? resolved.cy
|
|
2014
|
-
: n.absolutePosition.y + (n.layout.height || 0) / 2
|
|
2015
|
-
const scr = ${SCREEN_EVAL}
|
|
2016
|
-
const offscreen =
|
|
2017
|
-
!!scr && (cx < 0 || cy < 0 || cx > scr.width || cy > scr.height)
|
|
2018
|
-
return {
|
|
2019
|
-
cx,
|
|
2020
|
-
cy,
|
|
2021
|
-
...(offscreen ? { offscreen: true, screen: scr } : {}),
|
|
2022
|
-
match: {
|
|
2023
|
-
nodeId: n.nodeId ?? null,
|
|
2024
|
-
id: n.id,
|
|
2025
|
-
testID: n.testID,
|
|
2026
|
-
text: n.text ?? n.accessibilityLabel ?? null,
|
|
2027
|
-
type: n.type,
|
|
2028
|
-
},
|
|
2029
|
-
target: {
|
|
2030
|
-
nodeId: target.nodeId ?? null,
|
|
2031
|
-
id: target.id,
|
|
2032
|
-
testID: target.testID,
|
|
2033
|
-
text:
|
|
2034
|
-
target.text ??
|
|
2035
|
-
target.accessibilityLabel ??
|
|
2036
|
-
n.text ??
|
|
2037
|
-
n.accessibilityLabel ??
|
|
2038
|
-
null,
|
|
2039
|
-
type: target.type,
|
|
2040
|
-
},
|
|
2041
|
-
strategy: (resolved && resolved.strategy) || 'matched-node',
|
|
2042
|
-
}
|
|
2043
|
-
})()`
|
|
2044
|
-
})
|
|
1928
|
+
async function clearLogs(bridge) {
|
|
1929
|
+
await bridge.send({
|
|
1930
|
+
type: "evaluate",
|
|
1931
|
+
code: 'window.__sootsimObservability?.logs.clear(); "cleared"'
|
|
2045
1932
|
});
|
|
2046
1933
|
}
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
1934
|
+
function isForwardedWorkerLogTwin(a, b) {
|
|
1935
|
+
if (a.source === b.source) return false;
|
|
1936
|
+
if (a.level !== b.level) return false;
|
|
1937
|
+
if (Math.abs(a.ts - b.ts) > 1e3) return false;
|
|
1938
|
+
if (a.args.length !== b.args.length) return false;
|
|
1939
|
+
const sources = /* @__PURE__ */ new Set([a.source, b.source]);
|
|
1940
|
+
if (!sources.has("sootsim-worker")) return false;
|
|
1941
|
+
if (!sources.has("render-worker") && !sources.has("forwarded-render-worker")) {
|
|
1942
|
+
return false;
|
|
1943
|
+
}
|
|
1944
|
+
return a.args.every((arg, index) => arg === b.args[index]);
|
|
1945
|
+
}
|
|
1946
|
+
function filterLogEntries(entries, opts = {}) {
|
|
1947
|
+
let out = [];
|
|
1948
|
+
for (const entry of entries) {
|
|
1949
|
+
if (out.some((candidate) => isForwardedWorkerLogTwin(candidate, entry))) {
|
|
1950
|
+
continue;
|
|
1951
|
+
}
|
|
1952
|
+
out.push(entry);
|
|
1953
|
+
}
|
|
1954
|
+
if (!opts.showInternal) {
|
|
1955
|
+
out = out.filter((e) => {
|
|
1956
|
+
const first = e.args[0];
|
|
1957
|
+
return !(typeof first === "string" && first.startsWith("[rnx]"));
|
|
1958
|
+
});
|
|
1959
|
+
}
|
|
1960
|
+
if (opts.level) out = out.filter((e) => opts.level.has(e.level));
|
|
1961
|
+
if (opts.filter) {
|
|
1962
|
+
const lf = opts.filter.toLowerCase();
|
|
1963
|
+
out = out.filter((e) => e.args.join(" ").toLowerCase().includes(lf));
|
|
1964
|
+
}
|
|
1965
|
+
return out;
|
|
1966
|
+
}
|
|
1967
|
+
async function inspectTimelineSummary(bridge, query) {
|
|
1968
|
+
return await bridge.send({
|
|
1969
|
+
type: "call",
|
|
1970
|
+
path: "SootSim.bridges.timeline.summary",
|
|
1971
|
+
args: [query]
|
|
2060
1972
|
});
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
1973
|
+
}
|
|
1974
|
+
async function inspectTimelineRecent(bridge, query) {
|
|
1975
|
+
return await bridge.send({
|
|
1976
|
+
type: "call",
|
|
1977
|
+
path: "SootSim.bridges.timeline.recent",
|
|
1978
|
+
args: [query]
|
|
1979
|
+
});
|
|
1980
|
+
}
|
|
1981
|
+
async function detectOpenNativeUI(bridge) {
|
|
1982
|
+
let events;
|
|
1983
|
+
try {
|
|
1984
|
+
const res = await inspectTimelineRecent(bridge, {
|
|
1985
|
+
kinds: ["alert", "actionsheet"],
|
|
1986
|
+
limit: 80
|
|
1987
|
+
});
|
|
1988
|
+
events = res.events;
|
|
1989
|
+
} catch {
|
|
1990
|
+
return [];
|
|
1991
|
+
}
|
|
1992
|
+
const latest = /* @__PURE__ */ new Map();
|
|
1993
|
+
for (const ev of events) latest.set(ev.kind, ev);
|
|
1994
|
+
const out = [];
|
|
1995
|
+
const check = (kind, label) => {
|
|
1996
|
+
const ev = latest.get(kind);
|
|
1997
|
+
if (!ev || !ev.data || typeof ev.data !== "object") return;
|
|
1998
|
+
const d = ev.data;
|
|
1999
|
+
if (d.phase !== "show") return;
|
|
2000
|
+
out.push({ label, title: typeof d.title === "string" ? d.title : null });
|
|
2001
|
+
};
|
|
2002
|
+
check("alert", "Alert");
|
|
2003
|
+
check("actionsheet", "ActionSheet");
|
|
2004
|
+
return out;
|
|
2005
|
+
}
|
|
2006
|
+
async function inspectTimelineAdvanceCursor(bridge, cursorKey, watermark) {
|
|
2007
|
+
await bridge.send({
|
|
2008
|
+
type: "call",
|
|
2009
|
+
path: "SootSim.bridges.timeline.cursorAdvance",
|
|
2010
|
+
args: [cursorKey, watermark]
|
|
2011
|
+
});
|
|
2012
|
+
}
|
|
2013
|
+
async function inspectKeyboard(bridge) {
|
|
2014
|
+
const result = await bridge.send({
|
|
2015
|
+
type: "evaluate",
|
|
2016
|
+
code: `(() => {
|
|
2017
|
+
const kb = window.__sootsimKeyboard
|
|
2018
|
+
if (!kb || typeof kb.getLayout !== 'function') {
|
|
2019
|
+
return { error: 'keyboard bridge getLayout() not available' }
|
|
2020
|
+
}
|
|
2021
|
+
return kb.getLayout()
|
|
2022
|
+
})()`
|
|
2023
|
+
});
|
|
2024
|
+
return result ?? { error: "keyboard bridge returned no result" };
|
|
2025
|
+
}
|
|
2026
|
+
function isShellCommandUnavailable(error) {
|
|
2027
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2028
|
+
return message.includes("call target not found: SootSim.bridges.mainShell") || message.includes("test bridge unavailable before app-in-worker boot");
|
|
2029
|
+
}
|
|
2030
|
+
async function getShellState(bridge, readyTimeoutMs = 0) {
|
|
2031
|
+
const deadline = Date.now() + Math.max(0, readyTimeoutMs);
|
|
2032
|
+
while (true) {
|
|
2033
|
+
try {
|
|
2034
|
+
return await bridge.send({
|
|
2035
|
+
type: "call",
|
|
2036
|
+
path: "SootSim.bridges.mainShell.getState",
|
|
2037
|
+
args: []
|
|
2038
|
+
});
|
|
2039
|
+
} catch (error) {
|
|
2040
|
+
if (!isShellCommandUnavailable(error) || Date.now() >= deadline) throw error;
|
|
2041
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
async function inspectScreens(bridge) {
|
|
2046
|
+
const payload = await bridge.send({
|
|
2047
|
+
type: "evaluate",
|
|
2048
|
+
code: `(async () => {
|
|
2049
|
+
const test = window.__sootsimTest
|
|
2050
|
+
const kb = window.__sootsimKeyboard
|
|
2051
|
+
const navSnap =
|
|
2052
|
+
test && typeof test.getNavigationSnapshot === 'function'
|
|
2053
|
+
? await test.getNavigationSnapshot()
|
|
2054
|
+
: null
|
|
2055
|
+
const route =
|
|
2056
|
+
test && typeof test.getRouteInfo === 'function'
|
|
2057
|
+
? await test.getRouteInfo()
|
|
2058
|
+
: null
|
|
2059
|
+
const keyboard =
|
|
2060
|
+
kb && typeof kb.getLayout === 'function'
|
|
2061
|
+
? (() => {
|
|
2062
|
+
const layout = kb.getLayout()
|
|
2063
|
+
return layout ? {
|
|
2064
|
+
visible: layout.visible,
|
|
2065
|
+
mode: layout.mode,
|
|
2066
|
+
spec: layout.spec
|
|
2134
2067
|
? {
|
|
2135
|
-
|
|
2136
|
-
|
|
2068
|
+
keyboardType: layout.spec.keyboardType,
|
|
2069
|
+
returnKeyType: layout.spec.returnKeyType,
|
|
2137
2070
|
}
|
|
2138
2071
|
: null,
|
|
2139
|
-
|
|
2140
|
-
|
|
2072
|
+
} : null
|
|
2073
|
+
})()
|
|
2074
|
+
: null
|
|
2075
|
+
return { nav: navSnap, route, keyboard }
|
|
2076
|
+
})()`
|
|
2077
|
+
});
|
|
2078
|
+
const shell = await getShellState(bridge, 500).catch(() => null);
|
|
2079
|
+
return {
|
|
2080
|
+
shell,
|
|
2081
|
+
nav: payload?.nav ?? null,
|
|
2082
|
+
route: payload?.route ?? null,
|
|
2083
|
+
keyboard: payload?.keyboard ?? null
|
|
2084
|
+
};
|
|
2085
|
+
}
|
|
2086
|
+
var DEBUG_CHANNELS = [
|
|
2087
|
+
"portals",
|
|
2088
|
+
"sheets",
|
|
2089
|
+
"layout",
|
|
2090
|
+
"onlayout",
|
|
2091
|
+
"animated",
|
|
2092
|
+
"render",
|
|
2093
|
+
"touch",
|
|
2094
|
+
"yoga"
|
|
2095
|
+
];
|
|
2096
|
+
async function inspectDebugStatus(bridge) {
|
|
2097
|
+
return bridge.send({ type: "evaluate", code: "window.__sootsimDebug.status()" });
|
|
2098
|
+
}
|
|
2099
|
+
async function inspectDebugFlags(bridge) {
|
|
2100
|
+
return bridge.send({ type: "evaluate", code: "window.__sootsimDebug.flags()" });
|
|
2101
|
+
}
|
|
2102
|
+
async function inspectDebugFind(bridge, target) {
|
|
2103
|
+
return bridge.send({
|
|
2104
|
+
type: "evaluate",
|
|
2105
|
+
code: `window.__sootsimTest.debugFind(${JSON.stringify(target)})`
|
|
2106
|
+
});
|
|
2107
|
+
}
|
|
2108
|
+
async function inspectDebugRecent(bridge, channel, limit = 50) {
|
|
2109
|
+
const code = channel && channel !== "all" ? `window.__sootsimDebug.recent(${JSON.stringify(channel)}, ${limit})` : `window.__sootsimDebug.recent(undefined, ${limit})`;
|
|
2110
|
+
return bridge.send({ type: "evaluate", code });
|
|
2111
|
+
}
|
|
2112
|
+
async function setDebugChannels(bridge, action, channels) {
|
|
2113
|
+
const args = channels.length > 0 ? channels.map((c) => JSON.stringify(c)).join(", ") : action === "disable" ? "'all'" : "";
|
|
2114
|
+
return bridge.send({
|
|
2115
|
+
type: "evaluate",
|
|
2116
|
+
code: `window.__sootsimDebug.${action}(${args})`
|
|
2117
|
+
});
|
|
2118
|
+
}
|
|
2119
|
+
async function inspectMemory(bridge) {
|
|
2120
|
+
const result = await bridge.send({
|
|
2121
|
+
type: "evaluate",
|
|
2122
|
+
code: `(async () => {
|
|
2123
|
+
const host = window.__sootsimRenderHost
|
|
2124
|
+
const stats = host?.queryStats ? await host.queryStats() : null
|
|
2125
|
+
const shellRaw = window.__sootsimShellMemory
|
|
2126
|
+
? await window.__sootsimShellMemory()
|
|
2127
|
+
: null
|
|
2128
|
+
const shell = shellRaw && !shellRaw.error ? shellRaw : null
|
|
2129
|
+
// the compositor paints home + app:one + app:two, so every app-surface
|
|
2130
|
+
// picture and raster image is counted here and nowhere else.
|
|
2131
|
+
let compositor = null
|
|
2132
|
+
try {
|
|
2133
|
+
const cs = await window.__sootsimCompositor?.getStats?.(false, true)
|
|
2134
|
+
compositor = cs?.memory ?? null
|
|
2135
|
+
} catch {}
|
|
2136
|
+
const normalize = (m) =>
|
|
2137
|
+
m
|
|
2138
|
+
? {
|
|
2139
|
+
nodesRegistered: m.nodesRegistered ?? null,
|
|
2140
|
+
nodesDetached: m.nodesDetached ?? null,
|
|
2141
|
+
detachedTypes: m.detachedTypes ?? null,
|
|
2142
|
+
objects: m.objects ?? null,
|
|
2143
|
+
activeNativeAnimations: m.activeNativeAnimations ?? null,
|
|
2144
|
+
imageLoader: m.imageLoader ?? null,
|
|
2145
|
+
workerHeap: m.workerHeap ?? null,
|
|
2146
|
+
wasmHeapBytes: m.wasmHeapBytes ?? null,
|
|
2141
2147
|
}
|
|
2148
|
+
: null
|
|
2149
|
+
const hostMem = performance.memory
|
|
2150
|
+
? {
|
|
2151
|
+
usedJSHeapSize: performance.memory.usedJSHeapSize,
|
|
2152
|
+
totalJSHeapSize: performance.memory.totalJSHeapSize,
|
|
2153
|
+
jsHeapSizeLimit: performance.memory.jsHeapSizeLimit,
|
|
2142
2154
|
}
|
|
2155
|
+
: null
|
|
2156
|
+
return {
|
|
2157
|
+
tenant: normalize(stats?.memory),
|
|
2158
|
+
shell: normalize(shell),
|
|
2159
|
+
compositor: normalize(compositor),
|
|
2160
|
+
hostHeap: hostMem,
|
|
2161
|
+
}
|
|
2162
|
+
})()`
|
|
2163
|
+
});
|
|
2164
|
+
return result ?? { tenant: null, shell: null, compositor: null, hostHeap: null };
|
|
2165
|
+
}
|
|
2166
|
+
function formatTimelineSummary(summary) {
|
|
2167
|
+
if (summary.total === 0) return "nothing recorded";
|
|
2168
|
+
const parts = [];
|
|
2169
|
+
const ORDER = [
|
|
2170
|
+
"error",
|
|
2171
|
+
"warning",
|
|
2172
|
+
"console",
|
|
2173
|
+
"fetch",
|
|
2174
|
+
"toast",
|
|
2175
|
+
"alert",
|
|
2176
|
+
"actionsheet",
|
|
2177
|
+
"picker",
|
|
2178
|
+
"notification",
|
|
2179
|
+
"screen",
|
|
2180
|
+
"route",
|
|
2181
|
+
"keyboard",
|
|
2182
|
+
"app-launch",
|
|
2183
|
+
"shell",
|
|
2184
|
+
"scroll",
|
|
2185
|
+
"gesture",
|
|
2186
|
+
"text-input",
|
|
2187
|
+
"react-commit",
|
|
2188
|
+
"animation",
|
|
2189
|
+
"reanimated"
|
|
2190
|
+
];
|
|
2191
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2192
|
+
for (const k of ORDER) {
|
|
2193
|
+
const n = summary.byKind[k];
|
|
2194
|
+
if (n) {
|
|
2195
|
+
parts.push(`${n} ${k}${n === 1 ? "" : "s"}`);
|
|
2196
|
+
seen.add(k);
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
for (const [k, n] of Object.entries(summary.byKind)) {
|
|
2200
|
+
if (!seen.has(k) && n) parts.push(`${n} ${k}${n === 1 ? "" : "s"}`);
|
|
2201
|
+
}
|
|
2202
|
+
return parts.join(" \xB7 ");
|
|
2203
|
+
}
|
|
2204
|
+
var NOISY_TIMELINE_KINDS = /* @__PURE__ */ new Set([
|
|
2205
|
+
"react-commit",
|
|
2206
|
+
"layout",
|
|
2207
|
+
"scroll"
|
|
2208
|
+
]);
|
|
2209
|
+
function formatTimelineRelativeTime(t, anchor) {
|
|
2210
|
+
if (anchor === null) return new Date(t).toLocaleTimeString();
|
|
2211
|
+
const dt = (t - anchor) / 1e3;
|
|
2212
|
+
const sign = dt >= 0 ? "+" : "";
|
|
2213
|
+
return `${sign}${dt.toFixed(2)}s`;
|
|
2214
|
+
}
|
|
2215
|
+
function formatTimelinePayload(kind, d) {
|
|
2216
|
+
switch (kind) {
|
|
2217
|
+
case "app-launch":
|
|
2218
|
+
return d.phase === "launch" ? `launch ${d.appName ?? d.toAppId ?? ""}` : `dismiss ${d.appName ?? d.fromAppId ?? ""} \u2192 ${d.toAppId ?? ""}`;
|
|
2219
|
+
case "toast":
|
|
2220
|
+
return `"${d.text ?? ""}"${d.durationMs ? ` (${d.durationMs}ms)` : ""}`;
|
|
2221
|
+
case "keyboard":
|
|
2222
|
+
return `${d.phase ?? "?"}${d.heightPx ? ` h=${d.heightPx}` : ""}${d.mode ? ` ${d.mode}` : ""}`;
|
|
2223
|
+
case "screen":
|
|
2224
|
+
return `${d.phase ?? "?"} ${d.name ?? d.activeName ?? ""}`;
|
|
2225
|
+
case "flow-step":
|
|
2226
|
+
return `${d.status ?? "?"} ${d.stepName ?? "step"}${d.durationMs ? ` ${d.durationMs}ms` : ""}`;
|
|
2227
|
+
case "route":
|
|
2228
|
+
return `${d.phase ?? "?"} ${d.path ?? d.pathname ?? ""}`;
|
|
2229
|
+
case "alert":
|
|
2230
|
+
case "actionsheet":
|
|
2231
|
+
case "picker":
|
|
2232
|
+
return `${d.phase ?? "?"} ${d.title ?? d.message ?? ""}`;
|
|
2233
|
+
case "notification":
|
|
2234
|
+
return `${d.title ?? ""}${d.body ? ` \u2014 ${d.body}` : ""}`;
|
|
2235
|
+
case "fetch":
|
|
2236
|
+
return `${d.method ?? "GET"} ${d.url ?? ""}${d.status ? ` -> ${d.status}` : ""}`;
|
|
2237
|
+
case "console":
|
|
2238
|
+
case "console-log":
|
|
2239
|
+
return `${d.level ?? "log"}: ${(d.message ?? "").toString().slice(0, 120)}`;
|
|
2240
|
+
case "shell":
|
|
2241
|
+
return `${d.event ?? d.type ?? d.phase ?? ""}`;
|
|
2242
|
+
case "scroll":
|
|
2243
|
+
return `${d.phase ?? "?"} ${d.target ?? ""}`;
|
|
2244
|
+
case "gesture":
|
|
2245
|
+
return `${d.phase ?? "?"} ${d.type ?? ""}`;
|
|
2246
|
+
case "text-input":
|
|
2247
|
+
return `${d.phase ?? "?"}${d.value !== void 0 ? ` "${String(d.value).slice(0, 40)}"` : ""}`;
|
|
2248
|
+
case "layout":
|
|
2249
|
+
return `${d.kind ?? "?"} ${d.testID ?? d.type ?? ""}${d.skipped ? ` skipped:${d.reason ?? "unknown"}` : ""}`;
|
|
2250
|
+
case "react-commit": {
|
|
2251
|
+
const slowest = d.slowest;
|
|
2252
|
+
return `${d.fiberCount ?? "?"} fibers ${d.durationMs ?? "?"}ms${slowest?.displayName ? ` \xB7 ${slowest.displayName} ${slowest.durationMs ?? "?"}ms` : ""}`;
|
|
2253
|
+
}
|
|
2254
|
+
case "frame":
|
|
2255
|
+
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` : ""}`;
|
|
2256
|
+
case "reanimated":
|
|
2257
|
+
case "animation":
|
|
2258
|
+
return `${d.kind ?? ""} ${d.target ?? ""}${d.durationMs ? ` ${d.durationMs}ms` : ""}`;
|
|
2259
|
+
}
|
|
2260
|
+
return "";
|
|
2261
|
+
}
|
|
2262
|
+
function formatTimelineEvent(event, anchor) {
|
|
2263
|
+
const ts = formatTimelineRelativeTime(event.t, anchor).padStart(8);
|
|
2264
|
+
const ctx = event.context.padEnd(6);
|
|
2265
|
+
const kind = `[${event.kind}]`.padEnd(15);
|
|
2266
|
+
const d = event.data;
|
|
2267
|
+
const payload = d && typeof d === "object" ? formatTimelinePayload(event.kind, d) : "";
|
|
2268
|
+
return ` ${ts} ${ctx} ${kind} ${payload}`;
|
|
2269
|
+
}
|
|
2270
|
+
function formatTimelineEvents(events) {
|
|
2271
|
+
if (!events.length) return "";
|
|
2272
|
+
const anchor = events[0]?.t ?? null;
|
|
2273
|
+
return events.map((e) => formatTimelineEvent(e, anchor)).join("\n");
|
|
2274
|
+
}
|
|
2143
2275
|
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
nodeId: target.nodeId ?? null,
|
|
2177
|
-
id: target.id,
|
|
2178
|
-
testID: target.testID,
|
|
2179
|
-
text:
|
|
2180
|
-
target.text ??
|
|
2181
|
-
target.accessibilityLabel ??
|
|
2182
|
-
n.text ??
|
|
2183
|
-
n.accessibilityLabel ??
|
|
2184
|
-
null,
|
|
2185
|
-
type: target.type,
|
|
2186
|
-
},
|
|
2187
|
-
strategy: (resolved && resolved.strategy) || 'matched-node',
|
|
2188
|
-
total,
|
|
2189
|
-
idx,
|
|
2190
|
-
}
|
|
2191
|
-
})()`
|
|
2192
|
-
})
|
|
2276
|
+
// cli/commands/inspect/resolve-target.ts
|
|
2277
|
+
async function resolveTargetCoords(bridge, target) {
|
|
2278
|
+
const arg = JSON.stringify(target.value);
|
|
2279
|
+
const findExpr = target.mode === "testid" ? `(await t.findByTestId(${arg})) || (await t.findById(${arg}))` : `await t.findByText(${arg})`;
|
|
2280
|
+
const result = await bridge.send({
|
|
2281
|
+
type: "evaluate",
|
|
2282
|
+
code: `(async () => {
|
|
2283
|
+
const t = window.__sootsimTest
|
|
2284
|
+
if (!t) return null
|
|
2285
|
+
const n = ${findExpr}
|
|
2286
|
+
if (!n || !n.absolutePosition || !n.layout) return null
|
|
2287
|
+
const resolved =
|
|
2288
|
+
typeof n.nodeId === 'number' && typeof t.resolveTapTarget === 'function'
|
|
2289
|
+
? await t.resolveTapTarget(n.nodeId)
|
|
2290
|
+
: null
|
|
2291
|
+
const cx =
|
|
2292
|
+
typeof resolved?.cx === 'number'
|
|
2293
|
+
? resolved.cx
|
|
2294
|
+
: n.absolutePosition.x + (n.layout.width || 0) / 2
|
|
2295
|
+
const cy =
|
|
2296
|
+
typeof resolved?.cy === 'number'
|
|
2297
|
+
? resolved.cy
|
|
2298
|
+
: n.absolutePosition.y + (n.layout.height || 0) / 2
|
|
2299
|
+
return {
|
|
2300
|
+
x: cx,
|
|
2301
|
+
y: cy,
|
|
2302
|
+
id: n.id ?? null,
|
|
2303
|
+
testID: n.testID ?? null,
|
|
2304
|
+
text: ${JSON.stringify(target.mode === "text")} ? ${arg} : (n.text ?? n.accessibilityLabel ?? null),
|
|
2305
|
+
type: n.type ?? null,
|
|
2306
|
+
}
|
|
2307
|
+
})()`
|
|
2193
2308
|
});
|
|
2309
|
+
return result ?? null;
|
|
2194
2310
|
}
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2311
|
+
|
|
2312
|
+
// src/sim-client.ts
|
|
2313
|
+
var SimClientError = class extends Error {
|
|
2314
|
+
constructor(message) {
|
|
2315
|
+
super(message);
|
|
2316
|
+
this.name = new.target.name;
|
|
2317
|
+
}
|
|
2318
|
+
};
|
|
2319
|
+
var SimConnectError = class extends SimClientError {
|
|
2320
|
+
};
|
|
2321
|
+
var SimReadyTimeoutError = class extends SimClientError {
|
|
2322
|
+
status;
|
|
2323
|
+
constructor(status) {
|
|
2324
|
+
super(`sim not ready: ${waitReadyReason(status)}`);
|
|
2325
|
+
this.status = status;
|
|
2326
|
+
}
|
|
2327
|
+
};
|
|
2328
|
+
var SimTapError = class extends SimClientError {
|
|
2329
|
+
outcome;
|
|
2330
|
+
constructor(message, outcome = null) {
|
|
2331
|
+
super(message);
|
|
2332
|
+
this.outcome = outcome;
|
|
2333
|
+
}
|
|
2334
|
+
};
|
|
2335
|
+
var SimKeyboardError = class extends SimClientError {
|
|
2336
|
+
};
|
|
2337
|
+
var SimPerformError = class extends SimClientError {
|
|
2338
|
+
result;
|
|
2339
|
+
constructor(result) {
|
|
2340
|
+
const failed = result.steps.find((step) => !step.ok);
|
|
2341
|
+
const at = failed ? `${failed.index} (${failed.type})` : String(result.completed);
|
|
2342
|
+
const cause = failed?.error ?? result.error;
|
|
2343
|
+
super(`perform failed at step ${at}${cause ? `: ${cause}` : ""}`);
|
|
2344
|
+
this.result = result;
|
|
2345
|
+
}
|
|
2346
|
+
};
|
|
2347
|
+
var SimResetError = class extends SimClientError {
|
|
2348
|
+
result;
|
|
2349
|
+
constructor(result) {
|
|
2350
|
+
super(`reset (${result.strategy}) failed${result.error ? `: ${result.error}` : ""}`);
|
|
2351
|
+
this.result = result;
|
|
2352
|
+
}
|
|
2353
|
+
};
|
|
2354
|
+
function sleep(ms) {
|
|
2355
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2356
|
+
}
|
|
2357
|
+
function decodePngDataUrl(dataUrl) {
|
|
2358
|
+
if (typeof dataUrl !== "string" || !dataUrl.startsWith("data:image/png;base64,")) {
|
|
2359
|
+
throw new SimClientError(
|
|
2360
|
+
`screenshot bridge returned a non-png payload: ${JSON.stringify(
|
|
2361
|
+
typeof dataUrl === "string" ? dataUrl.slice(0, 80) : dataUrl
|
|
2362
|
+
)}`
|
|
2363
|
+
);
|
|
2364
|
+
}
|
|
2365
|
+
const base64 = dataUrl.slice("data:image/png;base64,".length);
|
|
2366
|
+
const binary = atob(base64);
|
|
2367
|
+
const bytes = new Uint8Array(binary.length);
|
|
2368
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
2369
|
+
return { dataUrl, bytes };
|
|
2370
|
+
}
|
|
2371
|
+
var TARGET_SEARCH_PARAMS = [
|
|
2372
|
+
"bundle",
|
|
2373
|
+
"demo",
|
|
2374
|
+
"app",
|
|
2375
|
+
"open",
|
|
2376
|
+
"port",
|
|
2377
|
+
"inspectOpen",
|
|
2378
|
+
"appFonts",
|
|
2379
|
+
"appSplash"
|
|
2380
|
+
];
|
|
2381
|
+
function buildSimOpenUrl(currentUrl, target) {
|
|
2382
|
+
const url = new URL(currentUrl);
|
|
2383
|
+
for (const param of TARGET_SEARCH_PARAMS) url.searchParams.delete(param);
|
|
2384
|
+
const pathname = url.pathname.replace(/\/+$/, "") || "/";
|
|
2385
|
+
const prefix = pathname === "/__soot" || pathname.startsWith("/__soot/") ? "/__soot" : "";
|
|
2386
|
+
const normalized = String(target).trim();
|
|
2387
|
+
if (/^\d+$/.test(normalized)) {
|
|
2388
|
+
url.pathname = `${prefix}/rn/${normalized}`;
|
|
2389
|
+
} else {
|
|
2390
|
+
url.pathname = `${prefix}/rn`;
|
|
2391
|
+
url.searchParams.set("open", normalized);
|
|
2392
|
+
}
|
|
2393
|
+
return url.toString();
|
|
2394
|
+
}
|
|
2395
|
+
function isTextTapTarget(target) {
|
|
2396
|
+
return typeof target === "object" && "text" in target && typeof target.text === "string" && !("layout" in target) && !("absolutePosition" in target);
|
|
2397
|
+
}
|
|
2398
|
+
var SimClient = class {
|
|
2399
|
+
simId;
|
|
2400
|
+
bridge;
|
|
2401
|
+
closed = false;
|
|
2402
|
+
constructor(bridge, simId, _options = {}) {
|
|
2403
|
+
this.bridge = bridge;
|
|
2404
|
+
this.simId = simId;
|
|
2405
|
+
}
|
|
2406
|
+
// --- transport (WsBridge/InspectBridge) ---
|
|
2407
|
+
send(cmd, opts) {
|
|
2408
|
+
if (this.closed) {
|
|
2409
|
+
return Promise.reject(new SimConnectError("sim client is closed"));
|
|
2410
|
+
}
|
|
2411
|
+
const payload = cmd.simId === void 0 ? { ...cmd, simId: this.simId } : cmd;
|
|
2412
|
+
return this.bridge.send(payload, opts);
|
|
2413
|
+
}
|
|
2414
|
+
listSims() {
|
|
2415
|
+
return this.bridge.listSims();
|
|
2416
|
+
}
|
|
2417
|
+
focusSim(simId = this.simId) {
|
|
2418
|
+
return this.bridge.focusSim(simId);
|
|
2419
|
+
}
|
|
2420
|
+
closeSim(simId = this.simId) {
|
|
2421
|
+
return this.bridge.closeSim(simId);
|
|
2422
|
+
}
|
|
2423
|
+
claim(simId = this.simId, opts) {
|
|
2424
|
+
return this.bridge.claim(simId, opts);
|
|
2425
|
+
}
|
|
2426
|
+
/** release the socket. a script that connected must close to exit cleanly. */
|
|
2427
|
+
close() {
|
|
2428
|
+
if (this.closed) return;
|
|
2429
|
+
this.closed = true;
|
|
2430
|
+
this.bridge.close();
|
|
2431
|
+
}
|
|
2432
|
+
// --- navigation + readiness ---
|
|
2433
|
+
/**
|
|
2434
|
+
* point this sim at a dev target: a metro port (`8081`), or anything the
|
|
2435
|
+
* shell's connect flow accepts (`localhost:8081`, an app slug…). resolves
|
|
2436
|
+
* once the reloaded page answers on the new target route.
|
|
2437
|
+
*/
|
|
2438
|
+
async open(target, opts = {}) {
|
|
2439
|
+
const sims = await this.listSims();
|
|
2440
|
+
const sim = sims.find((entry) => entry.id === this.simId);
|
|
2441
|
+
if (!sim) {
|
|
2442
|
+
throw new SimConnectError(`sim ${this.simId} is no longer connected`);
|
|
2443
|
+
}
|
|
2444
|
+
const currentUrl = sim.url ?? sim.origin;
|
|
2445
|
+
if (!currentUrl) {
|
|
2446
|
+
throw new SimConnectError(
|
|
2447
|
+
`sim ${this.simId} reported no URL to derive the shell from`
|
|
2448
|
+
);
|
|
2449
|
+
}
|
|
2450
|
+
const nextUrl = buildSimOpenUrl(currentUrl, target);
|
|
2451
|
+
const expected = new URL(nextUrl);
|
|
2452
|
+
await this.send({
|
|
2453
|
+
type: "evaluate",
|
|
2454
|
+
code: `window.location.href = ${JSON.stringify(nextUrl)}`
|
|
2455
|
+
});
|
|
2456
|
+
const deadline = Date.now() + (opts.timeoutMs ?? 3e4);
|
|
2457
|
+
while (Date.now() < deadline) {
|
|
2458
|
+
await sleep(250);
|
|
2459
|
+
try {
|
|
2460
|
+
const { url } = await inspectUrl(this);
|
|
2461
|
+
const current = new URL(url);
|
|
2462
|
+
if (current.pathname === expected.pathname && current.searchParams.get("open") === expected.searchParams.get("open")) {
|
|
2463
|
+
return;
|
|
2464
|
+
}
|
|
2465
|
+
} catch {
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
throw new SimConnectError(
|
|
2469
|
+
`timed out waiting for sim ${this.simId} to load ${nextUrl}`
|
|
2470
|
+
);
|
|
2471
|
+
}
|
|
2472
|
+
/**
|
|
2473
|
+
* block until the guest app has mounted and painted real content.
|
|
2474
|
+
* throws SimReadyTimeoutError (with the last probe status) on timeout.
|
|
2475
|
+
*/
|
|
2476
|
+
async waitReady(opts = {}) {
|
|
2477
|
+
const status = await inspectWaitReady(this, opts.timeoutMs ?? 2e4, {
|
|
2478
|
+
onProgress: opts.onProgress
|
|
2479
|
+
});
|
|
2480
|
+
if (!status.ready) throw new SimReadyTimeoutError(status);
|
|
2481
|
+
return status;
|
|
2482
|
+
}
|
|
2483
|
+
/** wait for layout/animation/transition settle after an interaction. */
|
|
2484
|
+
settle(opts = {}) {
|
|
2485
|
+
return waitForSootsimIdle({
|
|
2486
|
+
bridge: this,
|
|
2487
|
+
maxMs: opts.maxMs ?? 3e3,
|
|
2488
|
+
strict: opts.strict
|
|
2489
|
+
});
|
|
2490
|
+
}
|
|
2491
|
+
// --- read verbs ---
|
|
2492
|
+
/** first matching node, or null. query precedence matches the CLI's find. */
|
|
2493
|
+
async find(query) {
|
|
2494
|
+
const nodes = await this.findAll(query);
|
|
2495
|
+
return nodes[0] ?? null;
|
|
2496
|
+
}
|
|
2497
|
+
async findAll(query) {
|
|
2498
|
+
const mapped = {
|
|
2499
|
+
testId: query.testId ?? null,
|
|
2500
|
+
role: query.role ?? null,
|
|
2501
|
+
type: query.type ?? null,
|
|
2502
|
+
text: query.text ?? null,
|
|
2503
|
+
pressable: query.pressable
|
|
2504
|
+
};
|
|
2505
|
+
const found = await inspectFind(this, mapped);
|
|
2506
|
+
if (!found) {
|
|
2507
|
+
throw new SimClientError(
|
|
2508
|
+
"find() needs at least one of testId, text, role, type, or pressable"
|
|
2509
|
+
);
|
|
2510
|
+
}
|
|
2511
|
+
const { result } = found;
|
|
2512
|
+
if (Array.isArray(result)) return result;
|
|
2513
|
+
return result ? [result] : [];
|
|
2514
|
+
}
|
|
2515
|
+
async tree(depth = 5) {
|
|
2516
|
+
const { tree } = await inspectTree(this, depth);
|
|
2517
|
+
return tree;
|
|
2518
|
+
}
|
|
2519
|
+
/** compact visible-UI dump with shell + keyboard state, as the CLI prints. */
|
|
2520
|
+
describe(opts = {}) {
|
|
2521
|
+
return inspectDescribe(this, {
|
|
2522
|
+
describe: true,
|
|
2523
|
+
verbose: opts.verbose ?? false,
|
|
2524
|
+
filter: opts.filter ?? "",
|
|
2525
|
+
compact: !opts.verbose,
|
|
2526
|
+
hideXy: false,
|
|
2527
|
+
fullText: true
|
|
2528
|
+
});
|
|
2529
|
+
}
|
|
2530
|
+
accessibilityTree() {
|
|
2531
|
+
return inspectAccessibilityTree(this);
|
|
2532
|
+
}
|
|
2533
|
+
async logs(opts = {}) {
|
|
2534
|
+
return filterLogEntries(await inspectLogs(this), opts);
|
|
2535
|
+
}
|
|
2536
|
+
errors(limit = 20) {
|
|
2537
|
+
return inspectErrors(this, limit);
|
|
2538
|
+
}
|
|
2539
|
+
warnings(limit = 20) {
|
|
2540
|
+
return inspectWarnings(this, limit);
|
|
2541
|
+
}
|
|
2542
|
+
keyboard() {
|
|
2543
|
+
return inspectKeyboard(this);
|
|
2544
|
+
}
|
|
2545
|
+
shellState(readyTimeoutMs = 0) {
|
|
2546
|
+
return getShellState(this, readyTimeoutMs);
|
|
2547
|
+
}
|
|
2548
|
+
async url() {
|
|
2549
|
+
const { url } = await inspectUrl(this);
|
|
2550
|
+
return url;
|
|
2551
|
+
}
|
|
2552
|
+
async screenshot(opts = {}) {
|
|
2553
|
+
const result = await this.send({
|
|
2554
|
+
type: "screenshot",
|
|
2555
|
+
...opts.layers ? { layers: opts.layers } : {},
|
|
2556
|
+
...opts.crop ? { crop: opts.crop } : {}
|
|
2557
|
+
});
|
|
2558
|
+
return decodePngDataUrl(result);
|
|
2559
|
+
}
|
|
2560
|
+
// --- input verbs ---
|
|
2561
|
+
/**
|
|
2562
|
+
* tap by testID, visible text, node, or coordinates. resolution, retry, and
|
|
2563
|
+
* hit verification are the CLI's shared tap kernels. throws SimTapError when
|
|
2564
|
+
* the target is missing, covered, offscreen, or the press missed.
|
|
2565
|
+
*/
|
|
2566
|
+
async tap(target) {
|
|
2567
|
+
if (typeof target === "string") {
|
|
2568
|
+
return this.throwUnlessTapped(await tapBest(this, target), target);
|
|
2569
|
+
}
|
|
2570
|
+
if ("testId" in target && typeof target.testId === "string") {
|
|
2571
|
+
return this.throwUnlessTapped(await tapById(this, target.testId), target.testId);
|
|
2572
|
+
}
|
|
2573
|
+
if (isTextTapTarget(target)) {
|
|
2574
|
+
const { text, ...textOpts } = target;
|
|
2575
|
+
return this.throwUnlessTapped(await tapByText(this, text, textOpts), text);
|
|
2576
|
+
}
|
|
2577
|
+
if ("testID" in target && typeof target.testID === "string") {
|
|
2578
|
+
return this.throwUnlessTapped(await tapById(this, target.testID), target.testID);
|
|
2579
|
+
}
|
|
2580
|
+
const coords = this.coordsForTapTarget(target);
|
|
2581
|
+
const result = await this.send({ type: "tap", x: coords.x, y: coords.y });
|
|
2582
|
+
if (!isTapSuccess(result)) {
|
|
2583
|
+
throw new SimTapError(
|
|
2584
|
+
`tap at ${coords.x},${coords.y} missed: ${JSON.stringify(result)}`
|
|
2585
|
+
);
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
async longPress(target, opts = {}) {
|
|
2589
|
+
const coords = await this.resolveCoords(target);
|
|
2590
|
+
const result = await this.send(
|
|
2591
|
+
{
|
|
2592
|
+
type: "longPress",
|
|
2593
|
+
x: coords.x,
|
|
2594
|
+
y: coords.y,
|
|
2595
|
+
durationMs: opts.durationMs ?? 600
|
|
2596
|
+
},
|
|
2597
|
+
{ timeoutMs: (opts.durationMs ?? 600) + 15e3 }
|
|
2598
|
+
);
|
|
2599
|
+
if (!result?.ok) {
|
|
2600
|
+
throw new SimTapError(`longPress at ${coords.x},${coords.y} missed`);
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
/** type through the visual iOS keyboard. requires a focused input. */
|
|
2604
|
+
async type(text) {
|
|
2605
|
+
await this.requireVisibleKeyboard("type");
|
|
2606
|
+
await this.send({ type: "keyboard", action: "type", text });
|
|
2607
|
+
}
|
|
2608
|
+
/** press a keyboard key ('return', 'delete', 'space', …). */
|
|
2609
|
+
async press(key) {
|
|
2610
|
+
await this.requireVisibleKeyboard("press");
|
|
2611
|
+
await this.send({ type: "keyboard", action: "press", text: key });
|
|
2612
|
+
}
|
|
2613
|
+
async dismissKeyboard() {
|
|
2614
|
+
await this.send({ type: "keyboard", action: "dismiss" });
|
|
2615
|
+
}
|
|
2616
|
+
// --- batched input, compound state, reset ---
|
|
2617
|
+
/**
|
|
2618
|
+
* execute an ordered step batch inside the page in ONE round-trip, so waits
|
|
2619
|
+
* and gesture physics run on real wall-clock timing. `tapText` steps are
|
|
2620
|
+
* resolved to coordinates client-side before the batch is sent. throws
|
|
2621
|
+
* SimPerformError (carrying the per-step result) when the batch fails.
|
|
2622
|
+
*/
|
|
2623
|
+
async perform(steps, options) {
|
|
2624
|
+
const resolved = [];
|
|
2625
|
+
for (const step of steps) {
|
|
2626
|
+
if (step.type !== "tapText") {
|
|
2627
|
+
resolved.push(step);
|
|
2628
|
+
continue;
|
|
2629
|
+
}
|
|
2630
|
+
const target = await resolveTargetCoords(this, {
|
|
2631
|
+
mode: "text",
|
|
2632
|
+
value: step.text
|
|
2242
2633
|
});
|
|
2243
|
-
if (!
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
cy,
|
|
2253
|
-
...offscreen ? { offscreen: true, screen } : {},
|
|
2634
|
+
if (!target) {
|
|
2635
|
+
throw new SimTapError(
|
|
2636
|
+
`perform tapText: no node with text ${JSON.stringify(step.text)}`
|
|
2637
|
+
);
|
|
2638
|
+
}
|
|
2639
|
+
resolved.push({
|
|
2640
|
+
type: "tap",
|
|
2641
|
+
x: target.x,
|
|
2642
|
+
y: target.y,
|
|
2254
2643
|
target: {
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
type: node.type
|
|
2644
|
+
testID: target.testID ?? void 0,
|
|
2645
|
+
text: target.text ?? void 0,
|
|
2646
|
+
type: target.type ?? void 0
|
|
2259
2647
|
}
|
|
2648
|
+
});
|
|
2649
|
+
}
|
|
2650
|
+
const batchTimeoutMs = options?.timeoutMs ?? 3e4;
|
|
2651
|
+
const result = await this.send(
|
|
2652
|
+
{
|
|
2653
|
+
type: "perform",
|
|
2654
|
+
steps: resolved,
|
|
2655
|
+
...options ? { performOptions: options } : {}
|
|
2656
|
+
},
|
|
2657
|
+
{ timeoutMs: batchTimeoutMs + 5e3 }
|
|
2658
|
+
);
|
|
2659
|
+
if (!result?.ok) throw new SimPerformError(result);
|
|
2660
|
+
return result;
|
|
2661
|
+
}
|
|
2662
|
+
/** screenshot + tree + route + recent errors in one round-trip. */
|
|
2663
|
+
state(options) {
|
|
2664
|
+
return this.send({
|
|
2665
|
+
type: "state",
|
|
2666
|
+
...options ? { stateOptions: options } : {}
|
|
2667
|
+
});
|
|
2668
|
+
}
|
|
2669
|
+
/**
|
|
2670
|
+
* two-tier app state wipe ('data' clears storage/caches, 'full' also clears
|
|
2671
|
+
* keychain/permissions/native-module state). throws SimResetError on failure.
|
|
2672
|
+
*/
|
|
2673
|
+
async reset(options) {
|
|
2674
|
+
const result = await this.send(
|
|
2675
|
+
{ type: "reset", ...options ? { resetOptions: options } : {} },
|
|
2676
|
+
{ timeoutMs: 6e4 }
|
|
2677
|
+
);
|
|
2678
|
+
if (!result?.ok) throw new SimResetError(result);
|
|
2679
|
+
return result;
|
|
2680
|
+
}
|
|
2681
|
+
// --- internals ---
|
|
2682
|
+
throwUnlessTapped(outcome, label) {
|
|
2683
|
+
if (!outcome.failure && isTapSuccess(outcome.result)) return;
|
|
2684
|
+
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";
|
|
2685
|
+
throw new SimTapError(`tap ${JSON.stringify(label)} failed: ${reason}`, outcome);
|
|
2686
|
+
}
|
|
2687
|
+
coordsForTapTarget(target) {
|
|
2688
|
+
if (typeof target === "object" && "x" in target && "y" in target && typeof target.x === "number" && typeof target.y === "number") {
|
|
2689
|
+
return { x: target.x, y: target.y };
|
|
2690
|
+
}
|
|
2691
|
+
if (typeof target === "object" && "absolutePosition" in target && target.absolutePosition && target.layout) {
|
|
2692
|
+
return {
|
|
2693
|
+
x: target.absolutePosition.x + (target.layout.width ?? 0) / 2,
|
|
2694
|
+
y: target.absolutePosition.y + (target.layout.height ?? 0) / 2
|
|
2260
2695
|
};
|
|
2261
2696
|
}
|
|
2262
|
-
|
|
2263
|
-
|
|
2697
|
+
throw new SimClientError(
|
|
2698
|
+
"tap target needs coordinates, a testId, text, or a node from find()"
|
|
2699
|
+
);
|
|
2700
|
+
}
|
|
2701
|
+
async resolveCoords(target) {
|
|
2702
|
+
if (typeof target === "string") {
|
|
2703
|
+
const resolved = await resolveTargetCoords(this, { mode: "testid", value: target }) ?? await resolveTargetCoords(this, { mode: "text", value: target });
|
|
2704
|
+
if (!resolved) throw new SimTapError(`no node matching ${JSON.stringify(target)}`);
|
|
2705
|
+
return resolved;
|
|
2706
|
+
}
|
|
2707
|
+
if ("testId" in target && typeof target.testId === "string") {
|
|
2708
|
+
const resolved = await resolveTargetCoords(this, {
|
|
2709
|
+
mode: "testid",
|
|
2710
|
+
value: target.testId
|
|
2711
|
+
});
|
|
2712
|
+
if (!resolved) {
|
|
2713
|
+
throw new SimTapError(`no node with testID ${JSON.stringify(target.testId)}`);
|
|
2714
|
+
}
|
|
2715
|
+
return resolved;
|
|
2716
|
+
}
|
|
2717
|
+
if ("text" in target && typeof target.text === "string" && !("layout" in target)) {
|
|
2718
|
+
const resolved = await resolveTargetCoords(this, {
|
|
2719
|
+
mode: "text",
|
|
2720
|
+
value: target.text
|
|
2721
|
+
});
|
|
2722
|
+
if (!resolved) {
|
|
2723
|
+
throw new SimTapError(`no node with text ${JSON.stringify(target.text)}`);
|
|
2724
|
+
}
|
|
2725
|
+
return resolved;
|
|
2726
|
+
}
|
|
2727
|
+
return this.coordsForTapTarget(target);
|
|
2728
|
+
}
|
|
2729
|
+
async requireVisibleKeyboard(action) {
|
|
2730
|
+
const deadline = Date.now() + 1500;
|
|
2731
|
+
for (; ; ) {
|
|
2732
|
+
const state = await inspectKeyboard(this);
|
|
2733
|
+
if (!("error" in state) && state.visible) return;
|
|
2734
|
+
if (Date.now() >= deadline) {
|
|
2735
|
+
throw new SimKeyboardError(
|
|
2736
|
+
`${action} requires the iOS keyboard to be visible \u2014 focus an input first (tap a TextInput)`
|
|
2737
|
+
);
|
|
2738
|
+
}
|
|
2739
|
+
await sleep(150);
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2742
|
+
};
|
|
2264
2743
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2265
2744
|
0 && (module.exports = {
|
|
2266
2745
|
DEBUG_CHANNELS,
|
|
@@ -2270,8 +2749,17 @@ async function tapBest(bridge, query, opts = {}) {
|
|
|
2270
2749
|
READY_CONTENT_NODE_FLOOR,
|
|
2271
2750
|
READY_NODE_STABLE_MS,
|
|
2272
2751
|
READY_PROBE_POLL_MS,
|
|
2752
|
+
SimClient,
|
|
2753
|
+
SimClientError,
|
|
2754
|
+
SimConnectError,
|
|
2755
|
+
SimKeyboardError,
|
|
2756
|
+
SimPerformError,
|
|
2757
|
+
SimReadyTimeoutError,
|
|
2758
|
+
SimResetError,
|
|
2759
|
+
SimTapError,
|
|
2273
2760
|
WAIT_READY_PROBE,
|
|
2274
2761
|
WAIT_READY_PROGRESS_INTERVAL_MS,
|
|
2762
|
+
buildSimOpenUrl,
|
|
2275
2763
|
clearConsole,
|
|
2276
2764
|
clearLogs,
|
|
2277
2765
|
clearRequests,
|