rnxsim 0.1.417 → 0.1.419
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli/cloud-client.ts +104 -32
- package/cli/cloud-dispatch.ts +38 -4
- package/cli/commands/inspect/actions.ts +313 -385
- package/cli/commands/inspect/core.ts +27 -12
- package/cli/commands/inspect/resolve-target.ts +2 -3
- package/cli/commands/inspect/settle.ts +10 -0
- package/cli/commands/inspect/wait-ready.ts +2 -0
- package/cli/commands/inspect.ts +31 -48
- package/cli/commands/platform.ts +41 -10
- package/cli/outbound-endpoints.ts +1 -1
- package/cli/self-invocation.ts +2 -2
- package/cli/shell-init.ts +1 -1
- package/dist-lib/agent-daemon-client.cjs +1 -1
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/agent-identity.cjs +1 -1
- package/dist-lib/agent-sessions.cjs +1 -1
- package/dist-lib/attached-projects.cjs +1 -1
- package/dist-lib/auth/shared-session.cjs +1 -1
- package/dist-lib/backend-origin.cjs +1 -1
- package/dist-lib/beta.cjs +1 -1
- package/dist-lib/beta.mjs +1 -1
- package/dist-lib/bridge-constants.cjs +1 -1
- package/dist-lib/bridge-contract-input.cjs +1 -1
- package/dist-lib/bridge-contract-input.mjs +1 -1
- package/dist-lib/bridge-contract.cjs +7 -1
- package/dist-lib/bridge-contract.mjs +5 -1
- package/dist-lib/capture-contract.cjs +1 -1
- package/dist-lib/capture-contract.mjs +1 -1
- package/dist-lib/cli-constants.cjs +1 -1
- package/dist-lib/cloud-contract.cjs +1 -1
- package/dist-lib/cloud-contract.mjs +1 -1
- package/dist-lib/config.cjs +1 -1
- package/dist-lib/detox/index.cjs +1 -1
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +1 -1
- package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
- package/dist-lib/host/replacement-module-handler.cjs +1 -1
- package/dist-lib/host/websocket-proxy.cjs +1 -1
- package/dist-lib/index.cjs +631 -749
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/menu.cjs +1 -1
- package/dist-lib/menu.mjs +1 -1
- package/dist-lib/metro-fingerprint-registry.cjs +1 -1
- package/dist-lib/metro-fingerprint-registry.mjs +1 -1
- package/dist-lib/metro-production-bundle.cjs +1 -1
- package/dist-lib/metro-production-bundle.mjs +1 -1
- package/dist-lib/metro.cjs +1 -1
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/public-brand.cjs +1 -1
- package/dist-lib/react-native-host-modules.cjs +1 -1
- package/dist-lib/react-native-host-modules.mjs +1 -1
- package/dist-lib/render-mode.cjs +1 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
- package/dist-lib/sdk.cjs +1209 -1342
- package/dist-lib/sdk.mjs +1209 -1342
- package/dist-lib/skills.cjs +63 -2
- package/dist-lib/vite.cjs +1 -1
- package/package.json +1 -1
- package/src/bridge-contract.ts +3 -0
- package/src/host/bridge-host.ts +1 -1
- package/src/host/plane-host.ts +1 -1
- package/src/native-dev-bundle-url.ts +1 -1
- package/src/vite-plugin.ts +1 -1
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { inspectTree, type InspectBridge } from './core'
|
|
2
|
+
import { resolveTargetCoords } from './resolve-target'
|
|
1
3
|
import { waitForSootsimIdle } from './settling'
|
|
2
|
-
import type {
|
|
4
|
+
import type { SimSemanticNode } from '../../../src/bridge-contract'
|
|
3
5
|
|
|
4
6
|
export type TapFailure = 'not-found' | 'missed' | 'special'
|
|
5
7
|
|
|
@@ -37,18 +39,99 @@ export interface TapTimingOptions {
|
|
|
37
39
|
retryWaitMs?: number
|
|
38
40
|
}
|
|
39
41
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
42
|
+
type ScreenSize = { width: number; height: number }
|
|
43
|
+
|
|
44
|
+
function isScreenSize(value: unknown): value is ScreenSize {
|
|
45
|
+
if (value === null || typeof value !== 'object') return false
|
|
46
|
+
const width = Reflect.get(value, 'width')
|
|
47
|
+
const height = Reflect.get(value, 'height')
|
|
48
|
+
return (
|
|
49
|
+
typeof width === 'number' &&
|
|
50
|
+
Number.isFinite(width) &&
|
|
51
|
+
width > 0 &&
|
|
52
|
+
typeof height === 'number' &&
|
|
53
|
+
Number.isFinite(height) &&
|
|
54
|
+
height > 0
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function viewportFromCapture(value: unknown): unknown {
|
|
59
|
+
if (value === null || typeof value !== 'object') return undefined
|
|
60
|
+
const tree = Reflect.get(value, 'tree')
|
|
61
|
+
return tree !== null && typeof tree === 'object'
|
|
62
|
+
? Reflect.get(tree, 'viewport')
|
|
63
|
+
: undefined
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// the semantic tree has no covering root, so its first node is often a 64x64
|
|
67
|
+
// icon. capture.tree.viewport is the device layout size on local and cloud.
|
|
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 offscreenPayload(
|
|
82
|
+
cx: number,
|
|
83
|
+
cy: number,
|
|
84
|
+
screen: ScreenSize | null,
|
|
85
|
+
): { offscreen: true; screen: ScreenSize } | Record<string, never> {
|
|
86
|
+
if (!screen || (cx >= 0 && cy >= 0 && cx <= screen.width && cy <= screen.height)) {
|
|
87
|
+
return {}
|
|
88
|
+
}
|
|
89
|
+
return { offscreen: true, screen }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function geometryContains(geometry: SimSemanticNode['geometry'], x: number, y: number) {
|
|
93
|
+
return (
|
|
94
|
+
x >= geometry.x &&
|
|
95
|
+
y >= geometry.y &&
|
|
96
|
+
x <= geometry.x + geometry.width &&
|
|
97
|
+
y <= geometry.y + geometry.height
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function hitTestSemanticTree(
|
|
102
|
+
nodes: readonly SimSemanticNode[],
|
|
103
|
+
x: number,
|
|
104
|
+
y: number,
|
|
105
|
+
ancestors: SimSemanticNode[] = [],
|
|
106
|
+
): { node: SimSemanticNode; ancestors: SimSemanticNode[] } | null {
|
|
107
|
+
for (let index = nodes.length - 1; index >= 0; index--) {
|
|
108
|
+
const node = nodes[index]
|
|
109
|
+
if (!node || !geometryContains(node.geometry, x, y)) continue
|
|
110
|
+
const childHit = node.children?.length
|
|
111
|
+
? hitTestSemanticTree(node.children, x, y, [...ancestors, node])
|
|
112
|
+
: null
|
|
113
|
+
return childHit ?? { node, ancestors }
|
|
114
|
+
}
|
|
115
|
+
return null
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function nodeMatchesRequested(node: SimSemanticNode, requested: TapTargetSummary) {
|
|
119
|
+
if (typeof requested.nodeId === 'number' && node.nodeId === requested.nodeId) {
|
|
120
|
+
return true
|
|
121
|
+
}
|
|
122
|
+
if (requested.testID && node.testID === requested.testID) return true
|
|
123
|
+
if (requested.id && node.testID === requested.id) return true
|
|
124
|
+
return false
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function coveringNodeSummary(node: SimSemanticNode) {
|
|
128
|
+
return {
|
|
129
|
+
nodeId: node.nodeId,
|
|
130
|
+
testID: node.testID ?? null,
|
|
131
|
+
text: node.text ?? node.label ?? null,
|
|
132
|
+
type: node.type,
|
|
133
|
+
}
|
|
134
|
+
}
|
|
52
135
|
|
|
53
136
|
const DEFAULT_AGENT_TIMING: Required<TapTimingOptions> = {
|
|
54
137
|
initialWaitMs: 3000,
|
|
@@ -71,6 +154,7 @@ function tapTiming(agent: boolean, opts: TapTimingOptions = {}) {
|
|
|
71
154
|
|
|
72
155
|
export function isTapSuccess(result: any): boolean {
|
|
73
156
|
if (!result || result.hit === false || result.ok === false) return false
|
|
157
|
+
if (result.handled === false) return false
|
|
74
158
|
if (result.requestedTargetMatched === false) return false
|
|
75
159
|
if (
|
|
76
160
|
result.pointerTapHandled === false &&
|
|
@@ -118,22 +202,30 @@ export async function tapResolvedTarget(
|
|
|
118
202
|
let lastPayload: any = null
|
|
119
203
|
let lastResult: any = null
|
|
120
204
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
205
|
+
const canWaitForIdle = bridge.plane !== 'cloud' && timing.initialWaitMs > 0
|
|
206
|
+
if (canWaitForIdle) {
|
|
207
|
+
try {
|
|
208
|
+
await waitForSootsimIdle({
|
|
209
|
+
bridge,
|
|
210
|
+
maxMs: timing.initialWaitMs,
|
|
211
|
+
pollMs: 32,
|
|
212
|
+
stablePolls: 2,
|
|
213
|
+
})
|
|
214
|
+
} catch {
|
|
215
|
+
// best-effort readiness gate; the retry loop decides success.
|
|
216
|
+
}
|
|
130
217
|
}
|
|
131
218
|
|
|
132
219
|
const deadline = Date.now() + timing.deadlineMs
|
|
133
220
|
let lastOffscreenAt: string | null = null
|
|
221
|
+
const screen = await readScreen(bridge)
|
|
134
222
|
while (Date.now() <= deadline || attempts === 0) {
|
|
135
223
|
attempts++
|
|
136
|
-
const
|
|
224
|
+
const resolved = await args.resolve()
|
|
225
|
+
const payload =
|
|
226
|
+
resolved && typeof resolved.cx === 'number' && typeof resolved.cy === 'number'
|
|
227
|
+
? { ...resolved, ...offscreenPayload(resolved.cx, resolved.cy, screen) }
|
|
228
|
+
: resolved
|
|
137
229
|
lastPayload = payload
|
|
138
230
|
|
|
139
231
|
if (
|
|
@@ -170,122 +262,31 @@ export async function tapResolvedTarget(
|
|
|
170
262
|
} else {
|
|
171
263
|
lastOffscreenAt = null
|
|
172
264
|
const requestedTarget = tapTargetFromPayload(payload, args.textFallback)
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
const requestedTarget = ${JSON.stringify(requestedTarget)}
|
|
185
|
-
const inspected = await t.inspectAt(${payload.cx}, ${payload.cy})
|
|
186
|
-
const resolved =
|
|
187
|
-
inspected && typeof inspected.nodeId === 'number'
|
|
188
|
-
? await t.resolveTapTarget(inspected.nodeId)
|
|
189
|
-
: null
|
|
190
|
-
const node = (resolved && resolved.target) || inspected
|
|
191
|
-
const coordinateTarget = node
|
|
192
|
-
? {
|
|
193
|
-
nodeId: node.nodeId ?? null,
|
|
194
|
-
id: node.id ?? null,
|
|
195
|
-
testID: node.testID ?? null,
|
|
196
|
-
text: node.text ?? node.accessibilityLabel ?? null,
|
|
197
|
-
type: node.type ?? null,
|
|
198
|
-
}
|
|
199
|
-
: null
|
|
200
|
-
const coordinateAncestors = Array.isArray(inspected?.ancestors)
|
|
201
|
-
? inspected.ancestors
|
|
202
|
-
: []
|
|
203
|
-
const ancestorMatchesRequested = coordinateAncestors.some(
|
|
204
|
-
(ancestor) =>
|
|
205
|
-
(typeof requestedTarget.nodeId === 'number' &&
|
|
206
|
-
ancestor?.nodeId === requestedTarget.nodeId) ||
|
|
207
|
-
(requestedTarget.testID &&
|
|
208
|
-
ancestor?.testID === requestedTarget.testID) ||
|
|
209
|
-
(requestedTarget.id && ancestor?.id === requestedTarget.id),
|
|
210
|
-
)
|
|
211
|
-
const requestedTargetMatched =
|
|
212
|
-
coordinateTarget === null
|
|
213
|
-
? false
|
|
214
|
-
: typeof requestedTarget.nodeId === 'number' &&
|
|
215
|
-
typeof coordinateTarget.nodeId === 'number'
|
|
216
|
-
? requestedTarget.nodeId === coordinateTarget.nodeId ||
|
|
217
|
-
ancestorMatchesRequested
|
|
218
|
-
: requestedTarget.testID && coordinateTarget.testID
|
|
219
|
-
? requestedTarget.testID === coordinateTarget.testID
|
|
220
|
-
|| ancestorMatchesRequested
|
|
221
|
-
: requestedTarget.id && coordinateTarget.id
|
|
222
|
-
? requestedTarget.id === coordinateTarget.id
|
|
223
|
-
|| ancestorMatchesRequested
|
|
224
|
-
: ancestorMatchesRequested
|
|
225
|
-
if (!requestedTargetMatched) {
|
|
226
|
-
return {
|
|
227
|
-
requestedTargetMatched,
|
|
228
|
-
requestedTarget,
|
|
229
|
-
coordinateTarget,
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
if (typeof t.activatePressAt !== 'function') {
|
|
233
|
-
return { error: 'tap-target-activation-unavailable' }
|
|
234
|
-
}
|
|
235
|
-
const activation = await t.activatePressAt(
|
|
236
|
-
${payload.cx},
|
|
237
|
-
${payload.cy},
|
|
238
|
-
requestedTarget.nodeId,
|
|
239
|
-
)
|
|
240
|
-
if (activation?.ok) {
|
|
241
|
-
window.dispatchEvent(
|
|
242
|
-
new CustomEvent('sootsim:agentAction', {
|
|
243
|
-
detail: {
|
|
244
|
-
type: 'tap',
|
|
245
|
-
x: ${payload.cx},
|
|
246
|
-
y: ${payload.cy},
|
|
247
|
-
target: requestedTarget,
|
|
248
|
-
},
|
|
249
|
-
}),
|
|
250
|
-
)
|
|
251
|
-
}
|
|
252
|
-
return {
|
|
253
|
-
activation,
|
|
254
|
-
requestedTargetMatched,
|
|
255
|
-
requestedTarget,
|
|
256
|
-
coordinateTarget,
|
|
257
|
-
}
|
|
258
|
-
})()`,
|
|
259
|
-
})
|
|
260
|
-
if (verification?.error) {
|
|
261
|
-
return {
|
|
262
|
-
payload,
|
|
263
|
-
result: {
|
|
264
|
-
hit: false,
|
|
265
|
-
reason: verification.error,
|
|
266
|
-
requestedTarget,
|
|
267
|
-
},
|
|
268
|
-
attempts,
|
|
269
|
-
failure: 'special',
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
if (verification?.requestedTargetMatched === false) {
|
|
273
|
-
return {
|
|
274
|
-
payload,
|
|
275
|
-
result: {
|
|
265
|
+
const { tree } = await inspectTree(bridge, 50)
|
|
266
|
+
if (Array.isArray(tree) && tree.length > 0) {
|
|
267
|
+
const hit = hitTestSemanticTree(tree, payload.cx, payload.cy)
|
|
268
|
+
const matched =
|
|
269
|
+
hit !== null &&
|
|
270
|
+
(nodeMatchesRequested(hit.node, requestedTarget) ||
|
|
271
|
+
hit.ancestors.some((ancestor) =>
|
|
272
|
+
nodeMatchesRequested(ancestor, requestedTarget),
|
|
273
|
+
))
|
|
274
|
+
if (hit && !matched) {
|
|
275
|
+
lastResult = {
|
|
276
276
|
hit: false,
|
|
277
277
|
reason: 'target-covered',
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
failure: 'missed'
|
|
278
|
+
requestedTarget,
|
|
279
|
+
coordinateTarget: coveringNodeSummary(hit.node),
|
|
280
|
+
}
|
|
281
|
+
return { payload, result: lastResult, attempts, failure: 'missed' }
|
|
282
282
|
}
|
|
283
283
|
}
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
284
|
+
const result = await tapCoordinates(
|
|
285
|
+
bridge,
|
|
286
|
+
payload.cx,
|
|
287
|
+
payload.cy,
|
|
288
|
+
requestedTarget,
|
|
289
|
+
)
|
|
289
290
|
lastResult = result
|
|
290
291
|
if (isTapSuccess(result)) return { payload, result, attempts }
|
|
291
292
|
}
|
|
@@ -293,6 +294,7 @@ export async function tapResolvedTarget(
|
|
|
293
294
|
|
|
294
295
|
const remaining = deadline - Date.now()
|
|
295
296
|
if (remaining <= 0) break
|
|
297
|
+
if (bridge.plane === 'cloud' || timing.retryWaitMs <= 0) break
|
|
296
298
|
try {
|
|
297
299
|
await waitForSootsimIdle({
|
|
298
300
|
bridge,
|
|
@@ -327,66 +329,81 @@ export async function tapCoordinates(
|
|
|
327
329
|
})
|
|
328
330
|
}
|
|
329
331
|
|
|
332
|
+
function payloadFromResolved(
|
|
333
|
+
resolved: {
|
|
334
|
+
x: number
|
|
335
|
+
y: number
|
|
336
|
+
testID?: string | null
|
|
337
|
+
text?: string | null
|
|
338
|
+
type?: string | null
|
|
339
|
+
},
|
|
340
|
+
match: {
|
|
341
|
+
nodeId?: number | null
|
|
342
|
+
testID?: string | null
|
|
343
|
+
text?: string | null
|
|
344
|
+
type?: string | null
|
|
345
|
+
},
|
|
346
|
+
extra: Record<string, unknown> = {},
|
|
347
|
+
) {
|
|
348
|
+
return {
|
|
349
|
+
cx: resolved.x,
|
|
350
|
+
cy: resolved.y,
|
|
351
|
+
match: {
|
|
352
|
+
nodeId: match.nodeId ?? null,
|
|
353
|
+
id: match.testID ?? null,
|
|
354
|
+
testID: match.testID ?? null,
|
|
355
|
+
text: match.text ?? null,
|
|
356
|
+
type: match.type ?? null,
|
|
357
|
+
},
|
|
358
|
+
target: {
|
|
359
|
+
nodeId: match.nodeId ?? null,
|
|
360
|
+
id: resolved.testID ?? match.testID ?? null,
|
|
361
|
+
testID: resolved.testID ?? match.testID ?? null,
|
|
362
|
+
text: resolved.text ?? match.text ?? null,
|
|
363
|
+
type: resolved.type ?? match.type ?? null,
|
|
364
|
+
},
|
|
365
|
+
strategy: 'matched-node',
|
|
366
|
+
...extra,
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
interface TextCandidate {
|
|
371
|
+
node: SimSemanticNode
|
|
372
|
+
ancestorTestIDs: string[]
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function collectTextCandidates(
|
|
376
|
+
nodes: readonly SimSemanticNode[],
|
|
377
|
+
ancestors: string[] = [],
|
|
378
|
+
): TextCandidate[] {
|
|
379
|
+
const out: TextCandidate[] = []
|
|
380
|
+
for (const node of nodes) {
|
|
381
|
+
out.push({ node, ancestorTestIDs: ancestors })
|
|
382
|
+
if (node.children?.length) {
|
|
383
|
+
const next = node.testID ? [...ancestors, node.testID] : ancestors
|
|
384
|
+
out.push(...collectTextCandidates(node.children, next))
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return out
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function nodeText(node: SimSemanticNode): string {
|
|
391
|
+
return node.text ?? node.label ?? ''
|
|
392
|
+
}
|
|
393
|
+
|
|
330
394
|
export async function tapById(
|
|
331
395
|
bridge: InspectBridge,
|
|
332
396
|
query: string,
|
|
333
397
|
opts: { agent?: boolean; timing?: TapTimingOptions } = {},
|
|
334
398
|
): Promise<TapOutcome> {
|
|
335
|
-
const arg = JSON.stringify(query)
|
|
336
399
|
return tapResolvedTarget(bridge, {
|
|
337
400
|
agent: opts.agent,
|
|
338
401
|
timing: opts.timing,
|
|
339
|
-
resolve: () =>
|
|
340
|
-
bridge
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
if (!t) return null
|
|
345
|
-
const n = (await t.findByTestId(${arg})) || (await t.findById(${arg}))
|
|
346
|
-
if (!n || !n.absolutePosition || !n.layout) return { cx: null }
|
|
347
|
-
const resolved =
|
|
348
|
-
typeof n.nodeId === 'number' && typeof t.resolveTapTarget === 'function'
|
|
349
|
-
? await t.resolveTapTarget(n.nodeId)
|
|
350
|
-
: null
|
|
351
|
-
const target = (resolved && resolved.target) || n
|
|
352
|
-
const cx =
|
|
353
|
-
resolved && typeof resolved.cx === 'number'
|
|
354
|
-
? resolved.cx
|
|
355
|
-
: n.absolutePosition.x + (n.layout.width || 0) / 2
|
|
356
|
-
const cy =
|
|
357
|
-
resolved && typeof resolved.cy === 'number'
|
|
358
|
-
? resolved.cy
|
|
359
|
-
: n.absolutePosition.y + (n.layout.height || 0) / 2
|
|
360
|
-
const scr = ${SCREEN_EVAL}
|
|
361
|
-
const offscreen =
|
|
362
|
-
!!scr && (cx < 0 || cy < 0 || cx > scr.width || cy > scr.height)
|
|
363
|
-
return {
|
|
364
|
-
cx,
|
|
365
|
-
cy,
|
|
366
|
-
...(offscreen ? { offscreen: true, screen: scr } : {}),
|
|
367
|
-
match: {
|
|
368
|
-
nodeId: n.nodeId ?? null,
|
|
369
|
-
id: n.id,
|
|
370
|
-
testID: n.testID,
|
|
371
|
-
text: n.text ?? n.accessibilityLabel ?? null,
|
|
372
|
-
type: n.type,
|
|
373
|
-
},
|
|
374
|
-
target: {
|
|
375
|
-
nodeId: target.nodeId ?? null,
|
|
376
|
-
id: target.id,
|
|
377
|
-
testID: target.testID,
|
|
378
|
-
text:
|
|
379
|
-
target.text ??
|
|
380
|
-
target.accessibilityLabel ??
|
|
381
|
-
n.text ??
|
|
382
|
-
n.accessibilityLabel ??
|
|
383
|
-
null,
|
|
384
|
-
type: target.type,
|
|
385
|
-
},
|
|
386
|
-
strategy: (resolved && resolved.strategy) || 'matched-node',
|
|
387
|
-
}
|
|
388
|
-
})()`,
|
|
389
|
-
}),
|
|
402
|
+
resolve: async () => {
|
|
403
|
+
const resolved = await resolveTargetCoords(bridge, { mode: 'testid', value: query })
|
|
404
|
+
if (!resolved) return { cx: null }
|
|
405
|
+
return payloadFromResolved(resolved, resolved)
|
|
406
|
+
},
|
|
390
407
|
})
|
|
391
408
|
}
|
|
392
409
|
|
|
@@ -396,153 +413,96 @@ export async function tapByText(
|
|
|
396
413
|
options: TapTextOptions = {},
|
|
397
414
|
opts: { agent?: boolean; timing?: TapTimingOptions } = {},
|
|
398
415
|
): Promise<TapOutcome> {
|
|
399
|
-
const filterArg = JSON.stringify({
|
|
400
|
-
query,
|
|
401
|
-
exact: !!options.exact,
|
|
402
|
-
role: options.role ?? null,
|
|
403
|
-
within: options.within ?? null,
|
|
404
|
-
minX: options.minX ?? null,
|
|
405
|
-
maxX: options.maxX ?? null,
|
|
406
|
-
minY: options.minY ?? null,
|
|
407
|
-
maxY: options.maxY ?? null,
|
|
408
|
-
near: options.near ?? null,
|
|
409
|
-
nth: options.nth ?? null,
|
|
410
|
-
first: !!options.first,
|
|
411
|
-
})
|
|
412
|
-
|
|
413
416
|
return tapResolvedTarget(bridge, {
|
|
414
417
|
agent: opts.agent,
|
|
415
418
|
timing: opts.timing,
|
|
416
419
|
textFallback: query,
|
|
417
|
-
resolve: () =>
|
|
418
|
-
bridge
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
if (F.maxX !== null && !(ax <= F.maxX)) return false
|
|
438
|
-
if (F.minY !== null && !(ay >= F.minY)) return false
|
|
439
|
-
if (F.maxY !== null && !(ay <= F.maxY)) return false
|
|
440
|
-
if (F.within && !c.ancestorTestIDs.includes(F.within)) return false
|
|
441
|
-
return true
|
|
442
|
-
})
|
|
420
|
+
resolve: async () => {
|
|
421
|
+
const { tree } = await inspectTree(bridge, 50)
|
|
422
|
+
if (!Array.isArray(tree)) return { matched: 0, total: 0 }
|
|
423
|
+
const needle = options.exact ? query : query.toLowerCase()
|
|
424
|
+
let candidates = collectTextCandidates(tree).filter(({ node, ancestorTestIDs }) => {
|
|
425
|
+
const text = nodeText(node)
|
|
426
|
+
if (!text) return false
|
|
427
|
+
if (options.exact ? text !== needle : !text.toLowerCase().includes(needle)) {
|
|
428
|
+
return false
|
|
429
|
+
}
|
|
430
|
+
if (options.role && node.role !== options.role) return false
|
|
431
|
+
if (options.within && !ancestorTestIDs.includes(options.within)) return false
|
|
432
|
+
const ax = node.geometry.x
|
|
433
|
+
const ay = node.geometry.y
|
|
434
|
+
if (options.minX != null && !(ax >= options.minX)) return false
|
|
435
|
+
if (options.maxX != null && !(ax <= options.maxX)) return false
|
|
436
|
+
if (options.minY != null && !(ay >= options.minY)) return false
|
|
437
|
+
if (options.maxY != null && !(ay <= options.maxY)) return false
|
|
438
|
+
return true
|
|
439
|
+
})
|
|
443
440
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
} else {
|
|
456
|
-
candidates.sort((a, b) => {
|
|
457
|
-
const ay = (a.info.absolutePosition && a.info.absolutePosition.y) || 0
|
|
458
|
-
const by = (b.info.absolutePosition && b.info.absolutePosition.y) || 0
|
|
459
|
-
if (Math.abs(ay - by) > 2) return ay - by
|
|
460
|
-
const ax = (a.info.absolutePosition && a.info.absolutePosition.x) || 0
|
|
461
|
-
const bx = (b.info.absolutePosition && b.info.absolutePosition.x) || 0
|
|
462
|
-
return ax - bx
|
|
463
|
-
})
|
|
441
|
+
if (options.near) {
|
|
442
|
+
const { x, y } = options.near
|
|
443
|
+
candidates.sort(
|
|
444
|
+
(a, b) =>
|
|
445
|
+
Math.hypot(a.node.geometry.x - x, a.node.geometry.y - y) -
|
|
446
|
+
Math.hypot(b.node.geometry.x - x, b.node.geometry.y - y),
|
|
447
|
+
)
|
|
448
|
+
} else {
|
|
449
|
+
candidates.sort((a, b) => {
|
|
450
|
+
if (Math.abs(a.node.geometry.y - b.node.geometry.y) > 2) {
|
|
451
|
+
return a.node.geometry.y - b.node.geometry.y
|
|
464
452
|
}
|
|
453
|
+
return a.node.geometry.x - b.node.geometry.x
|
|
454
|
+
})
|
|
455
|
+
}
|
|
465
456
|
|
|
466
|
-
|
|
467
|
-
|
|
457
|
+
const total = candidates.length
|
|
458
|
+
if (total === 0) return { matched: 0, total: 0 }
|
|
468
459
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
height: Math.round(c.info.layout.height || 0),
|
|
490
|
-
}
|
|
491
|
-
: null,
|
|
492
|
-
ancestorTestIDs: (c.ancestorTestIDs || []).slice(0, 5),
|
|
493
|
-
})),
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
const picked = candidates[idx]
|
|
498
|
-
const n = picked.info
|
|
499
|
-
if (!n.absolutePosition || !n.layout) return { matched: 0, total }
|
|
500
|
-
|
|
501
|
-
const resolved =
|
|
502
|
-
typeof n.nodeId === 'number' &&
|
|
503
|
-
typeof t.resolveTapTarget === 'function'
|
|
504
|
-
? await t.resolveTapTarget(n.nodeId)
|
|
505
|
-
: null
|
|
506
|
-
const target = (resolved && resolved.target) || n
|
|
507
|
-
const cx =
|
|
508
|
-
resolved && typeof resolved.cx === 'number'
|
|
509
|
-
? resolved.cx
|
|
510
|
-
: n.absolutePosition.x + (n.layout.width || 0) / 2
|
|
511
|
-
const cy =
|
|
512
|
-
resolved && typeof resolved.cy === 'number'
|
|
513
|
-
? resolved.cy
|
|
514
|
-
: n.absolutePosition.y + (n.layout.height || 0) / 2
|
|
515
|
-
const scr = ${SCREEN_EVAL}
|
|
516
|
-
const offscreen =
|
|
517
|
-
!!scr && (cx < 0 || cy < 0 || cx > scr.width || cy > scr.height)
|
|
518
|
-
return {
|
|
519
|
-
cx,
|
|
520
|
-
cy,
|
|
521
|
-
...(offscreen ? { offscreen: true, screen: scr } : {}),
|
|
522
|
-
match: {
|
|
523
|
-
nodeId: n.nodeId ?? null,
|
|
524
|
-
id: n.id,
|
|
525
|
-
testID: n.testID,
|
|
526
|
-
type: n.type,
|
|
527
|
-
},
|
|
528
|
-
target: {
|
|
529
|
-
nodeId: target.nodeId ?? null,
|
|
530
|
-
id: target.id,
|
|
531
|
-
testID: target.testID,
|
|
532
|
-
text:
|
|
533
|
-
target.text ??
|
|
534
|
-
target.accessibilityLabel ??
|
|
535
|
-
n.text ??
|
|
536
|
-
n.accessibilityLabel ??
|
|
537
|
-
null,
|
|
538
|
-
type: target.type,
|
|
460
|
+
let idx = 0
|
|
461
|
+
if (options.nth != null) {
|
|
462
|
+
idx = options.nth < 0 ? total + options.nth : options.nth
|
|
463
|
+
if (idx < 0 || idx >= total) {
|
|
464
|
+
return { matched: 0, total, nthOutOfRange: true, nth: options.nth }
|
|
465
|
+
}
|
|
466
|
+
} else if (total > 1 && !options.first && !options.near) {
|
|
467
|
+
return {
|
|
468
|
+
ambiguous: true,
|
|
469
|
+
total,
|
|
470
|
+
candidates: candidates.slice(0, 10).map((candidate, index) => ({
|
|
471
|
+
idx: index,
|
|
472
|
+
nodeId: candidate.node.nodeId,
|
|
473
|
+
type: candidate.node.type,
|
|
474
|
+
testID: candidate.node.testID ?? null,
|
|
475
|
+
text: nodeText(candidate.node).slice(0, 60),
|
|
476
|
+
abs: { x: candidate.node.geometry.x, y: candidate.node.geometry.y },
|
|
477
|
+
layout: {
|
|
478
|
+
width: Math.round(candidate.node.geometry.width),
|
|
479
|
+
height: Math.round(candidate.node.geometry.height),
|
|
539
480
|
},
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
481
|
+
ancestorTestIDs: candidate.ancestorTestIDs.slice(0, 5),
|
|
482
|
+
})),
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const picked = candidates[idx]
|
|
487
|
+
if (!picked) return { matched: 0, total }
|
|
488
|
+
const node = picked.node
|
|
489
|
+
let resolved = node.testID
|
|
490
|
+
? await resolveTargetCoords(bridge, { mode: 'testid', value: node.testID })
|
|
491
|
+
: await resolveTargetCoords(bridge, {
|
|
492
|
+
mode: 'text',
|
|
493
|
+
value: nodeText(node) || query,
|
|
494
|
+
})
|
|
495
|
+
if (!resolved) {
|
|
496
|
+
resolved = {
|
|
497
|
+
x: node.geometry.x + node.geometry.width / 2,
|
|
498
|
+
y: node.geometry.y + node.geometry.height / 2,
|
|
499
|
+
testID: node.testID ?? null,
|
|
500
|
+
text: nodeText(node) || query,
|
|
501
|
+
type: node.type,
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
return payloadFromResolved(resolved, node, { total, idx })
|
|
505
|
+
},
|
|
546
506
|
})
|
|
547
507
|
}
|
|
548
508
|
|
|
@@ -551,72 +511,40 @@ export async function tapBest(
|
|
|
551
511
|
query: string,
|
|
552
512
|
opts: { agent?: boolean; timing?: TapTimingOptions } = {},
|
|
553
513
|
): Promise<TapOutcome> {
|
|
554
|
-
const arg = JSON.stringify(query)
|
|
555
514
|
return tapResolvedTarget(bridge, {
|
|
556
515
|
agent: opts.agent,
|
|
557
516
|
timing: opts.timing,
|
|
558
517
|
textFallback: query,
|
|
559
518
|
resolve: async () => {
|
|
560
|
-
const
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
nodeId: byText.nodeId ?? null,
|
|
588
|
-
id: byText.id,
|
|
589
|
-
testID: byText.testID,
|
|
590
|
-
type: byText.type,
|
|
591
|
-
text: byText.text,
|
|
592
|
-
absolutePosition: byText.absolutePosition,
|
|
593
|
-
layout: byText.layout,
|
|
594
|
-
},
|
|
595
|
-
screen: ${SCREEN_EVAL},
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
return { strategy: 'none' }
|
|
599
|
-
})()`,
|
|
600
|
-
})
|
|
601
|
-
if (!payload || !('node' in payload)) return payload
|
|
602
|
-
const node = payload.node
|
|
603
|
-
const cx = node.absolutePosition.x + node.layout.width / 2
|
|
604
|
-
const cy = node.absolutePosition.y + node.layout.height / 2
|
|
605
|
-
const screen = payload.screen ?? null
|
|
606
|
-
const offscreen =
|
|
607
|
-
!!screen && (cx < 0 || cy < 0 || cx > screen.width || cy > screen.height)
|
|
608
|
-
return {
|
|
609
|
-
...payload,
|
|
610
|
-
cx,
|
|
611
|
-
cy,
|
|
612
|
-
...(offscreen ? { offscreen: true, screen } : {}),
|
|
613
|
-
target: {
|
|
614
|
-
id: node.id,
|
|
615
|
-
testID: node.testID,
|
|
616
|
-
text: payload.strategy === 'text' ? query : node.text,
|
|
617
|
-
type: node.type,
|
|
618
|
-
},
|
|
519
|
+
const byId = await resolveTargetCoords(bridge, { mode: 'testid', value: query })
|
|
520
|
+
if (byId) {
|
|
521
|
+
return {
|
|
522
|
+
...payloadFromResolved(byId, byId),
|
|
523
|
+
strategy: 'testid',
|
|
524
|
+
node: {
|
|
525
|
+
nodeId: null,
|
|
526
|
+
id: byId.testID ?? query,
|
|
527
|
+
testID: byId.testID ?? query,
|
|
528
|
+
type: byId.type ?? null,
|
|
529
|
+
text: byId.text ?? null,
|
|
530
|
+
},
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
const byText = await resolveTargetCoords(bridge, { mode: 'text', value: query })
|
|
534
|
+
if (byText) {
|
|
535
|
+
return {
|
|
536
|
+
...payloadFromResolved(byText, byText),
|
|
537
|
+
strategy: 'text',
|
|
538
|
+
node: {
|
|
539
|
+
nodeId: null,
|
|
540
|
+
id: byText.testID ?? null,
|
|
541
|
+
testID: byText.testID ?? null,
|
|
542
|
+
type: byText.type ?? null,
|
|
543
|
+
text: byText.text ?? query,
|
|
544
|
+
},
|
|
545
|
+
}
|
|
619
546
|
}
|
|
547
|
+
return { strategy: 'none' }
|
|
620
548
|
},
|
|
621
549
|
})
|
|
622
550
|
}
|