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
package/dist-lib/sdk.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.419 | (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;
|
|
@@ -98,658 +98,10 @@ __export(sdk_exports, {
|
|
|
98
98
|
});
|
|
99
99
|
module.exports = __toCommonJS(sdk_exports);
|
|
100
100
|
|
|
101
|
-
// cli/commands/inspect/settling.ts
|
|
102
|
-
async function waitForSootsimIdle({
|
|
103
|
-
bridge,
|
|
104
|
-
simId,
|
|
105
|
-
maxMs,
|
|
106
|
-
pollMs = 50,
|
|
107
|
-
stablePolls = 3,
|
|
108
|
-
strict = false
|
|
109
|
-
}) {
|
|
110
|
-
const result = await bridge.send(
|
|
111
|
-
{
|
|
112
|
-
type: "evaluate",
|
|
113
|
-
simId,
|
|
114
|
-
code: `(async () => {
|
|
115
|
-
const start = Date.now()
|
|
116
|
-
const deadline = start + ${Math.max(0, Math.round(maxMs))}
|
|
117
|
-
const pollMs = ${Math.max(1, Math.round(pollMs))}
|
|
118
|
-
const requiredStablePolls = ${Math.max(1, Math.round(stablePolls))}
|
|
119
|
-
const strict = ${strict ? "true" : "false"}
|
|
120
|
-
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
121
|
-
|
|
122
|
-
// route-aware settle: a tap that pushes/pops a screen is async \u2014 the
|
|
123
|
-
// old screen still renders for a moment, then the new one mounts and
|
|
124
|
-
// its content loads. a pure layout-hash check reports "idle" on the
|
|
125
|
-
// outgoing screen or on the incoming skeleton, so a driver re-taps
|
|
126
|
-
// and stacks duplicate navigations. drain any in-flight screen
|
|
127
|
-
// transition FIRST, bounded by the overall budget. when no
|
|
128
|
-
// transition is happening this returns in ~80ms (startWindowMs), so
|
|
129
|
-
// a plain button tap barely pays for it.
|
|
130
|
-
try {
|
|
131
|
-
const wfst = window.__sootsimTest?.waitForScreenTransitions
|
|
132
|
-
if (typeof wfst === 'function') {
|
|
133
|
-
const remaining = deadline - Date.now()
|
|
134
|
-
if (remaining > 120) {
|
|
135
|
-
await wfst({
|
|
136
|
-
timeoutMs: Math.min(remaining - 80, 4000),
|
|
137
|
-
settleMs: 64,
|
|
138
|
-
startWindowMs: 80,
|
|
139
|
-
})
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
} catch {}
|
|
143
|
-
|
|
144
|
-
const readSnapshot = async () => {
|
|
145
|
-
let animating = false
|
|
146
|
-
let layoutDirty = false
|
|
147
|
-
let pendingFetches = 0
|
|
148
|
-
let requestTotal = 0
|
|
149
|
-
let requestInFlight = 0
|
|
150
|
-
let renderStatsAvailable = false
|
|
151
|
-
try {
|
|
152
|
-
const stats = await window.__sootsimRenderHost?.queryStats?.()
|
|
153
|
-
if (stats) {
|
|
154
|
-
renderStatsAvailable = true
|
|
155
|
-
animating =
|
|
156
|
-
stats.hasActiveAnims === true ||
|
|
157
|
-
stats.hasActiveNativeAnimations === true ||
|
|
158
|
-
stats.hasPendingAnimationFrames === true
|
|
159
|
-
layoutDirty =
|
|
160
|
-
stats.layoutDirty === true && stats.renderRequested === true
|
|
161
|
-
// a freshly-pushed screen showing a skeleton is layout-stable
|
|
162
|
-
// but not actually settled \u2014 its data/images are still in
|
|
163
|
-
// flight. treat bounded image-loader fetches as not-idle so
|
|
164
|
-
// settle waits for real content, capped by the budget.
|
|
165
|
-
const pf = stats.memory && stats.memory.imageLoader
|
|
166
|
-
? stats.memory.imageLoader.pendingFetches
|
|
167
|
-
: 0
|
|
168
|
-
pendingFetches = typeof pf === 'number' ? pf : 0
|
|
169
|
-
}
|
|
170
|
-
} catch {}
|
|
171
|
-
try {
|
|
172
|
-
const counts = await window.__sootsimTest?.getRequestCounts?.()
|
|
173
|
-
requestTotal = typeof counts?.total === 'number' ? counts.total : 0
|
|
174
|
-
requestInFlight = typeof counts?.inFlight === 'number' ? counts.inFlight : 0
|
|
175
|
-
} catch {}
|
|
176
|
-
const root = window.__sootsimRoot
|
|
177
|
-
const nodes = []
|
|
178
|
-
if (root) {
|
|
179
|
-
const walk = (n) => {
|
|
180
|
-
if (n.layout && n.layout.width > 0) {
|
|
181
|
-
nodes.push(Math.round(n.layout.x) + ',' + Math.round(n.layout.y) + ',' + Math.round(n.layout.width))
|
|
182
|
-
}
|
|
183
|
-
for (const c of n.children || []) walk(c)
|
|
184
|
-
}
|
|
185
|
-
walk(root)
|
|
186
|
-
}
|
|
187
|
-
return { layout: nodes.join(';'), animating, layoutDirty, pendingFetches, renderStatsAvailable, requestTotal, requestInFlight }
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// how long generic background network is allowed to keep us waiting AFTER
|
|
191
|
-
// the screen is otherwise visually settled. apps with continuous traffic
|
|
192
|
-
// (polling, realtime sockets, a tap that fires 20-50 fetches) never reach
|
|
193
|
-
// requestInFlight===0 / a stable requestTotal, so requiring full network
|
|
194
|
-
// quiet burns the ENTIRE budget on every command \u2014 the leading idle that
|
|
195
|
-
// failed PR-preview recordings for network-heavy apps (3pc fight-pulse).
|
|
196
|
-
// layout-stability + the image-loader already prove visible content
|
|
197
|
-
// loaded (a data load changes layout; an image load shows in the loader),
|
|
198
|
-
// so the generic request counters are a courtesy, not a gate: honor them
|
|
199
|
-
// only within this grace once the screen is visually still.
|
|
200
|
-
const networkQuietGraceMs = 1200
|
|
201
|
-
let lastLayout = ''
|
|
202
|
-
let lastRequestTotal = -1
|
|
203
|
-
let stable = 0
|
|
204
|
-
let visuallyStillSince = 0
|
|
205
|
-
while (Date.now() < deadline) {
|
|
206
|
-
const snapshot = await readSnapshot()
|
|
207
|
-
const strictOk =
|
|
208
|
-
!strict ||
|
|
209
|
-
(snapshot.renderStatsAvailable && !snapshot.animating && !snapshot.layoutDirty)
|
|
210
|
-
// visually settled: no animations, no image content loading, layout
|
|
211
|
-
// unchanged since the last poll.
|
|
212
|
-
const visuallyStill =
|
|
213
|
-
strictOk && snapshot.pendingFetches === 0 && snapshot.layout === lastLayout
|
|
214
|
-
// generic network quiet: nothing in flight and no new requests issued.
|
|
215
|
-
const networkQuiet =
|
|
216
|
-
snapshot.requestInFlight === 0 && snapshot.requestTotal === lastRequestTotal
|
|
217
|
-
if (visuallyStill) {
|
|
218
|
-
stable++
|
|
219
|
-
if (visuallyStillSince === 0) visuallyStillSince = Date.now()
|
|
220
|
-
// settle once the screen has held still long enough AND either the
|
|
221
|
-
// network is genuinely quiet OR it has refused to quiet within the
|
|
222
|
-
// grace (continuous background traffic must not block forever).
|
|
223
|
-
if (
|
|
224
|
-
stable >= requiredStablePolls &&
|
|
225
|
-
(networkQuiet || Date.now() - visuallyStillSince >= networkQuietGraceMs)
|
|
226
|
-
) {
|
|
227
|
-
return { settled: true, elapsed: Date.now() - start }
|
|
228
|
-
}
|
|
229
|
-
} else {
|
|
230
|
-
stable = 0
|
|
231
|
-
visuallyStillSince = 0
|
|
232
|
-
}
|
|
233
|
-
lastLayout = snapshot.layout
|
|
234
|
-
lastRequestTotal = snapshot.requestTotal
|
|
235
|
-
await sleep(pollMs)
|
|
236
|
-
}
|
|
237
|
-
return { settled: false, elapsed: Date.now() - start }
|
|
238
|
-
})()`
|
|
239
|
-
},
|
|
240
|
-
{
|
|
241
|
-
timeoutMs: maxMs + 1e3
|
|
242
|
-
}
|
|
243
|
-
);
|
|
244
|
-
const { elapsed, settled } = result ?? {};
|
|
245
|
-
return {
|
|
246
|
-
elapsed: typeof elapsed === "number" ? elapsed : maxMs,
|
|
247
|
-
settled: settled === true
|
|
248
|
-
};
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// cli/commands/inspect/actions.ts
|
|
252
|
-
var SCREEN_EVAL = `(() => {
|
|
253
|
-
const spec = window.SootSim?.state?.engineSnapshot?.windowState?.deviceSpec
|
|
254
|
-
return spec && spec.width > 0 && spec.height > 0
|
|
255
|
-
? { width: spec.width, height: spec.height }
|
|
256
|
-
: null
|
|
257
|
-
})()`;
|
|
258
|
-
var DEFAULT_AGENT_TIMING = {
|
|
259
|
-
initialWaitMs: 3e3,
|
|
260
|
-
deadlineMs: 5e3,
|
|
261
|
-
retryWaitMs: 700
|
|
262
|
-
};
|
|
263
|
-
var DEFAULT_INTERACTIVE_TIMING = {
|
|
264
|
-
initialWaitMs: 1200,
|
|
265
|
-
deadlineMs: 2500,
|
|
266
|
-
retryWaitMs: 700
|
|
267
|
-
};
|
|
268
|
-
function tapTiming(agent, opts = {}) {
|
|
269
|
-
return {
|
|
270
|
-
...agent ? DEFAULT_AGENT_TIMING : DEFAULT_INTERACTIVE_TIMING,
|
|
271
|
-
...opts
|
|
272
|
-
};
|
|
273
|
-
}
|
|
274
|
-
function isTapSuccess(result) {
|
|
275
|
-
if (!result || result.hit === false || result.ok === false) return false;
|
|
276
|
-
if (result.requestedTargetMatched === false) return false;
|
|
277
|
-
if (result.pointerTapHandled === false && !result.keyboardOpened && !result.isTextInput) {
|
|
278
|
-
return false;
|
|
279
|
-
}
|
|
280
|
-
return true;
|
|
281
|
-
}
|
|
282
|
-
function tapTargetFromPayload(payload, textFallback) {
|
|
283
|
-
return {
|
|
284
|
-
nodeId: payload.target?.nodeId ?? payload.match?.nodeId ?? payload.node?.nodeId ?? null,
|
|
285
|
-
id: payload.target?.id ?? payload.match?.id ?? payload.node?.id ?? null,
|
|
286
|
-
testID: payload.target?.testID ?? payload.match?.testID ?? payload.node?.testID ?? null,
|
|
287
|
-
text: payload.target?.text ?? payload.target?.accessibilityLabel ?? payload.match?.text ?? payload.match?.accessibilityLabel ?? payload.node?.text ?? textFallback ?? null,
|
|
288
|
-
type: payload.target?.type ?? payload.match?.type ?? payload.node?.type ?? null
|
|
289
|
-
};
|
|
290
|
-
}
|
|
291
|
-
async function tapResolvedTarget(bridge, args) {
|
|
292
|
-
const timing = tapTiming(!!args.agent, args.timing);
|
|
293
|
-
let attempts = 0;
|
|
294
|
-
let lastPayload = null;
|
|
295
|
-
let lastResult = null;
|
|
296
|
-
try {
|
|
297
|
-
await waitForSootsimIdle({
|
|
298
|
-
bridge,
|
|
299
|
-
maxMs: timing.initialWaitMs,
|
|
300
|
-
pollMs: 32,
|
|
301
|
-
stablePolls: 2
|
|
302
|
-
});
|
|
303
|
-
} catch {
|
|
304
|
-
}
|
|
305
|
-
const deadline = Date.now() + timing.deadlineMs;
|
|
306
|
-
let lastOffscreenAt = null;
|
|
307
|
-
while (Date.now() <= deadline || attempts === 0) {
|
|
308
|
-
attempts++;
|
|
309
|
-
const payload = await args.resolve();
|
|
310
|
-
lastPayload = payload;
|
|
311
|
-
if (payload?.error === "bridge-not-ready" || payload?.ambiguous || payload?.nthOutOfRange) {
|
|
312
|
-
return { payload, result: null, attempts, failure: "special" };
|
|
313
|
-
}
|
|
314
|
-
if (payload && typeof payload.cx === "number" && typeof payload.cy === "number") {
|
|
315
|
-
if (payload.offscreen) {
|
|
316
|
-
lastResult = {
|
|
317
|
-
hit: false,
|
|
318
|
-
reason: "offscreen",
|
|
319
|
-
x: payload.cx,
|
|
320
|
-
y: payload.cy,
|
|
321
|
-
screen: payload.screen
|
|
322
|
-
};
|
|
323
|
-
const at = `${Math.round(payload.cx)},${Math.round(payload.cy)}`;
|
|
324
|
-
if (lastOffscreenAt === at) {
|
|
325
|
-
return { payload, result: lastResult, attempts, failure: "missed" };
|
|
326
|
-
}
|
|
327
|
-
lastOffscreenAt = at;
|
|
328
|
-
} else {
|
|
329
|
-
lastOffscreenAt = null;
|
|
330
|
-
const requestedTarget = tapTargetFromPayload(payload, args.textFallback);
|
|
331
|
-
const verification = await bridge.send({
|
|
332
|
-
type: "evaluate",
|
|
333
|
-
code: `(async () => {
|
|
334
|
-
const t = window.__sootsimTest
|
|
335
|
-
if (
|
|
336
|
-
!t ||
|
|
337
|
-
typeof t.inspectAt !== 'function' ||
|
|
338
|
-
typeof t.resolveTapTarget !== 'function'
|
|
339
|
-
) {
|
|
340
|
-
return { error: 'tap-target-inspection-unavailable' }
|
|
341
|
-
}
|
|
342
|
-
const requestedTarget = ${JSON.stringify(requestedTarget)}
|
|
343
|
-
const inspected = await t.inspectAt(${payload.cx}, ${payload.cy})
|
|
344
|
-
const resolved =
|
|
345
|
-
inspected && typeof inspected.nodeId === 'number'
|
|
346
|
-
? await t.resolveTapTarget(inspected.nodeId)
|
|
347
|
-
: null
|
|
348
|
-
const node = (resolved && resolved.target) || inspected
|
|
349
|
-
const coordinateTarget = node
|
|
350
|
-
? {
|
|
351
|
-
nodeId: node.nodeId ?? null,
|
|
352
|
-
id: node.id ?? null,
|
|
353
|
-
testID: node.testID ?? null,
|
|
354
|
-
text: node.text ?? node.accessibilityLabel ?? null,
|
|
355
|
-
type: node.type ?? null,
|
|
356
|
-
}
|
|
357
|
-
: null
|
|
358
|
-
const coordinateAncestors = Array.isArray(inspected?.ancestors)
|
|
359
|
-
? inspected.ancestors
|
|
360
|
-
: []
|
|
361
|
-
const ancestorMatchesRequested = coordinateAncestors.some(
|
|
362
|
-
(ancestor) =>
|
|
363
|
-
(typeof requestedTarget.nodeId === 'number' &&
|
|
364
|
-
ancestor?.nodeId === requestedTarget.nodeId) ||
|
|
365
|
-
(requestedTarget.testID &&
|
|
366
|
-
ancestor?.testID === requestedTarget.testID) ||
|
|
367
|
-
(requestedTarget.id && ancestor?.id === requestedTarget.id),
|
|
368
|
-
)
|
|
369
|
-
const requestedTargetMatched =
|
|
370
|
-
coordinateTarget === null
|
|
371
|
-
? false
|
|
372
|
-
: typeof requestedTarget.nodeId === 'number' &&
|
|
373
|
-
typeof coordinateTarget.nodeId === 'number'
|
|
374
|
-
? requestedTarget.nodeId === coordinateTarget.nodeId ||
|
|
375
|
-
ancestorMatchesRequested
|
|
376
|
-
: requestedTarget.testID && coordinateTarget.testID
|
|
377
|
-
? requestedTarget.testID === coordinateTarget.testID
|
|
378
|
-
|| ancestorMatchesRequested
|
|
379
|
-
: requestedTarget.id && coordinateTarget.id
|
|
380
|
-
? requestedTarget.id === coordinateTarget.id
|
|
381
|
-
|| ancestorMatchesRequested
|
|
382
|
-
: ancestorMatchesRequested
|
|
383
|
-
if (!requestedTargetMatched) {
|
|
384
|
-
return {
|
|
385
|
-
requestedTargetMatched,
|
|
386
|
-
requestedTarget,
|
|
387
|
-
coordinateTarget,
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
if (typeof t.activatePressAt !== 'function') {
|
|
391
|
-
return { error: 'tap-target-activation-unavailable' }
|
|
392
|
-
}
|
|
393
|
-
const activation = await t.activatePressAt(
|
|
394
|
-
${payload.cx},
|
|
395
|
-
${payload.cy},
|
|
396
|
-
requestedTarget.nodeId,
|
|
397
|
-
)
|
|
398
|
-
if (activation?.ok) {
|
|
399
|
-
window.dispatchEvent(
|
|
400
|
-
new CustomEvent('sootsim:agentAction', {
|
|
401
|
-
detail: {
|
|
402
|
-
type: 'tap',
|
|
403
|
-
x: ${payload.cx},
|
|
404
|
-
y: ${payload.cy},
|
|
405
|
-
target: requestedTarget,
|
|
406
|
-
},
|
|
407
|
-
}),
|
|
408
|
-
)
|
|
409
|
-
}
|
|
410
|
-
return {
|
|
411
|
-
activation,
|
|
412
|
-
requestedTargetMatched,
|
|
413
|
-
requestedTarget,
|
|
414
|
-
coordinateTarget,
|
|
415
|
-
}
|
|
416
|
-
})()`
|
|
417
|
-
});
|
|
418
|
-
if (verification?.error) {
|
|
419
|
-
return {
|
|
420
|
-
payload,
|
|
421
|
-
result: {
|
|
422
|
-
hit: false,
|
|
423
|
-
reason: verification.error,
|
|
424
|
-
requestedTarget
|
|
425
|
-
},
|
|
426
|
-
attempts,
|
|
427
|
-
failure: "special"
|
|
428
|
-
};
|
|
429
|
-
}
|
|
430
|
-
if (verification?.requestedTargetMatched === false) {
|
|
431
|
-
return {
|
|
432
|
-
payload,
|
|
433
|
-
result: {
|
|
434
|
-
hit: false,
|
|
435
|
-
reason: "target-covered",
|
|
436
|
-
...verification
|
|
437
|
-
},
|
|
438
|
-
attempts,
|
|
439
|
-
failure: "missed"
|
|
440
|
-
};
|
|
441
|
-
}
|
|
442
|
-
const dispatched = verification?.activation?.tap ?? verification?.activation;
|
|
443
|
-
const result = dispatched && typeof dispatched === "object" ? { ...dispatched, ...verification?.activation, ...verification } : { hit: !!dispatched, ...verification };
|
|
444
|
-
lastResult = result;
|
|
445
|
-
if (isTapSuccess(result)) return { payload, result, attempts };
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
const remaining = deadline - Date.now();
|
|
449
|
-
if (remaining <= 0) break;
|
|
450
|
-
try {
|
|
451
|
-
await waitForSootsimIdle({
|
|
452
|
-
bridge,
|
|
453
|
-
maxMs: Math.min(remaining, timing.retryWaitMs),
|
|
454
|
-
pollMs: 32,
|
|
455
|
-
stablePolls: 2
|
|
456
|
-
});
|
|
457
|
-
} catch {
|
|
458
|
-
await new Promise((resolve) => setTimeout(resolve, Math.min(120, remaining)));
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
return {
|
|
462
|
-
payload: lastPayload,
|
|
463
|
-
result: lastResult,
|
|
464
|
-
attempts,
|
|
465
|
-
failure: lastPayload && typeof lastPayload.cx === "number" ? "missed" : "not-found"
|
|
466
|
-
};
|
|
467
|
-
}
|
|
468
|
-
async function tapCoordinates(bridge, x, y, target) {
|
|
469
|
-
return bridge.send({
|
|
470
|
-
type: "tap",
|
|
471
|
-
x,
|
|
472
|
-
y,
|
|
473
|
-
...target ? { target } : {}
|
|
474
|
-
});
|
|
475
|
-
}
|
|
476
|
-
async function tapById(bridge, query, opts = {}) {
|
|
477
|
-
const arg = JSON.stringify(query);
|
|
478
|
-
return tapResolvedTarget(bridge, {
|
|
479
|
-
agent: opts.agent,
|
|
480
|
-
timing: opts.timing,
|
|
481
|
-
resolve: () => bridge.send({
|
|
482
|
-
type: "evaluate",
|
|
483
|
-
code: `(async () => {
|
|
484
|
-
const t = window.__sootsimTest
|
|
485
|
-
if (!t) return null
|
|
486
|
-
const n = (await t.findByTestId(${arg})) || (await t.findById(${arg}))
|
|
487
|
-
if (!n || !n.absolutePosition || !n.layout) return { cx: null }
|
|
488
|
-
const resolved =
|
|
489
|
-
typeof n.nodeId === 'number' && typeof t.resolveTapTarget === 'function'
|
|
490
|
-
? await t.resolveTapTarget(n.nodeId)
|
|
491
|
-
: null
|
|
492
|
-
const target = (resolved && resolved.target) || n
|
|
493
|
-
const cx =
|
|
494
|
-
resolved && typeof resolved.cx === 'number'
|
|
495
|
-
? resolved.cx
|
|
496
|
-
: n.absolutePosition.x + (n.layout.width || 0) / 2
|
|
497
|
-
const cy =
|
|
498
|
-
resolved && typeof resolved.cy === 'number'
|
|
499
|
-
? resolved.cy
|
|
500
|
-
: n.absolutePosition.y + (n.layout.height || 0) / 2
|
|
501
|
-
const scr = ${SCREEN_EVAL}
|
|
502
|
-
const offscreen =
|
|
503
|
-
!!scr && (cx < 0 || cy < 0 || cx > scr.width || cy > scr.height)
|
|
504
|
-
return {
|
|
505
|
-
cx,
|
|
506
|
-
cy,
|
|
507
|
-
...(offscreen ? { offscreen: true, screen: scr } : {}),
|
|
508
|
-
match: {
|
|
509
|
-
nodeId: n.nodeId ?? null,
|
|
510
|
-
id: n.id,
|
|
511
|
-
testID: n.testID,
|
|
512
|
-
text: n.text ?? n.accessibilityLabel ?? null,
|
|
513
|
-
type: n.type,
|
|
514
|
-
},
|
|
515
|
-
target: {
|
|
516
|
-
nodeId: target.nodeId ?? null,
|
|
517
|
-
id: target.id,
|
|
518
|
-
testID: target.testID,
|
|
519
|
-
text:
|
|
520
|
-
target.text ??
|
|
521
|
-
target.accessibilityLabel ??
|
|
522
|
-
n.text ??
|
|
523
|
-
n.accessibilityLabel ??
|
|
524
|
-
null,
|
|
525
|
-
type: target.type,
|
|
526
|
-
},
|
|
527
|
-
strategy: (resolved && resolved.strategy) || 'matched-node',
|
|
528
|
-
}
|
|
529
|
-
})()`
|
|
530
|
-
})
|
|
531
|
-
});
|
|
532
|
-
}
|
|
533
|
-
async function tapByText(bridge, query, options = {}, opts = {}) {
|
|
534
|
-
const filterArg = JSON.stringify({
|
|
535
|
-
query,
|
|
536
|
-
exact: !!options.exact,
|
|
537
|
-
role: options.role ?? null,
|
|
538
|
-
within: options.within ?? null,
|
|
539
|
-
minX: options.minX ?? null,
|
|
540
|
-
maxX: options.maxX ?? null,
|
|
541
|
-
minY: options.minY ?? null,
|
|
542
|
-
maxY: options.maxY ?? null,
|
|
543
|
-
near: options.near ?? null,
|
|
544
|
-
nth: options.nth ?? null,
|
|
545
|
-
first: !!options.first
|
|
546
|
-
});
|
|
547
|
-
return tapResolvedTarget(bridge, {
|
|
548
|
-
agent: opts.agent,
|
|
549
|
-
timing: opts.timing,
|
|
550
|
-
textFallback: query,
|
|
551
|
-
resolve: () => bridge.send({
|
|
552
|
-
type: "evaluate",
|
|
553
|
-
code: `(async () => {
|
|
554
|
-
const t = window.__sootsimTest
|
|
555
|
-
if (!t) return { error: 'bridge-not-ready' }
|
|
556
|
-
const F = ${filterArg}
|
|
557
|
-
|
|
558
|
-
const res = await t.queryTextCandidates({
|
|
559
|
-
query: F.query,
|
|
560
|
-
exact: !!F.exact,
|
|
561
|
-
...(F.role ? { role: F.role } : {}),
|
|
562
|
-
})
|
|
563
|
-
|
|
564
|
-
let candidates = res.candidates || []
|
|
565
|
-
|
|
566
|
-
candidates = candidates.filter((c) => {
|
|
567
|
-
const ax = c.info.absolutePosition && c.info.absolutePosition.x
|
|
568
|
-
const ay = c.info.absolutePosition && c.info.absolutePosition.y
|
|
569
|
-
if (F.minX !== null && !(ax >= F.minX)) return false
|
|
570
|
-
if (F.maxX !== null && !(ax <= F.maxX)) return false
|
|
571
|
-
if (F.minY !== null && !(ay >= F.minY)) return false
|
|
572
|
-
if (F.maxY !== null && !(ay <= F.maxY)) return false
|
|
573
|
-
if (F.within && !c.ancestorTestIDs.includes(F.within)) return false
|
|
574
|
-
return true
|
|
575
|
-
})
|
|
576
|
-
|
|
577
|
-
if (F.near) {
|
|
578
|
-
candidates.sort((a, b) => {
|
|
579
|
-
const ax = (a.info.absolutePosition && a.info.absolutePosition.x) || 0
|
|
580
|
-
const ay = (a.info.absolutePosition && a.info.absolutePosition.y) || 0
|
|
581
|
-
const bx = (b.info.absolutePosition && b.info.absolutePosition.x) || 0
|
|
582
|
-
const by = (b.info.absolutePosition && b.info.absolutePosition.y) || 0
|
|
583
|
-
return (
|
|
584
|
-
Math.hypot(ax - F.near.x, ay - F.near.y) -
|
|
585
|
-
Math.hypot(bx - F.near.x, by - F.near.y)
|
|
586
|
-
)
|
|
587
|
-
})
|
|
588
|
-
} else {
|
|
589
|
-
candidates.sort((a, b) => {
|
|
590
|
-
const ay = (a.info.absolutePosition && a.info.absolutePosition.y) || 0
|
|
591
|
-
const by = (b.info.absolutePosition && b.info.absolutePosition.y) || 0
|
|
592
|
-
if (Math.abs(ay - by) > 2) return ay - by
|
|
593
|
-
const ax = (a.info.absolutePosition && a.info.absolutePosition.x) || 0
|
|
594
|
-
const bx = (b.info.absolutePosition && b.info.absolutePosition.x) || 0
|
|
595
|
-
return ax - bx
|
|
596
|
-
})
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
const total = candidates.length
|
|
600
|
-
if (total === 0) return { matched: 0, total: 0 }
|
|
601
|
-
|
|
602
|
-
let idx = 0
|
|
603
|
-
if (F.nth !== null) {
|
|
604
|
-
idx = F.nth < 0 ? total + F.nth : F.nth
|
|
605
|
-
if (idx < 0 || idx >= total) {
|
|
606
|
-
return { matched: 0, total, nthOutOfRange: true, nth: F.nth }
|
|
607
|
-
}
|
|
608
|
-
} else if (total > 1 && !F.first && !F.near) {
|
|
609
|
-
return {
|
|
610
|
-
ambiguous: true,
|
|
611
|
-
total,
|
|
612
|
-
candidates: candidates.slice(0, 10).map((c, i) => ({
|
|
613
|
-
idx: i,
|
|
614
|
-
nodeId: c.info.nodeId,
|
|
615
|
-
type: c.info.type,
|
|
616
|
-
testID: c.info.testID,
|
|
617
|
-
text: (c.info.text || '').slice(0, 60),
|
|
618
|
-
abs: c.info.absolutePosition,
|
|
619
|
-
layout: c.info.layout
|
|
620
|
-
? {
|
|
621
|
-
width: Math.round(c.info.layout.width || 0),
|
|
622
|
-
height: Math.round(c.info.layout.height || 0),
|
|
623
|
-
}
|
|
624
|
-
: null,
|
|
625
|
-
ancestorTestIDs: (c.ancestorTestIDs || []).slice(0, 5),
|
|
626
|
-
})),
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
const picked = candidates[idx]
|
|
631
|
-
const n = picked.info
|
|
632
|
-
if (!n.absolutePosition || !n.layout) return { matched: 0, total }
|
|
633
|
-
|
|
634
|
-
const resolved =
|
|
635
|
-
typeof n.nodeId === 'number' &&
|
|
636
|
-
typeof t.resolveTapTarget === 'function'
|
|
637
|
-
? await t.resolveTapTarget(n.nodeId)
|
|
638
|
-
: null
|
|
639
|
-
const target = (resolved && resolved.target) || n
|
|
640
|
-
const cx =
|
|
641
|
-
resolved && typeof resolved.cx === 'number'
|
|
642
|
-
? resolved.cx
|
|
643
|
-
: n.absolutePosition.x + (n.layout.width || 0) / 2
|
|
644
|
-
const cy =
|
|
645
|
-
resolved && typeof resolved.cy === 'number'
|
|
646
|
-
? resolved.cy
|
|
647
|
-
: n.absolutePosition.y + (n.layout.height || 0) / 2
|
|
648
|
-
const scr = ${SCREEN_EVAL}
|
|
649
|
-
const offscreen =
|
|
650
|
-
!!scr && (cx < 0 || cy < 0 || cx > scr.width || cy > scr.height)
|
|
651
|
-
return {
|
|
652
|
-
cx,
|
|
653
|
-
cy,
|
|
654
|
-
...(offscreen ? { offscreen: true, screen: scr } : {}),
|
|
655
|
-
match: {
|
|
656
|
-
nodeId: n.nodeId ?? null,
|
|
657
|
-
id: n.id,
|
|
658
|
-
testID: n.testID,
|
|
659
|
-
type: n.type,
|
|
660
|
-
},
|
|
661
|
-
target: {
|
|
662
|
-
nodeId: target.nodeId ?? null,
|
|
663
|
-
id: target.id,
|
|
664
|
-
testID: target.testID,
|
|
665
|
-
text:
|
|
666
|
-
target.text ??
|
|
667
|
-
target.accessibilityLabel ??
|
|
668
|
-
n.text ??
|
|
669
|
-
n.accessibilityLabel ??
|
|
670
|
-
null,
|
|
671
|
-
type: target.type,
|
|
672
|
-
},
|
|
673
|
-
strategy: (resolved && resolved.strategy) || 'matched-node',
|
|
674
|
-
total,
|
|
675
|
-
idx,
|
|
676
|
-
}
|
|
677
|
-
})()`
|
|
678
|
-
})
|
|
679
|
-
});
|
|
680
|
-
}
|
|
681
|
-
async function tapBest(bridge, query, opts = {}) {
|
|
682
|
-
const arg = JSON.stringify(query);
|
|
683
|
-
return tapResolvedTarget(bridge, {
|
|
684
|
-
agent: opts.agent,
|
|
685
|
-
timing: opts.timing,
|
|
686
|
-
textFallback: query,
|
|
687
|
-
resolve: async () => {
|
|
688
|
-
const payload = await bridge.send({
|
|
689
|
-
type: "evaluate",
|
|
690
|
-
code: `(async () => {
|
|
691
|
-
const t = window.__sootsimTest
|
|
692
|
-
if (!t) return { error: 'bridge-not-ready' }
|
|
693
|
-
const byTestId =
|
|
694
|
-
(await t.findByTestId(${arg})) || (await t.findById(${arg}))
|
|
695
|
-
if (byTestId && byTestId.absolutePosition && byTestId.layout) {
|
|
696
|
-
return {
|
|
697
|
-
strategy: 'testid',
|
|
698
|
-
node: {
|
|
699
|
-
nodeId: byTestId.nodeId ?? null,
|
|
700
|
-
id: byTestId.id,
|
|
701
|
-
testID: byTestId.testID,
|
|
702
|
-
type: byTestId.type,
|
|
703
|
-
text: byTestId.text,
|
|
704
|
-
absolutePosition: byTestId.absolutePosition,
|
|
705
|
-
layout: byTestId.layout,
|
|
706
|
-
},
|
|
707
|
-
screen: ${SCREEN_EVAL},
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
const byText = await t.findByText(${arg})
|
|
711
|
-
if (byText && byText.absolutePosition && byText.layout) {
|
|
712
|
-
return {
|
|
713
|
-
strategy: 'text',
|
|
714
|
-
node: {
|
|
715
|
-
nodeId: byText.nodeId ?? null,
|
|
716
|
-
id: byText.id,
|
|
717
|
-
testID: byText.testID,
|
|
718
|
-
type: byText.type,
|
|
719
|
-
text: byText.text,
|
|
720
|
-
absolutePosition: byText.absolutePosition,
|
|
721
|
-
layout: byText.layout,
|
|
722
|
-
},
|
|
723
|
-
screen: ${SCREEN_EVAL},
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
return { strategy: 'none' }
|
|
727
|
-
})()`
|
|
728
|
-
});
|
|
729
|
-
if (!payload || !("node" in payload)) return payload;
|
|
730
|
-
const node = payload.node;
|
|
731
|
-
const cx = node.absolutePosition.x + node.layout.width / 2;
|
|
732
|
-
const cy = node.absolutePosition.y + node.layout.height / 2;
|
|
733
|
-
const screen = payload.screen ?? null;
|
|
734
|
-
const offscreen = !!screen && (cx < 0 || cy < 0 || cx > screen.width || cy > screen.height);
|
|
735
|
-
return {
|
|
736
|
-
...payload,
|
|
737
|
-
cx,
|
|
738
|
-
cy,
|
|
739
|
-
...offscreen ? { offscreen: true, screen } : {},
|
|
740
|
-
target: {
|
|
741
|
-
id: node.id,
|
|
742
|
-
testID: node.testID,
|
|
743
|
-
text: payload.strategy === "text" ? query : node.text,
|
|
744
|
-
type: node.type
|
|
745
|
-
}
|
|
746
|
-
};
|
|
747
|
-
}
|
|
748
|
-
});
|
|
749
|
-
}
|
|
750
|
-
|
|
751
101
|
// src/bridge-contract.ts
|
|
752
102
|
var SEMANTIC_READY_CONTENT_NODE_FLOOR = 100;
|
|
103
|
+
var PUBLIC_NO_NETWORK_MARKER = "public RNX Cloud simulators have no network";
|
|
104
|
+
var HEADLESS_TENANT_FETCH_HUNG_MARKER = "headless tenant fetch hung";
|
|
753
105
|
function readyProbeHasContent(probe) {
|
|
754
106
|
return probe.liveFrameActive || probe.targets > 0 || probe.nodes >= SEMANTIC_READY_CONTENT_NODE_FLOOR;
|
|
755
107
|
}
|
|
@@ -1214,778 +566,1293 @@ async function inspectGetLayout(bridge, opts = {}) {
|
|
|
1214
566
|
w: Math.round(node.geometry.width),
|
|
1215
567
|
h: Math.round(node.geometry.height)
|
|
1216
568
|
}
|
|
1217
|
-
}));
|
|
569
|
+
}));
|
|
570
|
+
}
|
|
571
|
+
const raw = await bridge.send({
|
|
572
|
+
type: "evaluate",
|
|
573
|
+
code: layoutGetNativeEval(opts)
|
|
574
|
+
});
|
|
575
|
+
return Array.isArray(raw) ? raw : [];
|
|
576
|
+
}
|
|
577
|
+
function formatLayoutElements(elements, opts) {
|
|
578
|
+
if (elements.length === 0) return " no visible elements found";
|
|
579
|
+
const lines = elements.map((el) => {
|
|
580
|
+
const ident = el.testID ? `#${el.testID}` : el.selector ? el.selector : el.role ? `[${el.role}]` : `<${el.type}>`;
|
|
581
|
+
const parts = [ident, `@(${el.box.x},${el.box.y})`, `${el.box.w}x${el.box.h}`];
|
|
582
|
+
if (el.borderRadius !== void 0 && el.borderRadius > 0) {
|
|
583
|
+
parts.push(`radius:${el.borderRadius}`);
|
|
584
|
+
}
|
|
585
|
+
const p = el.padding;
|
|
586
|
+
if (p && (p.t || p.r || p.b || p.l)) {
|
|
587
|
+
const uniform = p.t === p.r && p.r === p.b && p.b === p.l;
|
|
588
|
+
parts.push(uniform ? `pad:${p.t}` : `pad:${p.t},${p.r},${p.b},${p.l}`);
|
|
589
|
+
}
|
|
590
|
+
if (el.fontSize !== void 0 && el.fontSize > 0) {
|
|
591
|
+
parts.push(`font:${el.fontSize}`);
|
|
592
|
+
}
|
|
593
|
+
if (opts?.styling) {
|
|
594
|
+
if (el.fontWeight) parts.push(`weight:${el.fontWeight}`);
|
|
595
|
+
if (el.color) parts.push(`color:${el.color}`);
|
|
596
|
+
if (el.bg) parts.push(`bg:${el.bg}`);
|
|
597
|
+
if (el.contrast) {
|
|
598
|
+
const score = el.contrast.ratio == null ? `?/${el.contrast.required}` : `${el.contrast.ratio}/${el.contrast.required}${el.contrast.ratio < el.contrast.required ? " FAIL" : ""}`;
|
|
599
|
+
parts.push(`contrast:${score}`);
|
|
600
|
+
if (el.contrast.background) parts.push(`on:${el.contrast.background}`);
|
|
601
|
+
}
|
|
602
|
+
if (el.opacity != null) parts.push(`opacity:${el.opacity}`);
|
|
603
|
+
if (el.textDecoration) parts.push(`deco:${el.textDecoration}`);
|
|
604
|
+
if (el.overflow) parts.push(`overflow:${el.overflow}`);
|
|
605
|
+
if (el.borderWidth) {
|
|
606
|
+
parts.push(
|
|
607
|
+
el.borderColor ? `border:${el.borderWidth}/${el.borderColor}` : `border:${el.borderWidth}`
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
if (el.text) {
|
|
612
|
+
const text = el.text.length > 40 ? `${el.text.slice(0, 39)}\u2026` : el.text;
|
|
613
|
+
parts.push(`"${text}"`);
|
|
614
|
+
}
|
|
615
|
+
return ` ${parts.join(" ")}`;
|
|
616
|
+
});
|
|
617
|
+
return lines.join("\n");
|
|
618
|
+
}
|
|
619
|
+
async function inspectAccessibilityTree(bridge) {
|
|
620
|
+
const result = await bridge.send({
|
|
621
|
+
type: "query",
|
|
622
|
+
query: { kind: "find", selector: { visible: true } }
|
|
623
|
+
});
|
|
624
|
+
if (!isSemanticQueryResult(result)) return [];
|
|
625
|
+
return result.nodes.flatMap((node) => {
|
|
626
|
+
if (!node.role && !node.label && !node.text && !node.pressable) return [];
|
|
627
|
+
return [
|
|
628
|
+
{
|
|
629
|
+
role: node.role ?? (node.pressable ? "button" : node.type === "text" ? "statictext" : "none"),
|
|
630
|
+
label: node.label ?? node.text ?? null,
|
|
631
|
+
hint: null,
|
|
632
|
+
state: null,
|
|
633
|
+
testID: node.testID ?? null,
|
|
634
|
+
position: {
|
|
635
|
+
x: Math.round(node.geometry.x),
|
|
636
|
+
y: Math.round(node.geometry.y)
|
|
637
|
+
},
|
|
638
|
+
size: {
|
|
639
|
+
w: Math.round(node.geometry.width),
|
|
640
|
+
h: Math.round(node.geometry.height)
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
];
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
function resolveFindMode(q) {
|
|
647
|
+
if (q.testId) {
|
|
648
|
+
return { mode: "testid", selector: { testID: q.testId } };
|
|
649
|
+
}
|
|
650
|
+
if (q.role) {
|
|
651
|
+
return { mode: "role", selector: { role: q.role } };
|
|
652
|
+
}
|
|
653
|
+
if (q.type) {
|
|
654
|
+
return { mode: "type", selector: { type: q.type } };
|
|
655
|
+
}
|
|
656
|
+
if (q.pressable) {
|
|
657
|
+
return { mode: "pressable", selector: { pressable: true } };
|
|
658
|
+
}
|
|
659
|
+
if (q.interactive) {
|
|
660
|
+
return { mode: "interactive-targets", selector: { pressable: true } };
|
|
661
|
+
}
|
|
662
|
+
if (q.visible) {
|
|
663
|
+
return { mode: "visible", selector: { visible: true } };
|
|
664
|
+
}
|
|
665
|
+
if (q.text) {
|
|
666
|
+
return { mode: "text", selector: { text: q.text } };
|
|
667
|
+
}
|
|
668
|
+
return null;
|
|
669
|
+
}
|
|
670
|
+
async function inspectFind(bridge, q) {
|
|
671
|
+
const resolved = resolveFindMode(q);
|
|
672
|
+
if (!resolved) return null;
|
|
673
|
+
const result = await bridge.send({
|
|
674
|
+
type: "query",
|
|
675
|
+
query: { kind: "find", selector: resolved.selector }
|
|
676
|
+
});
|
|
677
|
+
if (!isSemanticQueryResult(result)) return { mode: resolved.mode, result: [] };
|
|
678
|
+
const nodes = result.nodes.map(
|
|
679
|
+
(node) => ({
|
|
680
|
+
type: node.type,
|
|
681
|
+
nodeId: node.nodeId,
|
|
682
|
+
...node.testID ? { testID: node.testID } : {},
|
|
683
|
+
...node.text ? { text: node.text } : {},
|
|
684
|
+
...node.pressable ? { pressable: true } : {},
|
|
685
|
+
...node.role ? { accessibilityRole: node.role } : {},
|
|
686
|
+
...node.label ? { accessibilityLabel: node.label } : {},
|
|
687
|
+
absolutePosition: { x: node.geometry.x, y: node.geometry.y },
|
|
688
|
+
layout: { width: node.geometry.width, height: node.geometry.height }
|
|
689
|
+
})
|
|
690
|
+
);
|
|
691
|
+
const resultValue = resolved.mode === "testid" || resolved.mode === "text" ? nodes[0] ?? null : nodes;
|
|
692
|
+
return { mode: resolved.mode, result: resultValue };
|
|
693
|
+
}
|
|
694
|
+
function rankInteractive(nodes) {
|
|
695
|
+
return [...nodes].sort((a, b) => scoreInteractive(b) - scoreInteractive(a));
|
|
696
|
+
}
|
|
697
|
+
function scoreInteractive(n) {
|
|
698
|
+
let score = 0;
|
|
699
|
+
if (n.testID) score += 100;
|
|
700
|
+
if (typeof n.text === "string" && n.text.trim().length > 0) score += 60;
|
|
701
|
+
if (typeof n.accessibilityLabel === "string" && n.accessibilityLabel.trim().length > 0) {
|
|
702
|
+
score += 30;
|
|
703
|
+
}
|
|
704
|
+
if (n.accessibilityRole) score += 15;
|
|
705
|
+
const w = n.layout?.width ?? 0;
|
|
706
|
+
const h = n.layout?.height ?? 0;
|
|
707
|
+
const area = w * h;
|
|
708
|
+
if (area >= 400 && area <= 6e4) score += 25;
|
|
709
|
+
else if (area > 6e4) score -= 20;
|
|
710
|
+
const y = n.absolutePosition?.y ?? 0;
|
|
711
|
+
if (y < 0) score -= 30;
|
|
712
|
+
return score;
|
|
713
|
+
}
|
|
714
|
+
function tapCommandForNode(n) {
|
|
715
|
+
if (n.testID) return `rnx do tap-id ${shellEscape(n.testID)}`;
|
|
716
|
+
const text = typeof n.text === "string" ? n.text.trim() : "";
|
|
717
|
+
if (text.length > 0 && text.length <= 80) return `rnx do tap-text ${shellEscape(text)}`;
|
|
718
|
+
const x = Math.round(((n.absolutePosition?.x ?? 0) + (n.layout?.width ?? 0) / 2) * 10) / 10;
|
|
719
|
+
const y = Math.round(((n.absolutePosition?.y ?? 0) + (n.layout?.height ?? 0) / 2) * 10) / 10;
|
|
720
|
+
return `rnx do tap ${x} ${y}`;
|
|
721
|
+
}
|
|
722
|
+
function shellEscape(value) {
|
|
723
|
+
if (/^[A-Za-z0-9_./@:-]+$/.test(value)) return value;
|
|
724
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
725
|
+
}
|
|
726
|
+
function waitReadyReason(status) {
|
|
727
|
+
if (status.bridgeError) {
|
|
728
|
+
if (status.bridgeError.includes(HEADLESS_TENANT_FETCH_HUNG_MARKER) || status.bridgeError.includes(PUBLIC_NO_NETWORK_MARKER)) {
|
|
729
|
+
return status.bridgeError;
|
|
730
|
+
}
|
|
731
|
+
return `sim unreachable: ${status.bridgeError} \u2014 the target tab is closed or no sim is connected (check \`rnx list\`, reopen with \`rnx open\`)`;
|
|
732
|
+
}
|
|
733
|
+
const waitingOnGuestFetch = status.externalReady === false && /https?:\/\//.test(status.externalStatus);
|
|
734
|
+
const base = status.externalError ? `guest app errored: ${status.externalError}` : waitingOnGuestFetch ? `waiting on a guest fetch: ${status.externalStatus}` : status.loadingText ? `still showing "${status.loadingText}"` : status.externalReady === false ? "guest app is still loading" : status.flag !== true && status.targets > 0 ? "native content is rendered but the ready signal has not settled" : status.flag !== true ? "guest app has not emitted sootsim:externalAppReady" : status.targets <= 0 ? "ready flag emitted but no visible app content is inspectable yet" : "node tree is still changing";
|
|
735
|
+
return !status.externalError && status.suppressedEntryError ? `${base} (suppressed entry error: ${status.suppressedEntryError})` : base;
|
|
736
|
+
}
|
|
737
|
+
async function inspectWaitReady(bridge, timeoutMs = 2e4, options = {}) {
|
|
738
|
+
const start = Date.now();
|
|
739
|
+
let result;
|
|
740
|
+
try {
|
|
741
|
+
result = await bridge.send(
|
|
742
|
+
{
|
|
743
|
+
type: "waitFor",
|
|
744
|
+
waitForOptions: { condition: { type: "ready" }, timeoutMs }
|
|
745
|
+
},
|
|
746
|
+
{ timeoutMs: timeoutMs + 1e3 }
|
|
747
|
+
);
|
|
748
|
+
} catch (error) {
|
|
749
|
+
const bridgeError = error instanceof Error ? error.message : String(error);
|
|
750
|
+
if (bridgeError.startsWith("multiple sims are connected:") || bridgeError.startsWith("saved sim ")) {
|
|
751
|
+
throw error;
|
|
752
|
+
}
|
|
753
|
+
result = {
|
|
754
|
+
matched: false,
|
|
755
|
+
elapsedMs: Date.now() - start,
|
|
756
|
+
polls: 0,
|
|
757
|
+
error: bridgeError
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
const probe = result.probe;
|
|
761
|
+
const externalError = probe?.externalError ?? "";
|
|
762
|
+
const status = {
|
|
763
|
+
ready: result.matched && !externalError,
|
|
764
|
+
elapsedMs: result.elapsedMs,
|
|
765
|
+
nodes: probe?.nodes ?? 0,
|
|
766
|
+
targets: probe?.targets ?? 0,
|
|
767
|
+
liveFrameActive: probe?.liveFrameActive ?? false,
|
|
768
|
+
liveFrameChannels: probe?.liveFrameChannels ?? 0,
|
|
769
|
+
liveFramePublishes: probe?.liveFramePublishes ?? 0,
|
|
770
|
+
flag: probe?.flag,
|
|
771
|
+
loadingText: probe?.loadingText ?? "",
|
|
772
|
+
externalReady: probe?.externalReady ?? null,
|
|
773
|
+
externalStatus: probe?.externalStatus ?? "",
|
|
774
|
+
externalError,
|
|
775
|
+
suppressedEntryError: probe?.suppressedEntryError ?? "",
|
|
776
|
+
errors: probe?.errors ?? 0,
|
|
777
|
+
bridgeError: result.error ?? ""
|
|
778
|
+
};
|
|
779
|
+
if (!status.ready && options.onProgress) options.onProgress(status);
|
|
780
|
+
return status;
|
|
781
|
+
}
|
|
782
|
+
async function inspectWaitSelector(bridge, testId, timeoutMs = 5e3, opts = {}) {
|
|
783
|
+
const gone = opts.gone === true;
|
|
784
|
+
const result = await bridge.send(
|
|
785
|
+
{
|
|
786
|
+
type: "waitFor",
|
|
787
|
+
waitForOptions: {
|
|
788
|
+
condition: {
|
|
789
|
+
type: gone ? "absent" : "present",
|
|
790
|
+
selector: { testID: testId }
|
|
791
|
+
},
|
|
792
|
+
timeoutMs
|
|
793
|
+
}
|
|
794
|
+
},
|
|
795
|
+
{ timeoutMs: timeoutMs + 1e3 }
|
|
796
|
+
);
|
|
797
|
+
if (result?.error) throw new Error(result.error);
|
|
798
|
+
if (result?.node && !isSimSemanticTree([result.node])) {
|
|
799
|
+
throw new Error("waitFor returned an invalid semantic node");
|
|
1218
800
|
}
|
|
1219
|
-
|
|
801
|
+
return {
|
|
802
|
+
found: result?.matched === true,
|
|
803
|
+
...result?.node ? { node: result.node } : {},
|
|
804
|
+
elapsed: result?.elapsedMs ?? timeoutMs
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
function consoleEntriesEval(level, limit) {
|
|
808
|
+
const getter = level === "error" ? "getErrors" : "getWarnings";
|
|
809
|
+
return `(() => {
|
|
810
|
+
const out = []
|
|
811
|
+
const seen = new Set()
|
|
812
|
+
const norm = (s) => (typeof s === 'string' ? s : (() => { try { return JSON.stringify(s) } catch { return String(s) } })())
|
|
813
|
+
const key = (ts, args) => Math.round((ts || 0) / 250) + '|' + (Array.isArray(args) ? args.map(norm).join(' ') : norm(args))
|
|
814
|
+
const push = (e) => {
|
|
815
|
+
if (!e) return
|
|
816
|
+
const ts = typeof e.timestamp === 'number' ? e.timestamp : (typeof e.ts === 'number' ? e.ts : 0)
|
|
817
|
+
const k = key(ts, e.args)
|
|
818
|
+
if (seen.has(k)) return
|
|
819
|
+
seen.add(k)
|
|
820
|
+
out.push({ timestamp: ts, args: Array.isArray(e.args) ? e.args : [e.args], stack: e.stack || undefined, source: e.source || undefined })
|
|
821
|
+
}
|
|
822
|
+
// page-realm ws-bridge buffer first (richest stacks for page-origin errors)
|
|
823
|
+
try { for (const e of (window.__sootsimConsole?.${getter}(${limit}) || [])) push(e) } catch {}
|
|
824
|
+
// engine observability store \u2014 the always-on render-worker / forwarded path
|
|
825
|
+
try {
|
|
826
|
+
const obs = window.__sootsimObservability
|
|
827
|
+
const snap = obs && obs.logs && typeof obs.logs.getSnapshot === 'function' ? obs.logs.getSnapshot() : []
|
|
828
|
+
for (const e of snap) if (e && e.level === ${JSON.stringify(level)}) push(e)
|
|
829
|
+
} catch {}
|
|
830
|
+
return out.sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0)).slice(-${limit})
|
|
831
|
+
})()`;
|
|
832
|
+
}
|
|
833
|
+
async function inspectErrors(bridge, limit = 20) {
|
|
834
|
+
const result = await bridge.send({
|
|
1220
835
|
type: "evaluate",
|
|
1221
|
-
code:
|
|
836
|
+
code: consoleEntriesEval("error", limit)
|
|
1222
837
|
});
|
|
1223
|
-
return Array.isArray(
|
|
838
|
+
return Array.isArray(result) ? result : [];
|
|
1224
839
|
}
|
|
1225
|
-
function
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
const
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
if (
|
|
1244
|
-
|
|
1245
|
-
if (el.contrast) {
|
|
1246
|
-
const score = el.contrast.ratio == null ? `?/${el.contrast.required}` : `${el.contrast.ratio}/${el.contrast.required}${el.contrast.ratio < el.contrast.required ? " FAIL" : ""}`;
|
|
1247
|
-
parts.push(`contrast:${score}`);
|
|
1248
|
-
if (el.contrast.background) parts.push(`on:${el.contrast.background}`);
|
|
1249
|
-
}
|
|
1250
|
-
if (el.opacity != null) parts.push(`opacity:${el.opacity}`);
|
|
1251
|
-
if (el.textDecoration) parts.push(`deco:${el.textDecoration}`);
|
|
1252
|
-
if (el.overflow) parts.push(`overflow:${el.overflow}`);
|
|
1253
|
-
if (el.borderWidth) {
|
|
1254
|
-
parts.push(
|
|
1255
|
-
el.borderColor ? `border:${el.borderWidth}/${el.borderColor}` : `border:${el.borderWidth}`
|
|
1256
|
-
);
|
|
1257
|
-
}
|
|
1258
|
-
}
|
|
1259
|
-
if (el.text) {
|
|
1260
|
-
const text = el.text.length > 40 ? `${el.text.slice(0, 39)}\u2026` : el.text;
|
|
1261
|
-
parts.push(`"${text}"`);
|
|
840
|
+
async function inspectWarnings(bridge, limit = 20) {
|
|
841
|
+
const result = await bridge.send({
|
|
842
|
+
type: "evaluate",
|
|
843
|
+
code: consoleEntriesEval("warn", limit)
|
|
844
|
+
});
|
|
845
|
+
return Array.isArray(result) ? result : [];
|
|
846
|
+
}
|
|
847
|
+
var MERGED_CONSOLE_COUNT_EVAL = `(() => {
|
|
848
|
+
const norm = (s) => (typeof s === 'string' ? s : (() => { try { return JSON.stringify(s) } catch { return String(s) } })())
|
|
849
|
+
const key = (lvl, ts, args) => lvl + '|' + Math.round((ts || 0) / 250) + '|' + (Array.isArray(args) ? args.map(norm).join(' ') : norm(args))
|
|
850
|
+
const seen = new Set()
|
|
851
|
+
let errors = 0
|
|
852
|
+
let warnings = 0
|
|
853
|
+
const add = (lvl, ts, args) => {
|
|
854
|
+
if (lvl !== 'error' && lvl !== 'warn') return
|
|
855
|
+
const k = key(lvl, ts, args)
|
|
856
|
+
if (seen.has(k)) return
|
|
857
|
+
seen.add(k)
|
|
858
|
+
if (lvl === 'error') errors++
|
|
859
|
+
else warnings++
|
|
1262
860
|
}
|
|
1263
|
-
|
|
861
|
+
try {
|
|
862
|
+
const c = window.__sootsimConsole
|
|
863
|
+
for (const e of (c?.getErrors?.(200) || [])) add('error', e?.timestamp ?? e?.ts, e?.args)
|
|
864
|
+
for (const e of (c?.getWarnings?.(200) || [])) add('warn', e?.timestamp ?? e?.ts, e?.args)
|
|
865
|
+
} catch {}
|
|
866
|
+
try {
|
|
867
|
+
const obs = window.__sootsimObservability
|
|
868
|
+
const snap = obs && obs.logs && typeof obs.logs.getSnapshot === 'function' ? obs.logs.getSnapshot() : []
|
|
869
|
+
for (const e of snap) if (e) add(e.level, e.ts, e.args)
|
|
870
|
+
} catch {}
|
|
871
|
+
return { errors, warnings, total: errors + warnings }
|
|
872
|
+
})()`;
|
|
873
|
+
async function clearConsole(bridge) {
|
|
874
|
+
await bridge.send({
|
|
875
|
+
type: "evaluate",
|
|
876
|
+
code: 'window.__sootsimConsole?.clear(); window.__sootsimObservability?.logs?.clear?.(); "cleared"'
|
|
1264
877
|
});
|
|
1265
|
-
return lines.join("\n");
|
|
1266
878
|
}
|
|
1267
|
-
async function
|
|
879
|
+
async function inspectRequests(bridge, opts = {}) {
|
|
880
|
+
const limit = opts.limit ?? 20;
|
|
881
|
+
const method = opts.failed === false ? "getRequests" : "getFailedRequests";
|
|
1268
882
|
const result = await bridge.send({
|
|
1269
|
-
type: "
|
|
1270
|
-
|
|
883
|
+
type: "call",
|
|
884
|
+
path: `__sootsimTest.${method}`,
|
|
885
|
+
args: [limit]
|
|
1271
886
|
});
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
position: {
|
|
1283
|
-
x: Math.round(node.geometry.x),
|
|
1284
|
-
y: Math.round(node.geometry.y)
|
|
1285
|
-
},
|
|
1286
|
-
size: {
|
|
1287
|
-
w: Math.round(node.geometry.width),
|
|
1288
|
-
h: Math.round(node.geometry.height)
|
|
1289
|
-
}
|
|
1290
|
-
}
|
|
1291
|
-
];
|
|
887
|
+
return Array.isArray(result) ? result : [];
|
|
888
|
+
}
|
|
889
|
+
async function clearRequests(bridge) {
|
|
890
|
+
await bridge.send({ type: "call", path: "__sootsimTest.clearRequests", args: [] });
|
|
891
|
+
}
|
|
892
|
+
async function inspectScrollStateAt(bridge, x, y) {
|
|
893
|
+
const result = await bridge.send({
|
|
894
|
+
type: "call",
|
|
895
|
+
path: "__sootsimTest.getScrollStateAt",
|
|
896
|
+
args: [x, y]
|
|
1292
897
|
});
|
|
898
|
+
if (!result || typeof result !== "object") return null;
|
|
899
|
+
const state = result;
|
|
900
|
+
return {
|
|
901
|
+
...state,
|
|
902
|
+
scrollOffsetX: typeof state.scrollOffsetX === "number" ? state.scrollOffsetX : state.offsetX,
|
|
903
|
+
scrollOffsetY: typeof state.scrollOffsetY === "number" ? state.scrollOffsetY : state.offsetY
|
|
904
|
+
};
|
|
1293
905
|
}
|
|
1294
|
-
function
|
|
1295
|
-
if (
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
906
|
+
async function inspectLogs(bridge) {
|
|
907
|
+
if (bridge.plane === "cloud") {
|
|
908
|
+
const state = await bridge.send({
|
|
909
|
+
type: "state",
|
|
910
|
+
stateOptions: {
|
|
911
|
+
screenshot: false,
|
|
912
|
+
tree: false,
|
|
913
|
+
route: false,
|
|
914
|
+
errors: false,
|
|
915
|
+
logs: true,
|
|
916
|
+
logLimit: 100
|
|
917
|
+
}
|
|
918
|
+
});
|
|
919
|
+
const recentLogs = state && typeof state === "object" && Array.isArray(Reflect.get(state, "recentLogs")) ? Reflect.get(state, "recentLogs") : [];
|
|
920
|
+
return recentLogs.flatMap((entry, index) => {
|
|
921
|
+
if (!entry || typeof entry !== "object") return [];
|
|
922
|
+
const text = Reflect.get(entry, "text");
|
|
923
|
+
const rawLevel = Reflect.get(entry, "level");
|
|
924
|
+
if (typeof text !== "string") return [];
|
|
925
|
+
const level = rawLevel === "log" || rawLevel === "info" || rawLevel === "warn" || rawLevel === "error" || rawLevel === "debug" ? rawLevel : "error";
|
|
926
|
+
const at = Reflect.get(entry, "at");
|
|
927
|
+
const ts = typeof at === "number" && Number.isFinite(at) ? at : 0;
|
|
928
|
+
return [
|
|
929
|
+
{
|
|
930
|
+
id: `cloud-${ts}-${index}`,
|
|
931
|
+
source: "rnx-cloud",
|
|
932
|
+
level,
|
|
933
|
+
ts,
|
|
934
|
+
args: [text],
|
|
935
|
+
stack: null
|
|
936
|
+
}
|
|
937
|
+
];
|
|
938
|
+
});
|
|
1303
939
|
}
|
|
1304
|
-
|
|
1305
|
-
|
|
940
|
+
const res = await bridge.send({
|
|
941
|
+
type: "evaluate",
|
|
942
|
+
code: `(() => {
|
|
943
|
+
const obs = window.__sootsimObservability;
|
|
944
|
+
if (!obs) return { ok: false };
|
|
945
|
+
return { ok: true, entries: obs.logs.getSnapshot() };
|
|
946
|
+
})()`
|
|
947
|
+
});
|
|
948
|
+
if (!res || !res.ok) return [];
|
|
949
|
+
return res.entries ?? [];
|
|
950
|
+
}
|
|
951
|
+
async function clearLogs(bridge) {
|
|
952
|
+
await bridge.send({
|
|
953
|
+
type: "evaluate",
|
|
954
|
+
code: 'window.__sootsimObservability?.logs.clear(); "cleared"'
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
function isForwardedWorkerLogTwin(a, b) {
|
|
958
|
+
if (a.source === b.source) return false;
|
|
959
|
+
if (a.level !== b.level) return false;
|
|
960
|
+
if (Math.abs(a.ts - b.ts) > 1e3) return false;
|
|
961
|
+
if (a.args.length !== b.args.length) return false;
|
|
962
|
+
const sources = /* @__PURE__ */ new Set([a.source, b.source]);
|
|
963
|
+
if (!sources.has("sootsim-worker")) return false;
|
|
964
|
+
if (!sources.has("render-worker") && !sources.has("forwarded-render-worker")) {
|
|
965
|
+
return false;
|
|
1306
966
|
}
|
|
1307
|
-
|
|
1308
|
-
|
|
967
|
+
return a.args.every((arg, index) => arg === b.args[index]);
|
|
968
|
+
}
|
|
969
|
+
function filterLogEntries(entries, opts = {}) {
|
|
970
|
+
let out = [];
|
|
971
|
+
for (const entry of entries) {
|
|
972
|
+
if (out.some((candidate) => isForwardedWorkerLogTwin(candidate, entry))) {
|
|
973
|
+
continue;
|
|
974
|
+
}
|
|
975
|
+
out.push(entry);
|
|
1309
976
|
}
|
|
1310
|
-
if (
|
|
1311
|
-
|
|
977
|
+
if (!opts.showInternal) {
|
|
978
|
+
out = out.filter((e) => {
|
|
979
|
+
const first = e.args[0];
|
|
980
|
+
return !(typeof first === "string" && /^\[(?:rnx|sootsim)(?:\s[^\]]*)?\]/.test(first));
|
|
981
|
+
});
|
|
1312
982
|
}
|
|
1313
|
-
if (
|
|
1314
|
-
|
|
983
|
+
if (opts.level) out = out.filter((e) => opts.level.has(e.level));
|
|
984
|
+
if (opts.filter) {
|
|
985
|
+
const lf = opts.filter.toLowerCase();
|
|
986
|
+
out = out.filter((e) => e.args.join(" ").toLowerCase().includes(lf));
|
|
1315
987
|
}
|
|
1316
|
-
return
|
|
988
|
+
return out;
|
|
1317
989
|
}
|
|
1318
|
-
async function
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
query: { kind: "find", selector: resolved.selector }
|
|
990
|
+
async function inspectTimelineSummary(bridge, query) {
|
|
991
|
+
return await bridge.send({
|
|
992
|
+
type: "call",
|
|
993
|
+
path: "SootSim.bridges.timeline.summary",
|
|
994
|
+
args: [query]
|
|
1324
995
|
});
|
|
1325
|
-
if (!isSemanticQueryResult(result)) return { mode: resolved.mode, result: [] };
|
|
1326
|
-
const nodes = result.nodes.map(
|
|
1327
|
-
(node) => ({
|
|
1328
|
-
type: node.type,
|
|
1329
|
-
nodeId: node.nodeId,
|
|
1330
|
-
...node.testID ? { testID: node.testID } : {},
|
|
1331
|
-
...node.text ? { text: node.text } : {},
|
|
1332
|
-
...node.pressable ? { pressable: true } : {},
|
|
1333
|
-
...node.role ? { accessibilityRole: node.role } : {},
|
|
1334
|
-
...node.label ? { accessibilityLabel: node.label } : {},
|
|
1335
|
-
absolutePosition: { x: node.geometry.x, y: node.geometry.y },
|
|
1336
|
-
layout: { width: node.geometry.width, height: node.geometry.height }
|
|
1337
|
-
})
|
|
1338
|
-
);
|
|
1339
|
-
const resultValue = resolved.mode === "testid" || resolved.mode === "text" ? nodes[0] ?? null : nodes;
|
|
1340
|
-
return { mode: resolved.mode, result: resultValue };
|
|
1341
996
|
}
|
|
1342
|
-
function
|
|
1343
|
-
return
|
|
997
|
+
async function inspectTimelineRecent(bridge, query) {
|
|
998
|
+
return await bridge.send({
|
|
999
|
+
type: "call",
|
|
1000
|
+
path: "SootSim.bridges.timeline.recent",
|
|
1001
|
+
args: [query]
|
|
1002
|
+
});
|
|
1344
1003
|
}
|
|
1345
|
-
function
|
|
1346
|
-
let
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1004
|
+
async function detectOpenNativeUI(bridge) {
|
|
1005
|
+
let events;
|
|
1006
|
+
try {
|
|
1007
|
+
const res = await inspectTimelineRecent(bridge, {
|
|
1008
|
+
kinds: ["alert", "actionsheet"],
|
|
1009
|
+
limit: 80
|
|
1010
|
+
});
|
|
1011
|
+
events = res.events;
|
|
1012
|
+
} catch {
|
|
1013
|
+
return [];
|
|
1351
1014
|
}
|
|
1352
|
-
|
|
1353
|
-
const
|
|
1354
|
-
const
|
|
1355
|
-
const
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1015
|
+
const latest = /* @__PURE__ */ new Map();
|
|
1016
|
+
for (const ev of events) latest.set(ev.kind, ev);
|
|
1017
|
+
const out = [];
|
|
1018
|
+
const check = (kind, label) => {
|
|
1019
|
+
const ev = latest.get(kind);
|
|
1020
|
+
if (!ev || !ev.data || typeof ev.data !== "object") return;
|
|
1021
|
+
const d = ev.data;
|
|
1022
|
+
if (d.phase !== "show") return;
|
|
1023
|
+
out.push({ label, title: typeof d.title === "string" ? d.title : null });
|
|
1024
|
+
};
|
|
1025
|
+
check("alert", "Alert");
|
|
1026
|
+
check("actionsheet", "ActionSheet");
|
|
1027
|
+
return out;
|
|
1361
1028
|
}
|
|
1362
|
-
function
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
return `rnx do tap ${x} ${y}`;
|
|
1029
|
+
async function inspectTimelineAdvanceCursor(bridge, cursorKey, watermark) {
|
|
1030
|
+
await bridge.send({
|
|
1031
|
+
type: "call",
|
|
1032
|
+
path: "SootSim.bridges.timeline.cursorAdvance",
|
|
1033
|
+
args: [cursorKey, watermark]
|
|
1034
|
+
});
|
|
1369
1035
|
}
|
|
1370
|
-
function
|
|
1371
|
-
|
|
1372
|
-
|
|
1036
|
+
async function inspectKeyboard(bridge) {
|
|
1037
|
+
const result = await bridge.send({
|
|
1038
|
+
type: "evaluate",
|
|
1039
|
+
code: `(() => {
|
|
1040
|
+
const kb = window.__sootsimKeyboard
|
|
1041
|
+
if (!kb || typeof kb.getLayout !== 'function') {
|
|
1042
|
+
return { error: 'keyboard bridge getLayout() not available' }
|
|
1043
|
+
}
|
|
1044
|
+
return kb.getLayout()
|
|
1045
|
+
})()`
|
|
1046
|
+
});
|
|
1047
|
+
return result ?? { error: "keyboard bridge returned no result" };
|
|
1373
1048
|
}
|
|
1374
|
-
function
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
}
|
|
1378
|
-
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";
|
|
1379
|
-
return !status.externalError && status.suppressedEntryError ? `${base} (suppressed entry error: ${status.suppressedEntryError})` : base;
|
|
1049
|
+
function isShellCommandUnavailable(error) {
|
|
1050
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1051
|
+
return message.includes("call target not found: SootSim.bridges.mainShell") || message.includes("test bridge unavailable before app-in-worker boot");
|
|
1380
1052
|
}
|
|
1381
|
-
async function
|
|
1382
|
-
const
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
}
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
const bridgeError = error instanceof Error ? error.message : String(error);
|
|
1394
|
-
if (bridgeError.startsWith("multiple sims are connected:") || bridgeError.startsWith("saved sim ")) {
|
|
1395
|
-
throw error;
|
|
1053
|
+
async function getShellState(bridge, readyTimeoutMs = 0) {
|
|
1054
|
+
const deadline = Date.now() + Math.max(0, readyTimeoutMs);
|
|
1055
|
+
while (true) {
|
|
1056
|
+
try {
|
|
1057
|
+
return await bridge.send({
|
|
1058
|
+
type: "call",
|
|
1059
|
+
path: "SootSim.bridges.mainShell.getState",
|
|
1060
|
+
args: []
|
|
1061
|
+
});
|
|
1062
|
+
} catch (error) {
|
|
1063
|
+
if (!isShellCommandUnavailable(error) || Date.now() >= deadline) throw error;
|
|
1064
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
1396
1065
|
}
|
|
1397
|
-
result = {
|
|
1398
|
-
matched: false,
|
|
1399
|
-
elapsedMs: Date.now() - start,
|
|
1400
|
-
polls: 0,
|
|
1401
|
-
error: bridgeError
|
|
1402
|
-
};
|
|
1403
1066
|
}
|
|
1404
|
-
const probe = result.probe;
|
|
1405
|
-
const externalError = probe?.externalError ?? "";
|
|
1406
|
-
const status = {
|
|
1407
|
-
ready: result.matched && !externalError,
|
|
1408
|
-
elapsedMs: result.elapsedMs,
|
|
1409
|
-
nodes: probe?.nodes ?? 0,
|
|
1410
|
-
targets: probe?.targets ?? 0,
|
|
1411
|
-
liveFrameActive: probe?.liveFrameActive ?? false,
|
|
1412
|
-
liveFrameChannels: probe?.liveFrameChannels ?? 0,
|
|
1413
|
-
liveFramePublishes: probe?.liveFramePublishes ?? 0,
|
|
1414
|
-
flag: probe?.flag,
|
|
1415
|
-
loadingText: probe?.loadingText ?? "",
|
|
1416
|
-
externalReady: probe?.externalReady ?? null,
|
|
1417
|
-
externalStatus: probe?.externalStatus ?? "",
|
|
1418
|
-
externalError,
|
|
1419
|
-
suppressedEntryError: probe?.suppressedEntryError ?? "",
|
|
1420
|
-
errors: probe?.errors ?? 0,
|
|
1421
|
-
bridgeError: result.error ?? ""
|
|
1422
|
-
};
|
|
1423
|
-
if (!status.ready && options.onProgress) options.onProgress(status);
|
|
1424
|
-
return status;
|
|
1425
1067
|
}
|
|
1426
|
-
async function
|
|
1427
|
-
const
|
|
1428
|
-
|
|
1429
|
-
{
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1068
|
+
async function inspectScreens(bridge) {
|
|
1069
|
+
const payload = await bridge.send({
|
|
1070
|
+
type: "evaluate",
|
|
1071
|
+
code: `(async () => {
|
|
1072
|
+
const test = window.__sootsimTest
|
|
1073
|
+
const kb = window.__sootsimKeyboard
|
|
1074
|
+
const navSnap =
|
|
1075
|
+
test && typeof test.getNavigationSnapshot === 'function'
|
|
1076
|
+
? await test.getNavigationSnapshot()
|
|
1077
|
+
: null
|
|
1078
|
+
const route =
|
|
1079
|
+
test && typeof test.getRouteInfo === 'function'
|
|
1080
|
+
? await test.getRouteInfo()
|
|
1081
|
+
: null
|
|
1082
|
+
const keyboard =
|
|
1083
|
+
kb && typeof kb.getLayout === 'function'
|
|
1084
|
+
? (() => {
|
|
1085
|
+
const layout = kb.getLayout()
|
|
1086
|
+
return layout ? {
|
|
1087
|
+
visible: layout.visible,
|
|
1088
|
+
mode: layout.mode,
|
|
1089
|
+
spec: layout.spec
|
|
1090
|
+
? {
|
|
1091
|
+
keyboardType: layout.spec.keyboardType,
|
|
1092
|
+
returnKeyType: layout.spec.returnKeyType,
|
|
1093
|
+
}
|
|
1094
|
+
: null,
|
|
1095
|
+
} : null
|
|
1096
|
+
})()
|
|
1097
|
+
: null
|
|
1098
|
+
return { nav: navSnap, route, keyboard }
|
|
1099
|
+
})()`
|
|
1100
|
+
});
|
|
1101
|
+
const shell = await getShellState(bridge, 500).catch(() => null);
|
|
1445
1102
|
return {
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1103
|
+
shell,
|
|
1104
|
+
nav: payload?.nav ?? null,
|
|
1105
|
+
route: payload?.route ?? null,
|
|
1106
|
+
keyboard: payload?.keyboard ?? null
|
|
1449
1107
|
};
|
|
1450
1108
|
}
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
seen.add(k)
|
|
1464
|
-
out.push({ timestamp: ts, args: Array.isArray(e.args) ? e.args : [e.args], stack: e.stack || undefined, source: e.source || undefined })
|
|
1465
|
-
}
|
|
1466
|
-
// page-realm ws-bridge buffer first (richest stacks for page-origin errors)
|
|
1467
|
-
try { for (const e of (window.__sootsimConsole?.${getter}(${limit}) || [])) push(e) } catch {}
|
|
1468
|
-
// engine observability store \u2014 the always-on render-worker / forwarded path
|
|
1469
|
-
try {
|
|
1470
|
-
const obs = window.__sootsimObservability
|
|
1471
|
-
const snap = obs && obs.logs && typeof obs.logs.getSnapshot === 'function' ? obs.logs.getSnapshot() : []
|
|
1472
|
-
for (const e of snap) if (e && e.level === ${JSON.stringify(level)}) push(e)
|
|
1473
|
-
} catch {}
|
|
1474
|
-
return out.sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0)).slice(-${limit})
|
|
1475
|
-
})()`;
|
|
1109
|
+
var DEBUG_CHANNELS = [
|
|
1110
|
+
"portals",
|
|
1111
|
+
"sheets",
|
|
1112
|
+
"layout",
|
|
1113
|
+
"onlayout",
|
|
1114
|
+
"animated",
|
|
1115
|
+
"render",
|
|
1116
|
+
"touch",
|
|
1117
|
+
"yoga"
|
|
1118
|
+
];
|
|
1119
|
+
async function inspectDebugStatus(bridge) {
|
|
1120
|
+
return bridge.send({ type: "evaluate", code: "window.__sootsimDebug.status()" });
|
|
1476
1121
|
}
|
|
1477
|
-
async function
|
|
1478
|
-
|
|
1122
|
+
async function inspectDebugFlags(bridge) {
|
|
1123
|
+
return bridge.send({ type: "evaluate", code: "window.__sootsimDebug.flags()" });
|
|
1124
|
+
}
|
|
1125
|
+
async function inspectDebugFind(bridge, target) {
|
|
1126
|
+
return bridge.send({
|
|
1479
1127
|
type: "evaluate",
|
|
1480
|
-
code:
|
|
1128
|
+
code: `window.__sootsimTest.debugFind(${JSON.stringify(target)})`
|
|
1481
1129
|
});
|
|
1482
|
-
return Array.isArray(result) ? result : [];
|
|
1483
1130
|
}
|
|
1484
|
-
async function
|
|
1485
|
-
const
|
|
1131
|
+
async function inspectDebugRecent(bridge, channel, limit = 50) {
|
|
1132
|
+
const code = channel && channel !== "all" ? `window.__sootsimDebug.recent(${JSON.stringify(channel)}, ${limit})` : `window.__sootsimDebug.recent(undefined, ${limit})`;
|
|
1133
|
+
return bridge.send({ type: "evaluate", code });
|
|
1134
|
+
}
|
|
1135
|
+
async function setDebugChannels(bridge, action, channels) {
|
|
1136
|
+
const args = channels.length > 0 ? channels.map((c) => JSON.stringify(c)).join(", ") : action === "disable" ? "'all'" : "";
|
|
1137
|
+
return bridge.send({
|
|
1486
1138
|
type: "evaluate",
|
|
1487
|
-
code:
|
|
1139
|
+
code: `window.__sootsimDebug.${action}(${args})`
|
|
1488
1140
|
});
|
|
1489
|
-
return Array.isArray(result) ? result : [];
|
|
1490
1141
|
}
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
const
|
|
1494
|
-
const
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
for (const e of snap) if (e) add(e.level, e.ts, e.args)
|
|
1514
|
-
} catch {}
|
|
1515
|
-
return { errors, warnings, total: errors + warnings }
|
|
1516
|
-
})()`;
|
|
1517
|
-
async function clearConsole(bridge) {
|
|
1518
|
-
await bridge.send({
|
|
1142
|
+
async function inspectMemory(bridge) {
|
|
1143
|
+
if (bridge.plane === "cloud") {
|
|
1144
|
+
const value = await bridge.send({ type: "memory" });
|
|
1145
|
+
const rawWasmHeapBytes = typeof value === "object" && value !== null ? Reflect.get(value, "wasmHeapBytes") : null;
|
|
1146
|
+
const wasmHeapBytes = typeof rawWasmHeapBytes === "number" ? rawWasmHeapBytes : null;
|
|
1147
|
+
return {
|
|
1148
|
+
tenant: {
|
|
1149
|
+
nodesRegistered: null,
|
|
1150
|
+
nodesDetached: null,
|
|
1151
|
+
detachedTypes: null,
|
|
1152
|
+
objects: null,
|
|
1153
|
+
activeNativeAnimations: null,
|
|
1154
|
+
imageLoader: null,
|
|
1155
|
+
workerHeap: null,
|
|
1156
|
+
wasmHeapBytes
|
|
1157
|
+
},
|
|
1158
|
+
shell: null,
|
|
1159
|
+
compositor: null,
|
|
1160
|
+
hostHeap: null
|
|
1161
|
+
};
|
|
1162
|
+
}
|
|
1163
|
+
const result = await bridge.send({
|
|
1519
1164
|
type: "evaluate",
|
|
1520
|
-
code:
|
|
1165
|
+
code: `(async () => {
|
|
1166
|
+
const host = window.__sootsimRenderHost
|
|
1167
|
+
const stats = host?.queryStats ? await host.queryStats() : null
|
|
1168
|
+
const shellRaw = window.__sootsimShellMemory
|
|
1169
|
+
? await window.__sootsimShellMemory()
|
|
1170
|
+
: null
|
|
1171
|
+
const shell = shellRaw && !shellRaw.error ? shellRaw : null
|
|
1172
|
+
// the compositor paints home + app:one + app:two, so every app-surface
|
|
1173
|
+
// picture and raster image is counted here and nowhere else.
|
|
1174
|
+
let compositor = null
|
|
1175
|
+
try {
|
|
1176
|
+
const cs = await window.__sootsimCompositor?.getStats?.(false, true)
|
|
1177
|
+
compositor = cs?.memory ?? null
|
|
1178
|
+
} catch {}
|
|
1179
|
+
const normalize = (m) =>
|
|
1180
|
+
m
|
|
1181
|
+
? {
|
|
1182
|
+
nodesRegistered: m.nodesRegistered ?? null,
|
|
1183
|
+
nodesDetached: m.nodesDetached ?? null,
|
|
1184
|
+
detachedTypes: m.detachedTypes ?? null,
|
|
1185
|
+
objects: m.objects ?? null,
|
|
1186
|
+
activeNativeAnimations: m.activeNativeAnimations ?? null,
|
|
1187
|
+
imageLoader: m.imageLoader ?? null,
|
|
1188
|
+
workerHeap: m.workerHeap ?? null,
|
|
1189
|
+
wasmHeapBytes: m.wasmHeapBytes ?? null,
|
|
1190
|
+
}
|
|
1191
|
+
: null
|
|
1192
|
+
const hostMem = performance.memory
|
|
1193
|
+
? {
|
|
1194
|
+
usedJSHeapSize: performance.memory.usedJSHeapSize,
|
|
1195
|
+
totalJSHeapSize: performance.memory.totalJSHeapSize,
|
|
1196
|
+
jsHeapSizeLimit: performance.memory.jsHeapSizeLimit,
|
|
1197
|
+
}
|
|
1198
|
+
: null
|
|
1199
|
+
return {
|
|
1200
|
+
tenant: normalize(stats?.memory),
|
|
1201
|
+
shell: normalize(shell),
|
|
1202
|
+
compositor: normalize(compositor),
|
|
1203
|
+
hostHeap: hostMem,
|
|
1204
|
+
}
|
|
1205
|
+
})()`
|
|
1521
1206
|
});
|
|
1207
|
+
return result ?? { tenant: null, shell: null, compositor: null, hostHeap: null };
|
|
1208
|
+
}
|
|
1209
|
+
function formatTimelineSummary(summary) {
|
|
1210
|
+
if (summary.total === 0) return "nothing recorded";
|
|
1211
|
+
const parts = [];
|
|
1212
|
+
const ORDER = [
|
|
1213
|
+
"error",
|
|
1214
|
+
"warning",
|
|
1215
|
+
"console",
|
|
1216
|
+
"fetch",
|
|
1217
|
+
"toast",
|
|
1218
|
+
"alert",
|
|
1219
|
+
"actionsheet",
|
|
1220
|
+
"picker",
|
|
1221
|
+
"notification",
|
|
1222
|
+
"screen",
|
|
1223
|
+
"route",
|
|
1224
|
+
"keyboard",
|
|
1225
|
+
"app-launch",
|
|
1226
|
+
"shell",
|
|
1227
|
+
"scroll",
|
|
1228
|
+
"gesture",
|
|
1229
|
+
"text-input",
|
|
1230
|
+
"react-commit",
|
|
1231
|
+
"animation",
|
|
1232
|
+
"reanimated"
|
|
1233
|
+
];
|
|
1234
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1235
|
+
for (const k of ORDER) {
|
|
1236
|
+
const n = summary.byKind[k];
|
|
1237
|
+
if (n) {
|
|
1238
|
+
parts.push(`${n} ${k}${n === 1 ? "" : "s"}`);
|
|
1239
|
+
seen.add(k);
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
for (const [k, n] of Object.entries(summary.byKind)) {
|
|
1243
|
+
if (!seen.has(k) && n) parts.push(`${n} ${k}${n === 1 ? "" : "s"}`);
|
|
1244
|
+
}
|
|
1245
|
+
return parts.join(" \xB7 ");
|
|
1246
|
+
}
|
|
1247
|
+
var NOISY_TIMELINE_KINDS = /* @__PURE__ */ new Set([
|
|
1248
|
+
"react-commit",
|
|
1249
|
+
"layout",
|
|
1250
|
+
"scroll"
|
|
1251
|
+
]);
|
|
1252
|
+
function formatTimelineRelativeTime(t, anchor) {
|
|
1253
|
+
if (anchor === null) return new Date(t).toLocaleTimeString();
|
|
1254
|
+
const dt = (t - anchor) / 1e3;
|
|
1255
|
+
const sign = dt >= 0 ? "+" : "";
|
|
1256
|
+
return `${sign}${dt.toFixed(2)}s`;
|
|
1257
|
+
}
|
|
1258
|
+
function formatTimelinePayload(kind, d) {
|
|
1259
|
+
switch (kind) {
|
|
1260
|
+
case "app-launch":
|
|
1261
|
+
return d.phase === "launch" ? `launch ${d.appName ?? d.toAppId ?? ""}` : `dismiss ${d.appName ?? d.fromAppId ?? ""} \u2192 ${d.toAppId ?? ""}`;
|
|
1262
|
+
case "toast":
|
|
1263
|
+
return `"${d.text ?? ""}"${d.durationMs ? ` (${d.durationMs}ms)` : ""}`;
|
|
1264
|
+
case "keyboard":
|
|
1265
|
+
return `${d.phase ?? "?"}${d.heightPx ? ` h=${d.heightPx}` : ""}${d.mode ? ` ${d.mode}` : ""}`;
|
|
1266
|
+
case "screen":
|
|
1267
|
+
return `${d.phase ?? "?"} ${d.name ?? d.activeName ?? ""}`;
|
|
1268
|
+
case "flow-step":
|
|
1269
|
+
return `${d.status ?? "?"} ${d.stepName ?? "step"}${d.durationMs ? ` ${d.durationMs}ms` : ""}`;
|
|
1270
|
+
case "route":
|
|
1271
|
+
return `${d.phase ?? "?"} ${d.path ?? d.pathname ?? ""}`;
|
|
1272
|
+
case "alert":
|
|
1273
|
+
case "actionsheet":
|
|
1274
|
+
case "picker":
|
|
1275
|
+
return `${d.phase ?? "?"} ${d.title ?? d.message ?? ""}`;
|
|
1276
|
+
case "notification":
|
|
1277
|
+
return `${d.title ?? ""}${d.body ? ` \u2014 ${d.body}` : ""}`;
|
|
1278
|
+
case "fetch":
|
|
1279
|
+
return `${d.method ?? "GET"} ${d.url ?? ""}${d.status ? ` -> ${d.status}` : ""}`;
|
|
1280
|
+
case "console":
|
|
1281
|
+
case "console-log":
|
|
1282
|
+
return `${d.level ?? "log"}: ${(d.message ?? "").toString().slice(0, 120)}`;
|
|
1283
|
+
case "shell":
|
|
1284
|
+
return `${d.event ?? d.type ?? d.phase ?? ""}`;
|
|
1285
|
+
case "scroll":
|
|
1286
|
+
return `${d.phase ?? "?"} ${d.target ?? ""}`;
|
|
1287
|
+
case "gesture":
|
|
1288
|
+
return `${d.phase ?? "?"} ${d.type ?? ""}`;
|
|
1289
|
+
case "text-input":
|
|
1290
|
+
return `${d.phase ?? "?"}${d.value !== void 0 ? ` "${String(d.value).slice(0, 40)}"` : ""}`;
|
|
1291
|
+
case "layout":
|
|
1292
|
+
return `${d.kind ?? "?"} ${d.testID ?? d.type ?? ""}${d.skipped ? ` skipped:${d.reason ?? "unknown"}` : ""}`;
|
|
1293
|
+
case "react-commit": {
|
|
1294
|
+
const slowest = d.slowest;
|
|
1295
|
+
return `${d.fiberCount ?? "?"} fibers ${d.durationMs ?? "?"}ms${slowest?.displayName ? ` \xB7 ${slowest.displayName} ${slowest.durationMs ?? "?"}ms` : ""}`;
|
|
1296
|
+
}
|
|
1297
|
+
case "frame":
|
|
1298
|
+
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` : ""}`;
|
|
1299
|
+
case "reanimated":
|
|
1300
|
+
case "animation":
|
|
1301
|
+
return `${d.kind ?? ""} ${d.target ?? ""}${d.durationMs ? ` ${d.durationMs}ms` : ""}`;
|
|
1302
|
+
}
|
|
1303
|
+
return "";
|
|
1522
1304
|
}
|
|
1523
|
-
|
|
1524
|
-
const
|
|
1525
|
-
const
|
|
1526
|
-
const
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
});
|
|
1531
|
-
return Array.isArray(result) ? result : [];
|
|
1305
|
+
function formatTimelineEvent(event, anchor) {
|
|
1306
|
+
const ts = formatTimelineRelativeTime(event.t, anchor).padStart(8);
|
|
1307
|
+
const ctx = event.context.padEnd(6);
|
|
1308
|
+
const kind = `[${event.kind}]`.padEnd(15);
|
|
1309
|
+
const d = event.data;
|
|
1310
|
+
const payload = d && typeof d === "object" ? formatTimelinePayload(event.kind, d) : "";
|
|
1311
|
+
return ` ${ts} ${ctx} ${kind} ${payload}`;
|
|
1532
1312
|
}
|
|
1533
|
-
|
|
1534
|
-
|
|
1313
|
+
function formatTimelineEvents(events) {
|
|
1314
|
+
if (!events.length) return "";
|
|
1315
|
+
const anchor = events[0]?.t ?? null;
|
|
1316
|
+
return events.map((e) => formatTimelineEvent(e, anchor)).join("\n");
|
|
1535
1317
|
}
|
|
1536
|
-
|
|
1318
|
+
|
|
1319
|
+
// cli/commands/inspect/resolve-target.ts
|
|
1320
|
+
async function resolveTargetCoords(bridge, target) {
|
|
1537
1321
|
const result = await bridge.send({
|
|
1538
|
-
type: "
|
|
1539
|
-
|
|
1540
|
-
args: [x, y]
|
|
1322
|
+
type: "resolve",
|
|
1323
|
+
selector: target.mode === "testid" ? { testID: target.value } : { text: target.value }
|
|
1541
1324
|
});
|
|
1542
|
-
if (
|
|
1543
|
-
|
|
1325
|
+
if (result === null) return null;
|
|
1326
|
+
if (typeof result !== "object") throw new Error("resolve returned an invalid result");
|
|
1327
|
+
const match = Reflect.get(result, "match");
|
|
1328
|
+
const resolvedTarget = Reflect.get(result, "target");
|
|
1329
|
+
const semanticNodes = [match, resolvedTarget];
|
|
1330
|
+
const point = Reflect.get(result, "point");
|
|
1331
|
+
const x = point !== null && typeof point === "object" ? Reflect.get(point, "x") : void 0;
|
|
1332
|
+
const y = point !== null && typeof point === "object" ? Reflect.get(point, "y") : void 0;
|
|
1333
|
+
if (!isSimSemanticTree(semanticNodes) || typeof x !== "number" || !Number.isFinite(x) || typeof y !== "number" || !Number.isFinite(y)) {
|
|
1334
|
+
throw new Error("resolve returned an invalid result");
|
|
1335
|
+
}
|
|
1336
|
+
const semanticMatch = semanticNodes[0];
|
|
1337
|
+
if (!semanticMatch) throw new Error("resolve returned an invalid result");
|
|
1544
1338
|
return {
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1339
|
+
x,
|
|
1340
|
+
y,
|
|
1341
|
+
testID: semanticMatch.testID ?? null,
|
|
1342
|
+
text: semanticMatch.text ?? (target.mode === "text" ? target.value : semanticMatch.label),
|
|
1343
|
+
type: semanticMatch.type
|
|
1548
1344
|
};
|
|
1549
1345
|
}
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1346
|
+
|
|
1347
|
+
// cli/commands/inspect/settling.ts
|
|
1348
|
+
async function waitForSootsimIdle({
|
|
1349
|
+
bridge,
|
|
1350
|
+
simId,
|
|
1351
|
+
maxMs,
|
|
1352
|
+
pollMs = 50,
|
|
1353
|
+
stablePolls = 3,
|
|
1354
|
+
strict = false
|
|
1355
|
+
}) {
|
|
1356
|
+
const result = await bridge.send(
|
|
1357
|
+
{
|
|
1358
|
+
type: "evaluate",
|
|
1359
|
+
simId,
|
|
1360
|
+
code: `(async () => {
|
|
1361
|
+
const start = Date.now()
|
|
1362
|
+
const deadline = start + ${Math.max(0, Math.round(maxMs))}
|
|
1363
|
+
const pollMs = ${Math.max(1, Math.round(pollMs))}
|
|
1364
|
+
const requiredStablePolls = ${Math.max(1, Math.round(stablePolls))}
|
|
1365
|
+
const strict = ${strict ? "true" : "false"}
|
|
1366
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
1367
|
+
|
|
1368
|
+
// route-aware settle: a tap that pushes/pops a screen is async \u2014 the
|
|
1369
|
+
// old screen still renders for a moment, then the new one mounts and
|
|
1370
|
+
// its content loads. a pure layout-hash check reports "idle" on the
|
|
1371
|
+
// outgoing screen or on the incoming skeleton, so a driver re-taps
|
|
1372
|
+
// and stacks duplicate navigations. drain any in-flight screen
|
|
1373
|
+
// transition FIRST, bounded by the overall budget. when no
|
|
1374
|
+
// transition is happening this returns in ~80ms (startWindowMs), so
|
|
1375
|
+
// a plain button tap barely pays for it.
|
|
1376
|
+
try {
|
|
1377
|
+
const wfst = window.__sootsimTest?.waitForScreenTransitions
|
|
1378
|
+
if (typeof wfst === 'function') {
|
|
1379
|
+
const remaining = deadline - Date.now()
|
|
1380
|
+
if (remaining > 120) {
|
|
1381
|
+
await wfst({
|
|
1382
|
+
timeoutMs: Math.min(remaining - 80, 4000),
|
|
1383
|
+
settleMs: 64,
|
|
1384
|
+
startWindowMs: 80,
|
|
1385
|
+
})
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
} catch {}
|
|
1389
|
+
|
|
1390
|
+
const readSnapshot = async () => {
|
|
1391
|
+
let animating = false
|
|
1392
|
+
let layoutDirty = false
|
|
1393
|
+
let pendingFetches = 0
|
|
1394
|
+
let requestTotal = 0
|
|
1395
|
+
let requestInFlight = 0
|
|
1396
|
+
let renderStatsAvailable = false
|
|
1397
|
+
try {
|
|
1398
|
+
const stats = await window.__sootsimRenderHost?.queryStats?.()
|
|
1399
|
+
if (stats) {
|
|
1400
|
+
renderStatsAvailable = true
|
|
1401
|
+
animating =
|
|
1402
|
+
stats.hasActiveAnims === true ||
|
|
1403
|
+
stats.hasActiveNativeAnimations === true ||
|
|
1404
|
+
stats.hasPendingAnimationFrames === true
|
|
1405
|
+
layoutDirty =
|
|
1406
|
+
stats.layoutDirty === true && stats.renderRequested === true
|
|
1407
|
+
// a freshly-pushed screen showing a skeleton is layout-stable
|
|
1408
|
+
// but not actually settled \u2014 its data/images are still in
|
|
1409
|
+
// flight. treat bounded image-loader fetches as not-idle so
|
|
1410
|
+
// settle waits for real content, capped by the budget.
|
|
1411
|
+
const pf = stats.memory && stats.memory.imageLoader
|
|
1412
|
+
? stats.memory.imageLoader.pendingFetches
|
|
1413
|
+
: 0
|
|
1414
|
+
pendingFetches = typeof pf === 'number' ? pf : 0
|
|
1415
|
+
}
|
|
1416
|
+
} catch {}
|
|
1417
|
+
try {
|
|
1418
|
+
const counts = await window.__sootsimTest?.getRequestCounts?.()
|
|
1419
|
+
requestTotal = typeof counts?.total === 'number' ? counts.total : 0
|
|
1420
|
+
requestInFlight = typeof counts?.inFlight === 'number' ? counts.inFlight : 0
|
|
1421
|
+
} catch {}
|
|
1422
|
+
const root = window.__sootsimRoot
|
|
1423
|
+
const nodes = []
|
|
1424
|
+
if (root) {
|
|
1425
|
+
const walk = (n) => {
|
|
1426
|
+
if (n.layout && n.layout.width > 0) {
|
|
1427
|
+
nodes.push(Math.round(n.layout.x) + ',' + Math.round(n.layout.y) + ',' + Math.round(n.layout.width))
|
|
1428
|
+
}
|
|
1429
|
+
for (const c of n.children || []) walk(c)
|
|
1430
|
+
}
|
|
1431
|
+
walk(root)
|
|
1432
|
+
}
|
|
1433
|
+
return { layout: nodes.join(';'), animating, layoutDirty, pendingFetches, renderStatsAvailable, requestTotal, requestInFlight }
|
|
1561
1434
|
}
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1435
|
+
|
|
1436
|
+
// how long generic background network is allowed to keep us waiting AFTER
|
|
1437
|
+
// the screen is otherwise visually settled. apps with continuous traffic
|
|
1438
|
+
// (polling, realtime sockets, a tap that fires 20-50 fetches) never reach
|
|
1439
|
+
// requestInFlight===0 / a stable requestTotal, so requiring full network
|
|
1440
|
+
// quiet burns the ENTIRE budget on every command \u2014 the leading idle that
|
|
1441
|
+
// failed PR-preview recordings for network-heavy apps (3pc fight-pulse).
|
|
1442
|
+
// layout-stability + the image-loader already prove visible content
|
|
1443
|
+
// loaded (a data load changes layout; an image load shows in the loader),
|
|
1444
|
+
// so the generic request counters are a courtesy, not a gate: honor them
|
|
1445
|
+
// only within this grace once the screen is visually still.
|
|
1446
|
+
const networkQuietGraceMs = 1200
|
|
1447
|
+
let lastLayout = ''
|
|
1448
|
+
let lastRequestTotal = -1
|
|
1449
|
+
let stable = 0
|
|
1450
|
+
let visuallyStillSince = 0
|
|
1451
|
+
while (Date.now() < deadline) {
|
|
1452
|
+
const snapshot = await readSnapshot()
|
|
1453
|
+
const strictOk =
|
|
1454
|
+
!strict ||
|
|
1455
|
+
(snapshot.renderStatsAvailable && !snapshot.animating && !snapshot.layoutDirty)
|
|
1456
|
+
// visually settled: no animations, no image content loading, layout
|
|
1457
|
+
// unchanged since the last poll.
|
|
1458
|
+
const visuallyStill =
|
|
1459
|
+
strictOk && snapshot.pendingFetches === 0 && snapshot.layout === lastLayout
|
|
1460
|
+
// generic network quiet: nothing in flight and no new requests issued.
|
|
1461
|
+
const networkQuiet =
|
|
1462
|
+
snapshot.requestInFlight === 0 && snapshot.requestTotal === lastRequestTotal
|
|
1463
|
+
if (visuallyStill) {
|
|
1464
|
+
stable++
|
|
1465
|
+
if (visuallyStillSince === 0) visuallyStillSince = Date.now()
|
|
1466
|
+
// settle once the screen has held still long enough AND either the
|
|
1467
|
+
// network is genuinely quiet OR it has refused to quiet within the
|
|
1468
|
+
// grace (continuous background traffic must not block forever).
|
|
1469
|
+
if (
|
|
1470
|
+
stable >= requiredStablePolls &&
|
|
1471
|
+
(networkQuiet || Date.now() - visuallyStillSince >= networkQuietGraceMs)
|
|
1472
|
+
) {
|
|
1473
|
+
return { settled: true, elapsed: Date.now() - start }
|
|
1474
|
+
}
|
|
1475
|
+
} else {
|
|
1476
|
+
stable = 0
|
|
1477
|
+
visuallyStillSince = 0
|
|
1580
1478
|
}
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
})()`
|
|
1591
|
-
});
|
|
1592
|
-
if (!res || !res.ok) return [];
|
|
1593
|
-
return res.entries ?? [];
|
|
1594
|
-
}
|
|
1595
|
-
async function clearLogs(bridge) {
|
|
1596
|
-
await bridge.send({
|
|
1597
|
-
type: "evaluate",
|
|
1598
|
-
code: 'window.__sootsimObservability?.logs.clear(); "cleared"'
|
|
1599
|
-
});
|
|
1600
|
-
}
|
|
1601
|
-
function isForwardedWorkerLogTwin(a, b) {
|
|
1602
|
-
if (a.source === b.source) return false;
|
|
1603
|
-
if (a.level !== b.level) return false;
|
|
1604
|
-
if (Math.abs(a.ts - b.ts) > 1e3) return false;
|
|
1605
|
-
if (a.args.length !== b.args.length) return false;
|
|
1606
|
-
const sources = /* @__PURE__ */ new Set([a.source, b.source]);
|
|
1607
|
-
if (!sources.has("sootsim-worker")) return false;
|
|
1608
|
-
if (!sources.has("render-worker") && !sources.has("forwarded-render-worker")) {
|
|
1609
|
-
return false;
|
|
1610
|
-
}
|
|
1611
|
-
return a.args.every((arg, index) => arg === b.args[index]);
|
|
1612
|
-
}
|
|
1613
|
-
function filterLogEntries(entries, opts = {}) {
|
|
1614
|
-
let out = [];
|
|
1615
|
-
for (const entry of entries) {
|
|
1616
|
-
if (out.some((candidate) => isForwardedWorkerLogTwin(candidate, entry))) {
|
|
1617
|
-
continue;
|
|
1479
|
+
lastLayout = snapshot.layout
|
|
1480
|
+
lastRequestTotal = snapshot.requestTotal
|
|
1481
|
+
await sleep(pollMs)
|
|
1482
|
+
}
|
|
1483
|
+
return { settled: false, elapsed: Date.now() - start }
|
|
1484
|
+
})()`
|
|
1485
|
+
},
|
|
1486
|
+
{
|
|
1487
|
+
timeoutMs: maxMs + 1e3
|
|
1618
1488
|
}
|
|
1619
|
-
|
|
1620
|
-
}
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1489
|
+
);
|
|
1490
|
+
const { elapsed, settled } = result ?? {};
|
|
1491
|
+
return {
|
|
1492
|
+
elapsed: typeof elapsed === "number" ? elapsed : maxMs,
|
|
1493
|
+
settled: settled === true
|
|
1494
|
+
};
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
// cli/commands/inspect/actions.ts
|
|
1498
|
+
function isScreenSize(value) {
|
|
1499
|
+
if (value === null || typeof value !== "object") return false;
|
|
1500
|
+
const width = Reflect.get(value, "width");
|
|
1501
|
+
const height = Reflect.get(value, "height");
|
|
1502
|
+
return typeof width === "number" && Number.isFinite(width) && width > 0 && typeof height === "number" && Number.isFinite(height) && height > 0;
|
|
1503
|
+
}
|
|
1504
|
+
function viewportFromCapture(value) {
|
|
1505
|
+
if (value === null || typeof value !== "object") return void 0;
|
|
1506
|
+
const tree = Reflect.get(value, "tree");
|
|
1507
|
+
return tree !== null && typeof tree === "object" ? Reflect.get(tree, "viewport") : void 0;
|
|
1508
|
+
}
|
|
1509
|
+
async function readScreen(bridge) {
|
|
1510
|
+
try {
|
|
1511
|
+
const viewport = viewportFromCapture(
|
|
1512
|
+
await bridge.send({ type: "capture", includeVisual: false })
|
|
1513
|
+
);
|
|
1514
|
+
return isScreenSize(viewport) ? { width: viewport.width, height: viewport.height } : null;
|
|
1515
|
+
} catch {
|
|
1516
|
+
return null;
|
|
1626
1517
|
}
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1518
|
+
}
|
|
1519
|
+
function offscreenPayload(cx, cy, screen) {
|
|
1520
|
+
if (!screen || cx >= 0 && cy >= 0 && cx <= screen.width && cy <= screen.height) {
|
|
1521
|
+
return {};
|
|
1631
1522
|
}
|
|
1632
|
-
return
|
|
1523
|
+
return { offscreen: true, screen };
|
|
1633
1524
|
}
|
|
1634
|
-
|
|
1635
|
-
return
|
|
1636
|
-
type: "call",
|
|
1637
|
-
path: "SootSim.bridges.timeline.summary",
|
|
1638
|
-
args: [query]
|
|
1639
|
-
});
|
|
1525
|
+
function geometryContains(geometry, x, y) {
|
|
1526
|
+
return x >= geometry.x && y >= geometry.y && x <= geometry.x + geometry.width && y <= geometry.y + geometry.height;
|
|
1640
1527
|
}
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1528
|
+
function hitTestSemanticTree(nodes, x, y, ancestors = []) {
|
|
1529
|
+
for (let index = nodes.length - 1; index >= 0; index--) {
|
|
1530
|
+
const node = nodes[index];
|
|
1531
|
+
if (!node || !geometryContains(node.geometry, x, y)) continue;
|
|
1532
|
+
const childHit = node.children?.length ? hitTestSemanticTree(node.children, x, y, [...ancestors, node]) : null;
|
|
1533
|
+
return childHit ?? { node, ancestors };
|
|
1534
|
+
}
|
|
1535
|
+
return null;
|
|
1647
1536
|
}
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
const res = await inspectTimelineRecent(bridge, {
|
|
1652
|
-
kinds: ["alert", "actionsheet"],
|
|
1653
|
-
limit: 80
|
|
1654
|
-
});
|
|
1655
|
-
events = res.events;
|
|
1656
|
-
} catch {
|
|
1657
|
-
return [];
|
|
1537
|
+
function nodeMatchesRequested(node, requested) {
|
|
1538
|
+
if (typeof requested.nodeId === "number" && node.nodeId === requested.nodeId) {
|
|
1539
|
+
return true;
|
|
1658
1540
|
}
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1541
|
+
if (requested.testID && node.testID === requested.testID) return true;
|
|
1542
|
+
if (requested.id && node.testID === requested.id) return true;
|
|
1543
|
+
return false;
|
|
1544
|
+
}
|
|
1545
|
+
function coveringNodeSummary(node) {
|
|
1546
|
+
return {
|
|
1547
|
+
nodeId: node.nodeId,
|
|
1548
|
+
testID: node.testID ?? null,
|
|
1549
|
+
text: node.text ?? node.label ?? null,
|
|
1550
|
+
type: node.type
|
|
1668
1551
|
};
|
|
1669
|
-
check("alert", "Alert");
|
|
1670
|
-
check("actionsheet", "ActionSheet");
|
|
1671
|
-
return out;
|
|
1672
1552
|
}
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1553
|
+
var DEFAULT_AGENT_TIMING = {
|
|
1554
|
+
initialWaitMs: 3e3,
|
|
1555
|
+
deadlineMs: 5e3,
|
|
1556
|
+
retryWaitMs: 700
|
|
1557
|
+
};
|
|
1558
|
+
var DEFAULT_INTERACTIVE_TIMING = {
|
|
1559
|
+
initialWaitMs: 1200,
|
|
1560
|
+
deadlineMs: 2500,
|
|
1561
|
+
retryWaitMs: 700
|
|
1562
|
+
};
|
|
1563
|
+
function tapTiming(agent, opts = {}) {
|
|
1564
|
+
return {
|
|
1565
|
+
...agent ? DEFAULT_AGENT_TIMING : DEFAULT_INTERACTIVE_TIMING,
|
|
1566
|
+
...opts
|
|
1567
|
+
};
|
|
1679
1568
|
}
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
return kb.getLayout()
|
|
1689
|
-
})()`
|
|
1690
|
-
});
|
|
1691
|
-
return result ?? { error: "keyboard bridge returned no result" };
|
|
1569
|
+
function isTapSuccess(result) {
|
|
1570
|
+
if (!result || result.hit === false || result.ok === false) return false;
|
|
1571
|
+
if (result.handled === false) return false;
|
|
1572
|
+
if (result.requestedTargetMatched === false) return false;
|
|
1573
|
+
if (result.pointerTapHandled === false && !result.keyboardOpened && !result.isTextInput) {
|
|
1574
|
+
return false;
|
|
1575
|
+
}
|
|
1576
|
+
return true;
|
|
1692
1577
|
}
|
|
1693
|
-
function
|
|
1694
|
-
|
|
1695
|
-
|
|
1578
|
+
function tapTargetFromPayload(payload, textFallback) {
|
|
1579
|
+
return {
|
|
1580
|
+
nodeId: payload.target?.nodeId ?? payload.match?.nodeId ?? payload.node?.nodeId ?? null,
|
|
1581
|
+
id: payload.target?.id ?? payload.match?.id ?? payload.node?.id ?? null,
|
|
1582
|
+
testID: payload.target?.testID ?? payload.match?.testID ?? payload.node?.testID ?? null,
|
|
1583
|
+
text: payload.target?.text ?? payload.target?.accessibilityLabel ?? payload.match?.text ?? payload.match?.accessibilityLabel ?? payload.node?.text ?? textFallback ?? null,
|
|
1584
|
+
type: payload.target?.type ?? payload.match?.type ?? payload.node?.type ?? null
|
|
1585
|
+
};
|
|
1696
1586
|
}
|
|
1697
|
-
async function
|
|
1698
|
-
const
|
|
1699
|
-
|
|
1587
|
+
async function tapResolvedTarget(bridge, args) {
|
|
1588
|
+
const timing = tapTiming(!!args.agent, args.timing);
|
|
1589
|
+
let attempts = 0;
|
|
1590
|
+
let lastPayload = null;
|
|
1591
|
+
let lastResult = null;
|
|
1592
|
+
const canWaitForIdle = bridge.plane !== "cloud" && timing.initialWaitMs > 0;
|
|
1593
|
+
if (canWaitForIdle) {
|
|
1700
1594
|
try {
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1595
|
+
await waitForSootsimIdle({
|
|
1596
|
+
bridge,
|
|
1597
|
+
maxMs: timing.initialWaitMs,
|
|
1598
|
+
pollMs: 32,
|
|
1599
|
+
stablePolls: 2
|
|
1705
1600
|
});
|
|
1706
|
-
} catch
|
|
1707
|
-
|
|
1708
|
-
|
|
1601
|
+
} catch {
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
const deadline = Date.now() + timing.deadlineMs;
|
|
1605
|
+
let lastOffscreenAt = null;
|
|
1606
|
+
const screen = await readScreen(bridge);
|
|
1607
|
+
while (Date.now() <= deadline || attempts === 0) {
|
|
1608
|
+
attempts++;
|
|
1609
|
+
const resolved = await args.resolve();
|
|
1610
|
+
const payload = resolved && typeof resolved.cx === "number" && typeof resolved.cy === "number" ? { ...resolved, ...offscreenPayload(resolved.cx, resolved.cy, screen) } : resolved;
|
|
1611
|
+
lastPayload = payload;
|
|
1612
|
+
if (payload?.error === "bridge-not-ready" || payload?.ambiguous || payload?.nthOutOfRange) {
|
|
1613
|
+
return { payload, result: null, attempts, failure: "special" };
|
|
1614
|
+
}
|
|
1615
|
+
if (payload && typeof payload.cx === "number" && typeof payload.cy === "number") {
|
|
1616
|
+
if (payload.offscreen) {
|
|
1617
|
+
lastResult = {
|
|
1618
|
+
hit: false,
|
|
1619
|
+
reason: "offscreen",
|
|
1620
|
+
x: payload.cx,
|
|
1621
|
+
y: payload.cy,
|
|
1622
|
+
screen: payload.screen
|
|
1623
|
+
};
|
|
1624
|
+
const at = `${Math.round(payload.cx)},${Math.round(payload.cy)}`;
|
|
1625
|
+
if (lastOffscreenAt === at) {
|
|
1626
|
+
return { payload, result: lastResult, attempts, failure: "missed" };
|
|
1627
|
+
}
|
|
1628
|
+
lastOffscreenAt = at;
|
|
1629
|
+
} else {
|
|
1630
|
+
lastOffscreenAt = null;
|
|
1631
|
+
const requestedTarget = tapTargetFromPayload(payload, args.textFallback);
|
|
1632
|
+
const { tree } = await inspectTree(bridge, 50);
|
|
1633
|
+
if (Array.isArray(tree) && tree.length > 0) {
|
|
1634
|
+
const hit = hitTestSemanticTree(tree, payload.cx, payload.cy);
|
|
1635
|
+
const matched = hit !== null && (nodeMatchesRequested(hit.node, requestedTarget) || hit.ancestors.some(
|
|
1636
|
+
(ancestor) => nodeMatchesRequested(ancestor, requestedTarget)
|
|
1637
|
+
));
|
|
1638
|
+
if (hit && !matched) {
|
|
1639
|
+
lastResult = {
|
|
1640
|
+
hit: false,
|
|
1641
|
+
reason: "target-covered",
|
|
1642
|
+
requestedTarget,
|
|
1643
|
+
coordinateTarget: coveringNodeSummary(hit.node)
|
|
1644
|
+
};
|
|
1645
|
+
return { payload, result: lastResult, attempts, failure: "missed" };
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
const result = await tapCoordinates(
|
|
1649
|
+
bridge,
|
|
1650
|
+
payload.cx,
|
|
1651
|
+
payload.cy,
|
|
1652
|
+
requestedTarget
|
|
1653
|
+
);
|
|
1654
|
+
lastResult = result;
|
|
1655
|
+
if (isTapSuccess(result)) return { payload, result, attempts };
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
const remaining = deadline - Date.now();
|
|
1659
|
+
if (remaining <= 0) break;
|
|
1660
|
+
if (bridge.plane === "cloud" || timing.retryWaitMs <= 0) break;
|
|
1661
|
+
try {
|
|
1662
|
+
await waitForSootsimIdle({
|
|
1663
|
+
bridge,
|
|
1664
|
+
maxMs: Math.min(remaining, timing.retryWaitMs),
|
|
1665
|
+
pollMs: 32,
|
|
1666
|
+
stablePolls: 2
|
|
1667
|
+
});
|
|
1668
|
+
} catch {
|
|
1669
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(120, remaining)));
|
|
1709
1670
|
}
|
|
1710
1671
|
}
|
|
1711
|
-
}
|
|
1712
|
-
async function inspectScreens(bridge) {
|
|
1713
|
-
const payload = await bridge.send({
|
|
1714
|
-
type: "evaluate",
|
|
1715
|
-
code: `(async () => {
|
|
1716
|
-
const test = window.__sootsimTest
|
|
1717
|
-
const kb = window.__sootsimKeyboard
|
|
1718
|
-
const navSnap =
|
|
1719
|
-
test && typeof test.getNavigationSnapshot === 'function'
|
|
1720
|
-
? await test.getNavigationSnapshot()
|
|
1721
|
-
: null
|
|
1722
|
-
const route =
|
|
1723
|
-
test && typeof test.getRouteInfo === 'function'
|
|
1724
|
-
? await test.getRouteInfo()
|
|
1725
|
-
: null
|
|
1726
|
-
const keyboard =
|
|
1727
|
-
kb && typeof kb.getLayout === 'function'
|
|
1728
|
-
? (() => {
|
|
1729
|
-
const layout = kb.getLayout()
|
|
1730
|
-
return layout ? {
|
|
1731
|
-
visible: layout.visible,
|
|
1732
|
-
mode: layout.mode,
|
|
1733
|
-
spec: layout.spec
|
|
1734
|
-
? {
|
|
1735
|
-
keyboardType: layout.spec.keyboardType,
|
|
1736
|
-
returnKeyType: layout.spec.returnKeyType,
|
|
1737
|
-
}
|
|
1738
|
-
: null,
|
|
1739
|
-
} : null
|
|
1740
|
-
})()
|
|
1741
|
-
: null
|
|
1742
|
-
return { nav: navSnap, route, keyboard }
|
|
1743
|
-
})()`
|
|
1744
|
-
});
|
|
1745
|
-
const shell = await getShellState(bridge, 500).catch(() => null);
|
|
1746
1672
|
return {
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1673
|
+
payload: lastPayload,
|
|
1674
|
+
result: lastResult,
|
|
1675
|
+
attempts,
|
|
1676
|
+
failure: lastPayload && typeof lastPayload.cx === "number" ? "missed" : "not-found"
|
|
1751
1677
|
};
|
|
1752
1678
|
}
|
|
1753
|
-
|
|
1754
|
-
"portals",
|
|
1755
|
-
"sheets",
|
|
1756
|
-
"layout",
|
|
1757
|
-
"onlayout",
|
|
1758
|
-
"animated",
|
|
1759
|
-
"render",
|
|
1760
|
-
"touch",
|
|
1761
|
-
"yoga"
|
|
1762
|
-
];
|
|
1763
|
-
async function inspectDebugStatus(bridge) {
|
|
1764
|
-
return bridge.send({ type: "evaluate", code: "window.__sootsimDebug.status()" });
|
|
1765
|
-
}
|
|
1766
|
-
async function inspectDebugFlags(bridge) {
|
|
1767
|
-
return bridge.send({ type: "evaluate", code: "window.__sootsimDebug.flags()" });
|
|
1768
|
-
}
|
|
1769
|
-
async function inspectDebugFind(bridge, target) {
|
|
1770
|
-
return bridge.send({
|
|
1771
|
-
type: "evaluate",
|
|
1772
|
-
code: `window.__sootsimTest.debugFind(${JSON.stringify(target)})`
|
|
1773
|
-
});
|
|
1774
|
-
}
|
|
1775
|
-
async function inspectDebugRecent(bridge, channel, limit = 50) {
|
|
1776
|
-
const code = channel && channel !== "all" ? `window.__sootsimDebug.recent(${JSON.stringify(channel)}, ${limit})` : `window.__sootsimDebug.recent(undefined, ${limit})`;
|
|
1777
|
-
return bridge.send({ type: "evaluate", code });
|
|
1778
|
-
}
|
|
1779
|
-
async function setDebugChannels(bridge, action, channels) {
|
|
1780
|
-
const args = channels.length > 0 ? channels.map((c) => JSON.stringify(c)).join(", ") : action === "disable" ? "'all'" : "";
|
|
1679
|
+
async function tapCoordinates(bridge, x, y, target) {
|
|
1781
1680
|
return bridge.send({
|
|
1782
|
-
type: "
|
|
1783
|
-
|
|
1681
|
+
type: "tap",
|
|
1682
|
+
x,
|
|
1683
|
+
y,
|
|
1684
|
+
...target ? { target } : {}
|
|
1784
1685
|
});
|
|
1785
1686
|
}
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
}
|
|
1807
|
-
const result = await bridge.send({
|
|
1808
|
-
type: "evaluate",
|
|
1809
|
-
code: `(async () => {
|
|
1810
|
-
const host = window.__sootsimRenderHost
|
|
1811
|
-
const stats = host?.queryStats ? await host.queryStats() : null
|
|
1812
|
-
const shellRaw = window.__sootsimShellMemory
|
|
1813
|
-
? await window.__sootsimShellMemory()
|
|
1814
|
-
: null
|
|
1815
|
-
const shell = shellRaw && !shellRaw.error ? shellRaw : null
|
|
1816
|
-
// the compositor paints home + app:one + app:two, so every app-surface
|
|
1817
|
-
// picture and raster image is counted here and nowhere else.
|
|
1818
|
-
let compositor = null
|
|
1819
|
-
try {
|
|
1820
|
-
const cs = await window.__sootsimCompositor?.getStats?.(false, true)
|
|
1821
|
-
compositor = cs?.memory ?? null
|
|
1822
|
-
} catch {}
|
|
1823
|
-
const normalize = (m) =>
|
|
1824
|
-
m
|
|
1825
|
-
? {
|
|
1826
|
-
nodesRegistered: m.nodesRegistered ?? null,
|
|
1827
|
-
nodesDetached: m.nodesDetached ?? null,
|
|
1828
|
-
detachedTypes: m.detachedTypes ?? null,
|
|
1829
|
-
objects: m.objects ?? null,
|
|
1830
|
-
activeNativeAnimations: m.activeNativeAnimations ?? null,
|
|
1831
|
-
imageLoader: m.imageLoader ?? null,
|
|
1832
|
-
workerHeap: m.workerHeap ?? null,
|
|
1833
|
-
wasmHeapBytes: m.wasmHeapBytes ?? null,
|
|
1834
|
-
}
|
|
1835
|
-
: null
|
|
1836
|
-
const hostMem = performance.memory
|
|
1837
|
-
? {
|
|
1838
|
-
usedJSHeapSize: performance.memory.usedJSHeapSize,
|
|
1839
|
-
totalJSHeapSize: performance.memory.totalJSHeapSize,
|
|
1840
|
-
jsHeapSizeLimit: performance.memory.jsHeapSizeLimit,
|
|
1841
|
-
}
|
|
1842
|
-
: null
|
|
1843
|
-
return {
|
|
1844
|
-
tenant: normalize(stats?.memory),
|
|
1845
|
-
shell: normalize(shell),
|
|
1846
|
-
compositor: normalize(compositor),
|
|
1847
|
-
hostHeap: hostMem,
|
|
1848
|
-
}
|
|
1849
|
-
})()`
|
|
1850
|
-
});
|
|
1851
|
-
return result ?? { tenant: null, shell: null, compositor: null, hostHeap: null };
|
|
1687
|
+
function payloadFromResolved(resolved, match, extra = {}) {
|
|
1688
|
+
return {
|
|
1689
|
+
cx: resolved.x,
|
|
1690
|
+
cy: resolved.y,
|
|
1691
|
+
match: {
|
|
1692
|
+
nodeId: match.nodeId ?? null,
|
|
1693
|
+
id: match.testID ?? null,
|
|
1694
|
+
testID: match.testID ?? null,
|
|
1695
|
+
text: match.text ?? null,
|
|
1696
|
+
type: match.type ?? null
|
|
1697
|
+
},
|
|
1698
|
+
target: {
|
|
1699
|
+
nodeId: match.nodeId ?? null,
|
|
1700
|
+
id: resolved.testID ?? match.testID ?? null,
|
|
1701
|
+
testID: resolved.testID ?? match.testID ?? null,
|
|
1702
|
+
text: resolved.text ?? match.text ?? null,
|
|
1703
|
+
type: resolved.type ?? match.type ?? null
|
|
1704
|
+
},
|
|
1705
|
+
strategy: "matched-node",
|
|
1706
|
+
...extra
|
|
1707
|
+
};
|
|
1852
1708
|
}
|
|
1853
|
-
function
|
|
1854
|
-
|
|
1855
|
-
const
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
"fetch",
|
|
1861
|
-
"toast",
|
|
1862
|
-
"alert",
|
|
1863
|
-
"actionsheet",
|
|
1864
|
-
"picker",
|
|
1865
|
-
"notification",
|
|
1866
|
-
"screen",
|
|
1867
|
-
"route",
|
|
1868
|
-
"keyboard",
|
|
1869
|
-
"app-launch",
|
|
1870
|
-
"shell",
|
|
1871
|
-
"scroll",
|
|
1872
|
-
"gesture",
|
|
1873
|
-
"text-input",
|
|
1874
|
-
"react-commit",
|
|
1875
|
-
"animation",
|
|
1876
|
-
"reanimated"
|
|
1877
|
-
];
|
|
1878
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1879
|
-
for (const k of ORDER) {
|
|
1880
|
-
const n = summary.byKind[k];
|
|
1881
|
-
if (n) {
|
|
1882
|
-
parts.push(`${n} ${k}${n === 1 ? "" : "s"}`);
|
|
1883
|
-
seen.add(k);
|
|
1709
|
+
function collectTextCandidates(nodes, ancestors = []) {
|
|
1710
|
+
const out = [];
|
|
1711
|
+
for (const node of nodes) {
|
|
1712
|
+
out.push({ node, ancestorTestIDs: ancestors });
|
|
1713
|
+
if (node.children?.length) {
|
|
1714
|
+
const next = node.testID ? [...ancestors, node.testID] : ancestors;
|
|
1715
|
+
out.push(...collectTextCandidates(node.children, next));
|
|
1884
1716
|
}
|
|
1885
1717
|
}
|
|
1886
|
-
|
|
1887
|
-
if (!seen.has(k) && n) parts.push(`${n} ${k}${n === 1 ? "" : "s"}`);
|
|
1888
|
-
}
|
|
1889
|
-
return parts.join(" \xB7 ");
|
|
1718
|
+
return out;
|
|
1890
1719
|
}
|
|
1891
|
-
|
|
1892
|
-
"
|
|
1893
|
-
"layout",
|
|
1894
|
-
"scroll"
|
|
1895
|
-
]);
|
|
1896
|
-
function formatTimelineRelativeTime(t, anchor) {
|
|
1897
|
-
if (anchor === null) return new Date(t).toLocaleTimeString();
|
|
1898
|
-
const dt = (t - anchor) / 1e3;
|
|
1899
|
-
const sign = dt >= 0 ? "+" : "";
|
|
1900
|
-
return `${sign}${dt.toFixed(2)}s`;
|
|
1720
|
+
function nodeText(node) {
|
|
1721
|
+
return node.text ?? node.label ?? "";
|
|
1901
1722
|
}
|
|
1902
|
-
function
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
return
|
|
1910
|
-
case "screen":
|
|
1911
|
-
return `${d.phase ?? "?"} ${d.name ?? d.activeName ?? ""}`;
|
|
1912
|
-
case "flow-step":
|
|
1913
|
-
return `${d.status ?? "?"} ${d.stepName ?? "step"}${d.durationMs ? ` ${d.durationMs}ms` : ""}`;
|
|
1914
|
-
case "route":
|
|
1915
|
-
return `${d.phase ?? "?"} ${d.path ?? d.pathname ?? ""}`;
|
|
1916
|
-
case "alert":
|
|
1917
|
-
case "actionsheet":
|
|
1918
|
-
case "picker":
|
|
1919
|
-
return `${d.phase ?? "?"} ${d.title ?? d.message ?? ""}`;
|
|
1920
|
-
case "notification":
|
|
1921
|
-
return `${d.title ?? ""}${d.body ? ` \u2014 ${d.body}` : ""}`;
|
|
1922
|
-
case "fetch":
|
|
1923
|
-
return `${d.method ?? "GET"} ${d.url ?? ""}${d.status ? ` -> ${d.status}` : ""}`;
|
|
1924
|
-
case "console":
|
|
1925
|
-
case "console-log":
|
|
1926
|
-
return `${d.level ?? "log"}: ${(d.message ?? "").toString().slice(0, 120)}`;
|
|
1927
|
-
case "shell":
|
|
1928
|
-
return `${d.event ?? d.type ?? d.phase ?? ""}`;
|
|
1929
|
-
case "scroll":
|
|
1930
|
-
return `${d.phase ?? "?"} ${d.target ?? ""}`;
|
|
1931
|
-
case "gesture":
|
|
1932
|
-
return `${d.phase ?? "?"} ${d.type ?? ""}`;
|
|
1933
|
-
case "text-input":
|
|
1934
|
-
return `${d.phase ?? "?"}${d.value !== void 0 ? ` "${String(d.value).slice(0, 40)}"` : ""}`;
|
|
1935
|
-
case "layout":
|
|
1936
|
-
return `${d.kind ?? "?"} ${d.testID ?? d.type ?? ""}${d.skipped ? ` skipped:${d.reason ?? "unknown"}` : ""}`;
|
|
1937
|
-
case "react-commit": {
|
|
1938
|
-
const slowest = d.slowest;
|
|
1939
|
-
return `${d.fiberCount ?? "?"} fibers ${d.durationMs ?? "?"}ms${slowest?.displayName ? ` \xB7 ${slowest.displayName} ${slowest.durationMs ?? "?"}ms` : ""}`;
|
|
1723
|
+
async function tapById(bridge, query, opts = {}) {
|
|
1724
|
+
return tapResolvedTarget(bridge, {
|
|
1725
|
+
agent: opts.agent,
|
|
1726
|
+
timing: opts.timing,
|
|
1727
|
+
resolve: async () => {
|
|
1728
|
+
const resolved = await resolveTargetCoords(bridge, { mode: "testid", value: query });
|
|
1729
|
+
if (!resolved) return { cx: null };
|
|
1730
|
+
return payloadFromResolved(resolved, resolved);
|
|
1940
1731
|
}
|
|
1941
|
-
|
|
1942
|
-
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` : ""}`;
|
|
1943
|
-
case "reanimated":
|
|
1944
|
-
case "animation":
|
|
1945
|
-
return `${d.kind ?? ""} ${d.target ?? ""}${d.durationMs ? ` ${d.durationMs}ms` : ""}`;
|
|
1946
|
-
}
|
|
1947
|
-
return "";
|
|
1948
|
-
}
|
|
1949
|
-
function formatTimelineEvent(event, anchor) {
|
|
1950
|
-
const ts = formatTimelineRelativeTime(event.t, anchor).padStart(8);
|
|
1951
|
-
const ctx = event.context.padEnd(6);
|
|
1952
|
-
const kind = `[${event.kind}]`.padEnd(15);
|
|
1953
|
-
const d = event.data;
|
|
1954
|
-
const payload = d && typeof d === "object" ? formatTimelinePayload(event.kind, d) : "";
|
|
1955
|
-
return ` ${ts} ${ctx} ${kind} ${payload}`;
|
|
1732
|
+
});
|
|
1956
1733
|
}
|
|
1957
|
-
function
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1734
|
+
async function tapByText(bridge, query, options = {}, opts = {}) {
|
|
1735
|
+
return tapResolvedTarget(bridge, {
|
|
1736
|
+
agent: opts.agent,
|
|
1737
|
+
timing: opts.timing,
|
|
1738
|
+
textFallback: query,
|
|
1739
|
+
resolve: async () => {
|
|
1740
|
+
const { tree } = await inspectTree(bridge, 50);
|
|
1741
|
+
if (!Array.isArray(tree)) return { matched: 0, total: 0 };
|
|
1742
|
+
const needle = options.exact ? query : query.toLowerCase();
|
|
1743
|
+
let candidates = collectTextCandidates(tree).filter(({ node: node2, ancestorTestIDs }) => {
|
|
1744
|
+
const text = nodeText(node2);
|
|
1745
|
+
if (!text) return false;
|
|
1746
|
+
if (options.exact ? text !== needle : !text.toLowerCase().includes(needle)) {
|
|
1747
|
+
return false;
|
|
1748
|
+
}
|
|
1749
|
+
if (options.role && node2.role !== options.role) return false;
|
|
1750
|
+
if (options.within && !ancestorTestIDs.includes(options.within)) return false;
|
|
1751
|
+
const ax = node2.geometry.x;
|
|
1752
|
+
const ay = node2.geometry.y;
|
|
1753
|
+
if (options.minX != null && !(ax >= options.minX)) return false;
|
|
1754
|
+
if (options.maxX != null && !(ax <= options.maxX)) return false;
|
|
1755
|
+
if (options.minY != null && !(ay >= options.minY)) return false;
|
|
1756
|
+
if (options.maxY != null && !(ay <= options.maxY)) return false;
|
|
1757
|
+
return true;
|
|
1758
|
+
});
|
|
1759
|
+
if (options.near) {
|
|
1760
|
+
const { x, y } = options.near;
|
|
1761
|
+
candidates.sort(
|
|
1762
|
+
(a, b) => Math.hypot(a.node.geometry.x - x, a.node.geometry.y - y) - Math.hypot(b.node.geometry.x - x, b.node.geometry.y - y)
|
|
1763
|
+
);
|
|
1764
|
+
} else {
|
|
1765
|
+
candidates.sort((a, b) => {
|
|
1766
|
+
if (Math.abs(a.node.geometry.y - b.node.geometry.y) > 2) {
|
|
1767
|
+
return a.node.geometry.y - b.node.geometry.y;
|
|
1768
|
+
}
|
|
1769
|
+
return a.node.geometry.x - b.node.geometry.x;
|
|
1770
|
+
});
|
|
1771
|
+
}
|
|
1772
|
+
const total = candidates.length;
|
|
1773
|
+
if (total === 0) return { matched: 0, total: 0 };
|
|
1774
|
+
let idx = 0;
|
|
1775
|
+
if (options.nth != null) {
|
|
1776
|
+
idx = options.nth < 0 ? total + options.nth : options.nth;
|
|
1777
|
+
if (idx < 0 || idx >= total) {
|
|
1778
|
+
return { matched: 0, total, nthOutOfRange: true, nth: options.nth };
|
|
1779
|
+
}
|
|
1780
|
+
} else if (total > 1 && !options.first && !options.near) {
|
|
1781
|
+
return {
|
|
1782
|
+
ambiguous: true,
|
|
1783
|
+
total,
|
|
1784
|
+
candidates: candidates.slice(0, 10).map((candidate, index) => ({
|
|
1785
|
+
idx: index,
|
|
1786
|
+
nodeId: candidate.node.nodeId,
|
|
1787
|
+
type: candidate.node.type,
|
|
1788
|
+
testID: candidate.node.testID ?? null,
|
|
1789
|
+
text: nodeText(candidate.node).slice(0, 60),
|
|
1790
|
+
abs: { x: candidate.node.geometry.x, y: candidate.node.geometry.y },
|
|
1791
|
+
layout: {
|
|
1792
|
+
width: Math.round(candidate.node.geometry.width),
|
|
1793
|
+
height: Math.round(candidate.node.geometry.height)
|
|
1794
|
+
},
|
|
1795
|
+
ancestorTestIDs: candidate.ancestorTestIDs.slice(0, 5)
|
|
1796
|
+
}))
|
|
1797
|
+
};
|
|
1798
|
+
}
|
|
1799
|
+
const picked = candidates[idx];
|
|
1800
|
+
if (!picked) return { matched: 0, total };
|
|
1801
|
+
const node = picked.node;
|
|
1802
|
+
let resolved = node.testID ? await resolveTargetCoords(bridge, { mode: "testid", value: node.testID }) : await resolveTargetCoords(bridge, {
|
|
1803
|
+
mode: "text",
|
|
1804
|
+
value: nodeText(node) || query
|
|
1805
|
+
});
|
|
1806
|
+
if (!resolved) {
|
|
1807
|
+
resolved = {
|
|
1808
|
+
x: node.geometry.x + node.geometry.width / 2,
|
|
1809
|
+
y: node.geometry.y + node.geometry.height / 2,
|
|
1810
|
+
testID: node.testID ?? null,
|
|
1811
|
+
text: nodeText(node) || query,
|
|
1812
|
+
type: node.type
|
|
1813
|
+
};
|
|
1814
|
+
}
|
|
1815
|
+
return payloadFromResolved(resolved, node, { total, idx });
|
|
1816
|
+
}
|
|
1817
|
+
});
|
|
1961
1818
|
}
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1819
|
+
async function tapBest(bridge, query, opts = {}) {
|
|
1820
|
+
return tapResolvedTarget(bridge, {
|
|
1821
|
+
agent: opts.agent,
|
|
1822
|
+
timing: opts.timing,
|
|
1823
|
+
textFallback: query,
|
|
1824
|
+
resolve: async () => {
|
|
1825
|
+
const byId = await resolveTargetCoords(bridge, { mode: "testid", value: query });
|
|
1826
|
+
if (byId) {
|
|
1827
|
+
return {
|
|
1828
|
+
...payloadFromResolved(byId, byId),
|
|
1829
|
+
strategy: "testid",
|
|
1830
|
+
node: {
|
|
1831
|
+
nodeId: null,
|
|
1832
|
+
id: byId.testID ?? query,
|
|
1833
|
+
testID: byId.testID ?? query,
|
|
1834
|
+
type: byId.type ?? null,
|
|
1835
|
+
text: byId.text ?? null
|
|
1836
|
+
}
|
|
1837
|
+
};
|
|
1838
|
+
}
|
|
1839
|
+
const byText = await resolveTargetCoords(bridge, { mode: "text", value: query });
|
|
1840
|
+
if (byText) {
|
|
1841
|
+
return {
|
|
1842
|
+
...payloadFromResolved(byText, byText),
|
|
1843
|
+
strategy: "text",
|
|
1844
|
+
node: {
|
|
1845
|
+
nodeId: null,
|
|
1846
|
+
id: byText.testID ?? null,
|
|
1847
|
+
testID: byText.testID ?? null,
|
|
1848
|
+
type: byText.type ?? null,
|
|
1849
|
+
text: byText.text ?? query
|
|
1850
|
+
}
|
|
1851
|
+
};
|
|
1852
|
+
}
|
|
1853
|
+
return { strategy: "none" };
|
|
1854
|
+
}
|
|
1968
1855
|
});
|
|
1969
|
-
if (result === null) return null;
|
|
1970
|
-
if (typeof result !== "object") throw new Error("resolve returned an invalid result");
|
|
1971
|
-
const match = Reflect.get(result, "match");
|
|
1972
|
-
const resolvedTarget = Reflect.get(result, "target");
|
|
1973
|
-
const semanticNodes = [match, resolvedTarget];
|
|
1974
|
-
const point = Reflect.get(result, "point");
|
|
1975
|
-
const x = point !== null && typeof point === "object" ? Reflect.get(point, "x") : void 0;
|
|
1976
|
-
const y = point !== null && typeof point === "object" ? Reflect.get(point, "y") : void 0;
|
|
1977
|
-
if (!isSimSemanticTree(semanticNodes) || typeof x !== "number" || !Number.isFinite(x) || typeof y !== "number" || !Number.isFinite(y)) {
|
|
1978
|
-
throw new Error("resolve returned an invalid result");
|
|
1979
|
-
}
|
|
1980
|
-
const semanticMatch = semanticNodes[0];
|
|
1981
|
-
if (!semanticMatch) throw new Error("resolve returned an invalid result");
|
|
1982
|
-
return {
|
|
1983
|
-
x,
|
|
1984
|
-
y,
|
|
1985
|
-
testID: semanticMatch.testID ?? null,
|
|
1986
|
-
text: semanticMatch.text ?? (target.mode === "text" ? target.value : semanticMatch.label),
|
|
1987
|
-
type: semanticMatch.type
|
|
1988
|
-
};
|
|
1989
1856
|
}
|
|
1990
1857
|
|
|
1991
1858
|
// src/app-url.ts
|