peachsim 0.1.527
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +640 -0
- package/cli/app-config.ts +82 -0
- package/cli/app-fonts.ts +388 -0
- package/cli/app-project.ts +59 -0
- package/cli/app-splash.ts +171 -0
- package/cli/app-state-reset.ts +26 -0
- package/cli/auth.ts +184 -0
- package/cli/bin.ts +59 -0
- package/cli/bridge-diagnostics.ts +226 -0
- package/cli/bridge-flow-runner.ts +2806 -0
- package/cli/browser-backend.ts +44 -0
- package/cli/browser-evals.ts +96 -0
- package/cli/cli-update.ts +261 -0
- package/cli/cloud-client.ts +1530 -0
- package/cli/cloud-dispatch.ts +206 -0
- package/cli/cloud-session.ts +227 -0
- package/cli/command-contract.ts +221 -0
- package/cli/command-registry.ts +304 -0
- package/cli/commands/agent-wrapper.ts +1048 -0
- package/cli/commands/agent.ts +439 -0
- package/cli/commands/app-fonts.ts +99 -0
- package/cli/commands/assert.ts +542 -0
- package/cli/commands/auth.ts +71 -0
- package/cli/commands/box/bundle-plane.ts +71 -0
- package/cli/commands/box/checkout-plane.ts +256 -0
- package/cli/commands/box/plane-share.ts +208 -0
- package/cli/commands/box/rnx-command.ts +60 -0
- package/cli/commands/box/search-command.ts +157 -0
- package/cli/commands/box.ts +925 -0
- package/cli/commands/camera.ts +266 -0
- package/cli/commands/cleanup.ts +169 -0
- package/cli/commands/compat.ts +176 -0
- package/cli/commands/config.ts +34 -0
- package/cli/commands/control.ts +2154 -0
- package/cli/commands/cpu-profile.ts +941 -0
- package/cli/commands/daemon-mac-app.ts +165 -0
- package/cli/commands/daemon.ts +800 -0
- package/cli/commands/debug.ts +751 -0
- package/cli/commands/desktop.ts +39 -0
- package/cli/commands/detect.ts +201 -0
- package/cli/commands/detox.ts +481 -0
- package/cli/commands/device.ts +134 -0
- package/cli/commands/diagnose.ts +589 -0
- package/cli/commands/do-chain.ts +408 -0
- package/cli/commands/electron.ts +94 -0
- package/cli/commands/film.ts +379 -0
- package/cli/commands/flow.ts +1240 -0
- package/cli/commands/inspect/actions.ts +516 -0
- package/cli/commands/inspect/core.ts +2086 -0
- package/cli/commands/inspect/count.ts +17 -0
- package/cli/commands/inspect/describe.ts +119 -0
- package/cli/commands/inspect/env.ts +25 -0
- package/cli/commands/inspect/find.ts +169 -0
- package/cli/commands/inspect/get-layout.ts +53 -0
- package/cli/commands/inspect/keyboard.ts +54 -0
- package/cli/commands/inspect/list.ts +175 -0
- package/cli/commands/inspect/memory.ts +215 -0
- package/cli/commands/inspect/redaction.ts +39 -0
- package/cli/commands/inspect/resolve-target.ts +77 -0
- package/cli/commands/inspect/screens.ts +78 -0
- package/cli/commands/inspect/settle.ts +32 -0
- package/cli/commands/inspect/settling.ts +197 -0
- package/cli/commands/inspect/shared.ts +259 -0
- package/cli/commands/inspect/sleep.ts +15 -0
- package/cli/commands/inspect/tree.ts +23 -0
- package/cli/commands/inspect/url.ts +17 -0
- package/cli/commands/inspect/wait-event.ts +211 -0
- package/cli/commands/inspect/wait-idle.ts +25 -0
- package/cli/commands/inspect/wait-ready.ts +87 -0
- package/cli/commands/inspect/wait-selector.ts +59 -0
- package/cli/commands/inspect/wait.ts +32 -0
- package/cli/commands/inspect.ts +4738 -0
- package/cli/commands/install-cli.ts +275 -0
- package/cli/commands/install-desktop.ts +385 -0
- package/cli/commands/login.ts +357 -0
- package/cli/commands/logout.ts +24 -0
- package/cli/commands/maestro-generate.ts +375 -0
- package/cli/commands/maestro.ts +536 -0
- package/cli/commands/mode.ts +58 -0
- package/cli/commands/no-bridge-hint.ts +80 -0
- package/cli/commands/perf.ts +67 -0
- package/cli/commands/permissions.ts +203 -0
- package/cli/commands/platform.ts +410 -0
- package/cli/commands/preview.ts +103 -0
- package/cli/commands/profile.ts +106 -0
- package/cli/commands/react.ts +353 -0
- package/cli/commands/record.ts +1435 -0
- package/cli/commands/report-issue.ts +305 -0
- package/cli/commands/reset.ts +89 -0
- package/cli/commands/runtime.ts +348 -0
- package/cli/commands/screenshot-command.ts +106 -0
- package/cli/commands/screenshot-layers.ts +144 -0
- package/cli/commands/screenshot-mode.ts +38 -0
- package/cli/commands/screenshot.ts +492 -0
- package/cli/commands/screenshots-capture.ts +608 -0
- package/cli/commands/screenshots.ts +127 -0
- package/cli/commands/serve.ts +139 -0
- package/cli/commands/setup.ts +416 -0
- package/cli/commands/shell-boolean-mode.ts +82 -0
- package/cli/commands/skills.ts +468 -0
- package/cli/commands/slides.ts +357 -0
- package/cli/commands/state.ts +94 -0
- package/cli/commands/storage.ts +58 -0
- package/cli/commands/telemetry.ts +55 -0
- package/cli/commands/test.ts +535 -0
- package/cli/commands/three-mode.ts +771 -0
- package/cli/commands/timeline.ts +121 -0
- package/cli/commands/upgrade.ts +167 -0
- package/cli/commands/upload.ts +1203 -0
- package/cli/commands/version.ts +63 -0
- package/cli/commands/welcome.ts +17 -0
- package/cli/commands/what-happened.ts +328 -0
- package/cli/current-sim.ts +217 -0
- package/cli/desktop-companion.ts +315 -0
- package/cli/drivers/electron.ts +70 -0
- package/cli/drivers/index.ts +20 -0
- package/cli/drivers/playwright-provisioning.ts +184 -0
- package/cli/drivers/playwright-sim-host.ts +537 -0
- package/cli/drivers/playwright.ts +209 -0
- package/cli/drivers/registry.ts +65 -0
- package/cli/drivers/types.ts +102 -0
- package/cli/flow-export.ts +55 -0
- package/cli/flow-file.ts +142 -0
- package/cli/flow-live-status.ts +126 -0
- package/cli/flow-session.ts +140 -0
- package/cli/help-core.ts +143 -0
- package/cli/help.ts +44 -0
- package/cli/hidden-runtime-alias.ts +19 -0
- package/cli/hints.ts +214 -0
- package/cli/inspect-notice-state.ts +114 -0
- package/cli/internal-child.ts +176 -0
- package/cli/maestro-js.ts +323 -0
- package/cli/main.ts +704 -0
- package/cli/open-url.ts +8 -0
- package/cli/outbound-endpoints.ts +830 -0
- package/cli/parent-pid.ts +204 -0
- package/cli/parse-args.ts +189 -0
- package/cli/privacy.ts +53 -0
- package/cli/prompt.ts +56 -0
- package/cli/recording-access.ts +108 -0
- package/cli/registry.ts +2 -0
- package/cli/resolve-assets.ts +63 -0
- package/cli/rnx-in-process.ts +98 -0
- package/cli/run-registry.ts +226 -0
- package/cli/run-rnx-core.ts +148 -0
- package/cli/run-rnx.ts +137 -0
- package/cli/runtime-notes.ts +66 -0
- package/cli/runtime-summary.ts +25 -0
- package/cli/secure-file.ts +31 -0
- package/cli/self-invocation.ts +58 -0
- package/cli/send-to-box.ts +109 -0
- package/cli/setup-repository.ts +191 -0
- package/cli/shell-init.ts +98 -0
- package/cli/standalone.ts +4 -0
- package/cli/telemetry.ts +84 -0
- package/cli/ui.ts +68 -0
- package/cli/ws-bridge.ts +1020 -0
- package/detox/attempt-events.cjs +23 -0
- package/detox/colors.ts +54 -0
- package/detox/config-loader.ts +135 -0
- package/detox/element-types.ts +48 -0
- package/detox/expectations.ts +534 -0
- package/detox/gestures.ts +501 -0
- package/detox/index.ts +2694 -0
- package/detox/jest-preset.cjs +29 -0
- package/detox/jest-setup-after-env.cjs +22 -0
- package/detox/matchers.ts +30 -0
- package/detox/motion-change.cjs +109 -0
- package/detox/motion-change.d.cts +35 -0
- package/detox/navigation-lifecycle.ts +290 -0
- package/detox/navigation.ts +43 -0
- package/detox/proof-frame.cjs +114 -0
- package/detox/record-block-execution.cjs +78 -0
- package/detox/screenshots/animated-color-test-rest-norngh.png +0 -0
- package/detox/screenshots/color-test-after-drag-norngh.png +0 -0
- package/detox/screenshots/color-test-rest-norngh.png +0 -0
- package/detox/screenshots/theme-blue-toggle.png +0 -0
- package/detox/screenshots/theme-blue.png +0 -0
- package/detox/screenshots/theme-red-toggle.png +0 -0
- package/detox/screenshots/theme-red.png +0 -0
- package/dist-lib/agent-daemon-client.cjs +502 -0
- package/dist-lib/agent-events.cjs +48 -0
- package/dist-lib/agent-identity.cjs +53 -0
- package/dist-lib/agent-sessions.cjs +759 -0
- package/dist-lib/attached-projects.cjs +526 -0
- package/dist-lib/auth/shared-session.cjs +208 -0
- package/dist-lib/backend-origin.cjs +88 -0
- package/dist-lib/beta.cjs +41 -0
- package/dist-lib/beta.mjs +13 -0
- package/dist-lib/bridge-constants.cjs +161 -0
- package/dist-lib/bridge-contract-input.cjs +451 -0
- package/dist-lib/bridge-contract-input.mjs +414 -0
- package/dist-lib/bridge-contract.cjs +130 -0
- package/dist-lib/bridge-contract.mjs +94 -0
- package/dist-lib/capture-contract.cjs +220 -0
- package/dist-lib/capture-contract.mjs +190 -0
- package/dist-lib/cli-constants.cjs +32 -0
- package/dist-lib/cloud-contract.cjs +194 -0
- package/dist-lib/cloud-contract.mjs +145 -0
- package/dist-lib/cloud.cjs +670 -0
- package/dist-lib/cloud.mjs +639 -0
- package/dist-lib/config.cjs +96 -0
- package/dist-lib/detox/index.cjs +3789 -0
- package/dist-lib/detox/jest-preset.cjs +29 -0
- package/dist-lib/dev-bundle-resolution.cjs +290 -0
- package/dist-lib/home-paths.cjs +638 -0
- package/dist-lib/host/bridge-host.cjs +10903 -0
- package/dist-lib/host/fetch-proxy-handler.cjs +421 -0
- package/dist-lib/host/fetch-proxy-overrides.cjs +63 -0
- package/dist-lib/host/fetch-proxy-overrides.mjs +37 -0
- package/dist-lib/host/replacement-module-handler.cjs +469 -0
- package/dist-lib/host/websocket-proxy.cjs +298 -0
- package/dist-lib/index.cjs +4268 -0
- package/dist-lib/jump-to-source-babel.cjs +93 -0
- package/dist-lib/jump-to-source-native.cjs +112 -0
- package/dist-lib/menu.cjs +646 -0
- package/dist-lib/menu.mjs +604 -0
- package/dist-lib/metro-fingerprint-registry.cjs +186 -0
- package/dist-lib/metro-fingerprint-registry.mjs +156 -0
- package/dist-lib/metro-production-bundle.cjs +329 -0
- package/dist-lib/metro-production-bundle.mjs +295 -0
- package/dist-lib/metro.cjs +904 -0
- package/dist-lib/profiles.cjs +246 -0
- package/dist-lib/public-brand.cjs +39 -0
- package/dist-lib/react-native-host-modules.cjs +51 -0
- package/dist-lib/react-native-host-modules.mjs +25 -0
- package/dist-lib/render-mode.cjs +54 -0
- package/dist-lib/scripts/dev-server-scanner.cjs +1232 -0
- package/dist-lib/sdk.cjs +2433 -0
- package/dist-lib/sdk.mjs +2336 -0
- package/dist-lib/skills.cjs +39132 -0
- package/dist-lib/swift-submit.cjs +92 -0
- package/dist-lib/swift-submit.mjs +65 -0
- package/dist-lib/swift.cjs +1646 -0
- package/dist-lib/vite.cjs +1591 -0
- package/package.json +290 -0
- package/scripts/dev-server-scanner.ts +1291 -0
- package/scripts/optional-demo-registry.ts +113 -0
- package/skills/contrast/SKILL.md +165 -0
- package/skills/rnx-debug/SKILL.md +433 -0
- package/skills/rnx-perf/SKILL.md +399 -0
- package/skills/rnx-setup/SKILL.md +172 -0
- package/skills/rnx-test/SKILL.md +321 -0
- package/skills/rnx-visual/SKILL.md +126 -0
- package/src/agent-daemon-client.ts +410 -0
- package/src/agent-events.ts +71 -0
- package/src/agent-identity.ts +23 -0
- package/src/agent-prompt.ts +71 -0
- package/src/agent-sessions.ts +580 -0
- package/src/app-url.ts +39 -0
- package/src/attached-projects.ts +539 -0
- package/src/auth/shared-session.ts +235 -0
- package/src/backend-origin.ts +70 -0
- package/src/beta.ts +19 -0
- package/src/bridge-constants.ts +175 -0
- package/src/bridge-contract-input.ts +502 -0
- package/src/bridge-contract.ts +558 -0
- package/src/browser-cache.ts +16 -0
- package/src/capture-contract.ts +587 -0
- package/src/cli-constants.ts +1 -0
- package/src/cli-version.ts +40 -0
- package/src/cloud-contract.ts +274 -0
- package/src/cloud.ts +762 -0
- package/src/codex-client.ts +234 -0
- package/src/config.ts +132 -0
- package/src/connect.ts +101 -0
- package/src/dev-bundle-resolution.ts +346 -0
- package/src/dev-server-open.ts +198 -0
- package/src/disk-cleanup.ts +540 -0
- package/src/flow-timeout-scale.ts +15 -0
- package/src/home-paths.ts +806 -0
- package/src/host/agent-host.ts +666 -0
- package/src/host/bridge-host.ts +2788 -0
- package/src/host/fetch-proxy-handler.ts +449 -0
- package/src/host/fetch-proxy-overrides.ts +51 -0
- package/src/host/open-url.ts +89 -0
- package/src/host/plane-host.ts +203 -0
- package/src/host/replacement-module-handler.ts +158 -0
- package/src/host/websocket-proxy.ts +315 -0
- package/src/index.ts +71 -0
- package/src/jump-to-source-babel.ts +135 -0
- package/src/jump-to-source-native.ts +102 -0
- package/src/menu.ts +777 -0
- package/src/metro-fingerprint-registry.ts +302 -0
- package/src/metro-plugin.ts +341 -0
- package/src/metro-production-bundle.ts +471 -0
- package/src/native-dev-bundle-url.ts +118 -0
- package/src/native-seam-manifest.ts +341 -0
- package/src/playwright-host-log.ts +16 -0
- package/src/plist.ts +27 -0
- package/src/poll-until.ts +123 -0
- package/src/profiles.ts +179 -0
- package/src/public-brand.ts +9 -0
- package/src/react-native-host-modules.ts +26 -0
- package/src/render-mode.ts +27 -0
- package/src/rolldown-plugin-swift.ts +240 -0
- package/src/runtime-assets.ts +233 -0
- package/src/runtime-delivery.ts +54 -0
- package/src/screenshots/compose.ts +422 -0
- package/src/screenshots/frame-compose.ts +514 -0
- package/src/screenshots/orchestrate.ts +256 -0
- package/src/screenshots/org-deck.ts +91 -0
- package/src/screenshots/registry.ts +58 -0
- package/src/screenshots/schema.ts +364 -0
- package/src/sdk.ts +15 -0
- package/src/settings-bundle.ts +418 -0
- package/src/shell-bridge-identity.ts +15 -0
- package/src/sim-client.ts +697 -0
- package/src/skills/builtin/a11y-review.ts +132 -0
- package/src/skills/builtin/compat-check.ts +71 -0
- package/src/skills/builtin/maestro-test.ts +121 -0
- package/src/skills/builtin/perf-profile.ts +90 -0
- package/src/skills/builtin/screenshot-all.ts +52 -0
- package/src/skills/builtin/visual-diff.ts +100 -0
- package/src/skills/registry.ts +107 -0
- package/src/skills/types.ts +41 -0
- package/src/swift-submit.ts +185 -0
- package/src/swift.ts +7 -0
- package/src/team-machine-prompt.ts +73 -0
- package/src/vite-plugin-one.ts +264 -0
- package/src/vite-plugin-swift.ts +620 -0
- package/src/vite-plugin.ts +1946 -0
- package/src/worklets-babel.ts +250 -0
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
import { inspectTree, type InspectBridge } from './core'
|
|
2
|
+
import { resolveTargetCoords, type ResolvedTargetCoords } from './resolve-target'
|
|
3
|
+
import { waitForSootsimIdle } from './settling'
|
|
4
|
+
import type { SimSemanticNode } from '../../../src/bridge-contract'
|
|
5
|
+
import type { SootSimReplayTarget } from '@rnx/globals'
|
|
6
|
+
|
|
7
|
+
export type TapFailure = 'not-found' | 'missed' | 'special'
|
|
8
|
+
|
|
9
|
+
export type TapTargetSummary = SootSimReplayTarget
|
|
10
|
+
|
|
11
|
+
export interface TapOutcome {
|
|
12
|
+
payload: any
|
|
13
|
+
result: any
|
|
14
|
+
attempts: number
|
|
15
|
+
failure?: TapFailure
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface TapTextOptions {
|
|
19
|
+
exact?: boolean
|
|
20
|
+
role?: string | null
|
|
21
|
+
within?: string | null
|
|
22
|
+
minX?: number | null
|
|
23
|
+
maxX?: number | null
|
|
24
|
+
minY?: number | null
|
|
25
|
+
maxY?: number | null
|
|
26
|
+
near?: { x: number; y: number } | null
|
|
27
|
+
nth?: number | null
|
|
28
|
+
first?: boolean
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface TapTimingOptions {
|
|
32
|
+
initialWaitMs?: number
|
|
33
|
+
deadlineMs?: number
|
|
34
|
+
retryWaitMs?: number
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type ScreenSize = { width: number; height: number }
|
|
38
|
+
|
|
39
|
+
function isScreenSize(value: unknown): value is ScreenSize {
|
|
40
|
+
if (value === null || typeof value !== 'object') return false
|
|
41
|
+
const width = Reflect.get(value, 'width')
|
|
42
|
+
const height = Reflect.get(value, 'height')
|
|
43
|
+
return (
|
|
44
|
+
typeof width === 'number' &&
|
|
45
|
+
Number.isFinite(width) &&
|
|
46
|
+
width > 0 &&
|
|
47
|
+
typeof height === 'number' &&
|
|
48
|
+
Number.isFinite(height) &&
|
|
49
|
+
height > 0
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function viewportFromCapture(value: unknown): unknown {
|
|
54
|
+
if (value === null || typeof value !== 'object') return undefined
|
|
55
|
+
const tree = Reflect.get(value, 'tree')
|
|
56
|
+
return tree !== null && typeof tree === 'object'
|
|
57
|
+
? Reflect.get(tree, 'viewport')
|
|
58
|
+
: undefined
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// the semantic tree has no covering root, so its first node is often a 64x64
|
|
62
|
+
// icon. capture.tree.viewport is the device layout size on local and cloud.
|
|
63
|
+
//
|
|
64
|
+
// a whole screen capture is the only way to ask for it today, and it is not
|
|
65
|
+
// cheap: the headless runtime forces a full redraw of the tree before it walks
|
|
66
|
+
// it, which measured 3.9-5.5s on a real app in a cloud box against 120-290ms
|
|
67
|
+
// for every other command. that is why this runs on the failure path only.
|
|
68
|
+
async function readScreen(bridge: InspectBridge): Promise<ScreenSize | null> {
|
|
69
|
+
try {
|
|
70
|
+
const viewport = viewportFromCapture(
|
|
71
|
+
await bridge.send({ type: 'capture', includeVisual: false }),
|
|
72
|
+
)
|
|
73
|
+
return isScreenSize(viewport)
|
|
74
|
+
? { width: viewport.width, height: viewport.height }
|
|
75
|
+
: null
|
|
76
|
+
} catch {
|
|
77
|
+
return null
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function isOffscreen(cx: number, cy: number, screen: ScreenSize): boolean {
|
|
82
|
+
return !(cx >= 0 && cy >= 0 && cx <= screen.width && cy <= screen.height)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const DEFAULT_AGENT_TIMING: Required<TapTimingOptions> = {
|
|
86
|
+
initialWaitMs: 3000,
|
|
87
|
+
deadlineMs: 5000,
|
|
88
|
+
retryWaitMs: 700,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const DEFAULT_INTERACTIVE_TIMING: Required<TapTimingOptions> = {
|
|
92
|
+
initialWaitMs: 1200,
|
|
93
|
+
deadlineMs: 2500,
|
|
94
|
+
retryWaitMs: 700,
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function tapTiming(agent: boolean, opts: TapTimingOptions = {}) {
|
|
98
|
+
return {
|
|
99
|
+
...(agent ? DEFAULT_AGENT_TIMING : DEFAULT_INTERACTIVE_TIMING),
|
|
100
|
+
...opts,
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function isTapSuccess(result: any): boolean {
|
|
105
|
+
if (!result || result.hit === false || result.ok === false) return false
|
|
106
|
+
if (result.handled === false) return false
|
|
107
|
+
if (result.requestedTargetMatched === false) return false
|
|
108
|
+
if (
|
|
109
|
+
result.pointerTapHandled === false &&
|
|
110
|
+
!result.keyboardOpened &&
|
|
111
|
+
!result.isTextInput
|
|
112
|
+
) {
|
|
113
|
+
return false
|
|
114
|
+
}
|
|
115
|
+
return true
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function tapTargetFromPayload(
|
|
119
|
+
payload: any,
|
|
120
|
+
textFallback?: string,
|
|
121
|
+
): TapTargetSummary {
|
|
122
|
+
const id = payload.target?.id ?? payload.match?.id ?? payload.node?.id
|
|
123
|
+
return {
|
|
124
|
+
nodeId:
|
|
125
|
+
payload.target?.nodeId ?? payload.match?.nodeId ?? payload.node?.nodeId ?? null,
|
|
126
|
+
id: typeof id === 'string' ? id : null,
|
|
127
|
+
testID:
|
|
128
|
+
payload.target?.testID ?? payload.match?.testID ?? payload.node?.testID ?? null,
|
|
129
|
+
text:
|
|
130
|
+
payload.target?.text ??
|
|
131
|
+
payload.target?.accessibilityLabel ??
|
|
132
|
+
payload.match?.text ??
|
|
133
|
+
payload.match?.accessibilityLabel ??
|
|
134
|
+
payload.node?.text ??
|
|
135
|
+
textFallback ??
|
|
136
|
+
null,
|
|
137
|
+
type: payload.target?.type ?? payload.match?.type ?? payload.node?.type ?? null,
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export async function tapResolvedTarget(
|
|
142
|
+
bridge: InspectBridge,
|
|
143
|
+
args: {
|
|
144
|
+
textFallback?: string
|
|
145
|
+
resolve: () => Promise<any>
|
|
146
|
+
agent?: boolean
|
|
147
|
+
timing?: TapTimingOptions
|
|
148
|
+
},
|
|
149
|
+
): Promise<TapOutcome> {
|
|
150
|
+
const timing = tapTiming(!!args.agent, args.timing)
|
|
151
|
+
let attempts = 0
|
|
152
|
+
let lastPayload: any = null
|
|
153
|
+
let lastResult: any = null
|
|
154
|
+
|
|
155
|
+
// a CPU simulator only advances its animations while a command holds its
|
|
156
|
+
// clock, so a sheet that was still sliding in when the last command returned
|
|
157
|
+
// is still mid-slide now. settling first is what lets the resolved center and
|
|
158
|
+
// the hit test below describe the same tree.
|
|
159
|
+
const settleCloud = async () => {
|
|
160
|
+
try {
|
|
161
|
+
await bridge.send({ type: 'settle' })
|
|
162
|
+
} catch {
|
|
163
|
+
// best-effort readiness gate; the retry loop decides success.
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (bridge.plane === 'cloud') {
|
|
167
|
+
await settleCloud()
|
|
168
|
+
} else if (timing.initialWaitMs > 0) {
|
|
169
|
+
try {
|
|
170
|
+
await waitForSootsimIdle({
|
|
171
|
+
bridge,
|
|
172
|
+
maxMs: timing.initialWaitMs,
|
|
173
|
+
pollMs: 32,
|
|
174
|
+
stablePolls: 2,
|
|
175
|
+
})
|
|
176
|
+
} catch {
|
|
177
|
+
// best-effort readiness gate; the retry loop decides success.
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const deadline = Date.now() + timing.deadlineMs
|
|
182
|
+
let lastOffscreenAt: string | null = null
|
|
183
|
+
// a cloud sim refuses a point outside its screen itself, and says so, so
|
|
184
|
+
// asking it for a capture first would buy nothing. every other plane still
|
|
185
|
+
// presses whatever the hit test finds at tree coordinates no finger can
|
|
186
|
+
// reach, so there the screen has to be read before dispatching.
|
|
187
|
+
const screen = bridge.plane === 'cloud' ? null : await readScreen(bridge)
|
|
188
|
+
while (Date.now() <= deadline || attempts === 0) {
|
|
189
|
+
attempts++
|
|
190
|
+
const resolved = await args.resolve()
|
|
191
|
+
const payload =
|
|
192
|
+
resolved &&
|
|
193
|
+
typeof resolved.cx === 'number' &&
|
|
194
|
+
typeof resolved.cy === 'number' &&
|
|
195
|
+
screen &&
|
|
196
|
+
isOffscreen(resolved.cx, resolved.cy, screen)
|
|
197
|
+
? { ...resolved, offscreen: true, screen }
|
|
198
|
+
: resolved
|
|
199
|
+
lastPayload = payload
|
|
200
|
+
|
|
201
|
+
if (
|
|
202
|
+
payload?.error === 'bridge-not-ready' ||
|
|
203
|
+
payload?.ambiguous ||
|
|
204
|
+
payload?.nthOutOfRange
|
|
205
|
+
) {
|
|
206
|
+
return { payload, result: null, attempts, failure: 'special' }
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (payload && typeof payload.cx === 'number' && typeof payload.cy === 'number') {
|
|
210
|
+
// don't dispatch a point outside the device screen: the press would do
|
|
211
|
+
// nothing a user could reproduce. report the offscreen miss instead so
|
|
212
|
+
// drivers scroll first rather than getting stuck trusting a phantom hit.
|
|
213
|
+
if (payload.offscreen) {
|
|
214
|
+
lastResult = {
|
|
215
|
+
hit: false,
|
|
216
|
+
reason: 'offscreen',
|
|
217
|
+
x: payload.cx,
|
|
218
|
+
y: payload.cy,
|
|
219
|
+
screen: payload.screen,
|
|
220
|
+
}
|
|
221
|
+
// stable offscreen position twice in a row is deterministic — the
|
|
222
|
+
// node is scrolled out of view, not settling in. bail early instead
|
|
223
|
+
// of retrying to the deadline.
|
|
224
|
+
const at = `${Math.round(payload.cx)},${Math.round(payload.cy)}`
|
|
225
|
+
if (lastOffscreenAt === at) {
|
|
226
|
+
return { payload, result: lastResult, attempts, failure: 'missed' }
|
|
227
|
+
}
|
|
228
|
+
lastOffscreenAt = at
|
|
229
|
+
} else {
|
|
230
|
+
lastOffscreenAt = null
|
|
231
|
+
const requestedTarget = tapTargetFromPayload(payload, args.textFallback)
|
|
232
|
+
const result = await tapCoordinates(
|
|
233
|
+
bridge,
|
|
234
|
+
payload.cx,
|
|
235
|
+
payload.cy,
|
|
236
|
+
requestedTarget,
|
|
237
|
+
)
|
|
238
|
+
// a cloud sim reports the offscreen refusal itself, carrying the point
|
|
239
|
+
// and the screen in its diagnostics. give it the same shape the local
|
|
240
|
+
// check produces so there is one offscreen failure to report.
|
|
241
|
+
lastResult =
|
|
242
|
+
result?.reason === 'offscreen' && result.diagnostics
|
|
243
|
+
? {
|
|
244
|
+
...result,
|
|
245
|
+
hit: false,
|
|
246
|
+
x: result.diagnostics.point?.x ?? payload.cx,
|
|
247
|
+
y: result.diagnostics.point?.y ?? payload.cy,
|
|
248
|
+
screen: result.diagnostics.screen,
|
|
249
|
+
}
|
|
250
|
+
: result
|
|
251
|
+
if (result?.requestedTargetMatched === false) {
|
|
252
|
+
return { payload, result, attempts, failure: 'missed' }
|
|
253
|
+
}
|
|
254
|
+
if (isTapSuccess(result)) return { payload, result, attempts }
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const remaining = deadline - Date.now()
|
|
259
|
+
if (remaining <= 0) break
|
|
260
|
+
if (bridge.plane === 'cloud') {
|
|
261
|
+
if (lastResult?.reason !== 'target-moved' || attempts >= 3) break
|
|
262
|
+
await settleCloud()
|
|
263
|
+
continue
|
|
264
|
+
}
|
|
265
|
+
if (timing.retryWaitMs <= 0) break
|
|
266
|
+
try {
|
|
267
|
+
await waitForSootsimIdle({
|
|
268
|
+
bridge,
|
|
269
|
+
maxMs: Math.min(remaining, timing.retryWaitMs),
|
|
270
|
+
pollMs: 32,
|
|
271
|
+
stablePolls: 2,
|
|
272
|
+
})
|
|
273
|
+
} catch {
|
|
274
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(120, remaining)))
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
payload: lastPayload,
|
|
280
|
+
result: lastResult,
|
|
281
|
+
attempts,
|
|
282
|
+
failure: lastPayload && typeof lastPayload.cx === 'number' ? 'missed' : 'not-found',
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export async function tapCoordinates(
|
|
287
|
+
bridge: InspectBridge,
|
|
288
|
+
x: number,
|
|
289
|
+
y: number,
|
|
290
|
+
target?: TapTargetSummary,
|
|
291
|
+
): Promise<any> {
|
|
292
|
+
return bridge.send({
|
|
293
|
+
type: 'tap',
|
|
294
|
+
x,
|
|
295
|
+
y,
|
|
296
|
+
...(target ? { target } : {}),
|
|
297
|
+
})
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function payloadFromResolved(
|
|
301
|
+
resolved: ResolvedTargetCoords,
|
|
302
|
+
match: SootSimReplayTarget,
|
|
303
|
+
extra: Record<string, unknown> = {},
|
|
304
|
+
) {
|
|
305
|
+
return {
|
|
306
|
+
cx: resolved.x,
|
|
307
|
+
cy: resolved.y,
|
|
308
|
+
match: {
|
|
309
|
+
nodeId: match.nodeId ?? null,
|
|
310
|
+
id: match.testID ?? null,
|
|
311
|
+
testID: match.testID ?? null,
|
|
312
|
+
text: match.text ?? null,
|
|
313
|
+
type: match.type ?? null,
|
|
314
|
+
},
|
|
315
|
+
target: {
|
|
316
|
+
nodeId: resolved.nodeId ?? match.nodeId ?? null,
|
|
317
|
+
id: resolved.testID ?? match.testID ?? null,
|
|
318
|
+
testID: resolved.testID ?? match.testID ?? null,
|
|
319
|
+
text: resolved.text ?? match.text ?? null,
|
|
320
|
+
type: resolved.type ?? match.type ?? null,
|
|
321
|
+
},
|
|
322
|
+
strategy: 'matched-node',
|
|
323
|
+
...extra,
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
interface TextCandidate {
|
|
328
|
+
node: SimSemanticNode
|
|
329
|
+
ancestorTestIDs: string[]
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// a non-pressable node repeating the text of its nearest labelled ancestor,
|
|
333
|
+
// when that ancestor is pressable, is the ancestor's own caption (a button and
|
|
334
|
+
// its text child), so only the button is a candidate. otherwise every labelled
|
|
335
|
+
// button is an ambiguous pair. a pressable descendant is never folded into its
|
|
336
|
+
// ancestor: same text does not prove it shares the ancestor's tap target.
|
|
337
|
+
function collectTextCandidates(
|
|
338
|
+
nodes: readonly SimSemanticNode[],
|
|
339
|
+
ancestors: string[] = [],
|
|
340
|
+
pressableOwnerText = '',
|
|
341
|
+
): TextCandidate[] {
|
|
342
|
+
const out: TextCandidate[] = []
|
|
343
|
+
for (const node of nodes) {
|
|
344
|
+
const text = nodeText(node)
|
|
345
|
+
const caption = !!text && text === pressableOwnerText && !node.pressable
|
|
346
|
+
if (!caption) out.push({ node, ancestorTestIDs: ancestors })
|
|
347
|
+
if (node.children?.length) {
|
|
348
|
+
const next = node.testID ? [...ancestors, node.testID] : ancestors
|
|
349
|
+
const owner = text ? (node.pressable ? text : '') : pressableOwnerText
|
|
350
|
+
out.push(...collectTextCandidates(node.children, next, owner))
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return out
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function nodeText(node: SimSemanticNode): string {
|
|
357
|
+
return node.text ?? node.label ?? ''
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export async function tapById(
|
|
361
|
+
bridge: InspectBridge,
|
|
362
|
+
query: string,
|
|
363
|
+
opts: { agent?: boolean; timing?: TapTimingOptions } = {},
|
|
364
|
+
): Promise<TapOutcome> {
|
|
365
|
+
return tapResolvedTarget(bridge, {
|
|
366
|
+
agent: opts.agent,
|
|
367
|
+
timing: opts.timing,
|
|
368
|
+
resolve: async () => {
|
|
369
|
+
const resolved = await resolveTargetCoords(bridge, { mode: 'testid', value: query })
|
|
370
|
+
if (!resolved) return { cx: null }
|
|
371
|
+
return payloadFromResolved(resolved, resolved)
|
|
372
|
+
},
|
|
373
|
+
})
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export async function tapByText(
|
|
377
|
+
bridge: InspectBridge,
|
|
378
|
+
query: string,
|
|
379
|
+
options: TapTextOptions = {},
|
|
380
|
+
opts: { agent?: boolean; timing?: TapTimingOptions } = {},
|
|
381
|
+
): Promise<TapOutcome> {
|
|
382
|
+
return tapResolvedTarget(bridge, {
|
|
383
|
+
agent: opts.agent,
|
|
384
|
+
timing: opts.timing,
|
|
385
|
+
textFallback: query,
|
|
386
|
+
resolve: async () => {
|
|
387
|
+
const { tree } = await inspectTree(bridge, 50)
|
|
388
|
+
if (!Array.isArray(tree)) return { matched: 0, total: 0 }
|
|
389
|
+
const needle = options.exact ? query : query.toLowerCase()
|
|
390
|
+
let candidates = collectTextCandidates(tree).filter(({ node, ancestorTestIDs }) => {
|
|
391
|
+
const text = nodeText(node)
|
|
392
|
+
if (!text) return false
|
|
393
|
+
if (options.exact ? text !== needle : !text.toLowerCase().includes(needle)) {
|
|
394
|
+
return false
|
|
395
|
+
}
|
|
396
|
+
if (options.role && node.role !== options.role) return false
|
|
397
|
+
if (options.within && !ancestorTestIDs.includes(options.within)) return false
|
|
398
|
+
const ax = node.geometry.x
|
|
399
|
+
const ay = node.geometry.y
|
|
400
|
+
if (options.minX != null && !(ax >= options.minX)) return false
|
|
401
|
+
if (options.maxX != null && !(ax <= options.maxX)) return false
|
|
402
|
+
if (options.minY != null && !(ay >= options.minY)) return false
|
|
403
|
+
if (options.maxY != null && !(ay <= options.maxY)) return false
|
|
404
|
+
return true
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
if (options.near) {
|
|
408
|
+
const { x, y } = options.near
|
|
409
|
+
candidates.sort(
|
|
410
|
+
(a, b) =>
|
|
411
|
+
Math.hypot(a.node.geometry.x - x, a.node.geometry.y - y) -
|
|
412
|
+
Math.hypot(b.node.geometry.x - x, b.node.geometry.y - y),
|
|
413
|
+
)
|
|
414
|
+
} else {
|
|
415
|
+
candidates.sort((a, b) => {
|
|
416
|
+
if (Math.abs(a.node.geometry.y - b.node.geometry.y) > 2) {
|
|
417
|
+
return a.node.geometry.y - b.node.geometry.y
|
|
418
|
+
}
|
|
419
|
+
return a.node.geometry.x - b.node.geometry.x
|
|
420
|
+
})
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const total = candidates.length
|
|
424
|
+
if (total === 0) return { matched: 0, total: 0 }
|
|
425
|
+
|
|
426
|
+
let idx = 0
|
|
427
|
+
if (options.nth != null) {
|
|
428
|
+
idx = options.nth < 0 ? total + options.nth : options.nth
|
|
429
|
+
if (idx < 0 || idx >= total) {
|
|
430
|
+
return { matched: 0, total, nthOutOfRange: true, nth: options.nth }
|
|
431
|
+
}
|
|
432
|
+
} else if (total > 1 && !options.first && !options.near) {
|
|
433
|
+
return {
|
|
434
|
+
ambiguous: true,
|
|
435
|
+
total,
|
|
436
|
+
candidates: candidates.slice(0, 10).map((candidate, index) => ({
|
|
437
|
+
idx: index,
|
|
438
|
+
nodeId: candidate.node.nodeId,
|
|
439
|
+
type: candidate.node.type,
|
|
440
|
+
testID: candidate.node.testID ?? null,
|
|
441
|
+
text: nodeText(candidate.node).slice(0, 60),
|
|
442
|
+
abs: { x: candidate.node.geometry.x, y: candidate.node.geometry.y },
|
|
443
|
+
layout: {
|
|
444
|
+
width: Math.round(candidate.node.geometry.width),
|
|
445
|
+
height: Math.round(candidate.node.geometry.height),
|
|
446
|
+
},
|
|
447
|
+
ancestorTestIDs: candidate.ancestorTestIDs.slice(0, 5),
|
|
448
|
+
})),
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const picked = candidates[idx]
|
|
453
|
+
if (!picked) return { matched: 0, total }
|
|
454
|
+
const node = picked.node
|
|
455
|
+
let resolved = node.testID
|
|
456
|
+
? await resolveTargetCoords(bridge, { mode: 'testid', value: node.testID })
|
|
457
|
+
: await resolveTargetCoords(bridge, {
|
|
458
|
+
mode: 'text',
|
|
459
|
+
value: nodeText(node) || query,
|
|
460
|
+
})
|
|
461
|
+
if (!resolved) {
|
|
462
|
+
resolved = {
|
|
463
|
+
x: node.geometry.x + node.geometry.width / 2,
|
|
464
|
+
y: node.geometry.y + node.geometry.height / 2,
|
|
465
|
+
testID: node.testID ?? null,
|
|
466
|
+
text: nodeText(node) || query,
|
|
467
|
+
type: node.type,
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
return payloadFromResolved(resolved, node, { total, idx })
|
|
471
|
+
},
|
|
472
|
+
})
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
export async function tapBest(
|
|
476
|
+
bridge: InspectBridge,
|
|
477
|
+
query: string,
|
|
478
|
+
opts: { agent?: boolean; timing?: TapTimingOptions } = {},
|
|
479
|
+
): Promise<TapOutcome> {
|
|
480
|
+
return tapResolvedTarget(bridge, {
|
|
481
|
+
agent: opts.agent,
|
|
482
|
+
timing: opts.timing,
|
|
483
|
+
textFallback: query,
|
|
484
|
+
resolve: async () => {
|
|
485
|
+
const byId = await resolveTargetCoords(bridge, { mode: 'testid', value: query })
|
|
486
|
+
if (byId) {
|
|
487
|
+
return {
|
|
488
|
+
...payloadFromResolved(byId, byId),
|
|
489
|
+
strategy: 'testid',
|
|
490
|
+
node: {
|
|
491
|
+
nodeId: byId.nodeId ?? null,
|
|
492
|
+
id: byId.testID ?? query,
|
|
493
|
+
testID: byId.testID ?? query,
|
|
494
|
+
type: byId.type ?? null,
|
|
495
|
+
text: byId.text ?? null,
|
|
496
|
+
},
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
const byText = await resolveTargetCoords(bridge, { mode: 'text', value: query })
|
|
500
|
+
if (byText) {
|
|
501
|
+
return {
|
|
502
|
+
...payloadFromResolved(byText, byText),
|
|
503
|
+
strategy: 'text',
|
|
504
|
+
node: {
|
|
505
|
+
nodeId: byText.nodeId ?? null,
|
|
506
|
+
id: byText.testID ?? null,
|
|
507
|
+
testID: byText.testID ?? null,
|
|
508
|
+
type: byText.type ?? null,
|
|
509
|
+
text: byText.text ?? query,
|
|
510
|
+
},
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
return { strategy: 'none' }
|
|
514
|
+
},
|
|
515
|
+
})
|
|
516
|
+
}
|