peachsim 0.1.1625 → 0.1.1640
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +15 -2
- package/cli/app-config.ts +18 -6
- package/cli/app-fonts.ts +37 -17
- package/cli/app-stubs.ts +284 -20
- package/cli/bridge-flow-runner.ts +66 -45
- package/cli/cloud-client.ts +60 -40
- package/cli/command-registry.ts +1 -0
- package/cli/commands/control.ts +283 -49
- package/cli/commands/doctor.ts +30 -189
- package/cli/commands/flow.ts +79 -11
- package/cli/commands/inspect/core.ts +10 -5
- package/cli/commands/inspect/settling.ts +14 -2
- package/cli/commands/inspect/shared.ts +9 -4
- package/cli/commands/inspect/wait-ready.ts +12 -0
- package/cli/commands/inspect.ts +91 -50
- package/cli/commands/install-desktop.ts +1 -0
- package/cli/commands/maestro-compare.ts +97 -38
- package/cli/commands/maestro.ts +9 -1
- package/cli/commands/preview.ts +1 -1
- package/cli/commands/record.ts +118 -48
- package/cli/commands/screenshot.ts +23 -1
- package/cli/commands/serve.ts +7 -2
- package/cli/commands/setup.ts +19 -42
- package/cli/commands/swift.ts +14 -3
- package/cli/commands/upload.ts +93 -48
- package/cli/desktop-companion.ts +102 -17
- package/cli/drivers/desktop.ts +14 -1
- package/cli/drivers/playwright-sim-host.ts +103 -45
- package/cli/drivers/playwright.ts +9 -8
- package/cli/drivers/types.ts +5 -0
- package/cli/maestro-accessibility.ts +1 -1
- package/cli/maestro-compare.ts +84 -12
- package/cli/main.ts +3 -2
- package/cli/outbound-endpoints.ts +20 -6
- package/cli/pause-aware-timeout.ts +106 -0
- package/cli/scene-video.ts +89 -0
- package/cli/ws-bridge.ts +37 -19
- package/detox/attempt-events.cjs +66 -5
- package/detox/attempt-events.d.cts +7 -0
- package/detox/expectations.ts +7 -3
- package/detox/index.ts +269 -170
- package/detox/motion-change.cjs +206 -21
- package/detox/motion-change.d.cts +39 -2
- package/detox/navigation-lifecycle.ts +35 -3
- package/dist-lib/agent-daemon-client.cjs +1 -1
- package/dist-lib/agent-events.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 +8 -7
- package/dist-lib/beta.cjs +1 -1
- package/dist-lib/beta.mjs +1 -1
- package/dist-lib/bridge-constants.cjs +13 -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 +14 -8
- package/dist-lib/bridge-contract.mjs +13 -7
- package/dist-lib/bundle-channel.cjs +11887 -7900
- package/dist-lib/capture-contract.cjs +1 -1
- package/dist-lib/capture-contract.mjs +1 -1
- package/dist-lib/cloud-contract.cjs +1 -1
- package/dist-lib/cloud-contract.mjs +1 -1
- package/dist-lib/cloud-test-args.cjs +1 -1
- package/dist-lib/cloud-test-args.mjs +1 -1
- package/dist-lib/cloud.cjs +1 -1
- package/dist-lib/cloud.mjs +1 -1
- package/dist-lib/config.cjs +6 -3
- package/dist-lib/config.mjs +6 -3
- package/dist-lib/detox/index.cjs +1379 -725
- package/dist-lib/dev-bundle-resolution.cjs +14 -2
- package/dist-lib/expo-swift-bundle.cjs +9341 -5462
- package/dist-lib/expo-swift-metadata.cjs +11 -7
- package/dist-lib/expo-swift-metadata.mjs +11 -7
- package/dist-lib/home-paths.cjs +32 -3
- package/dist-lib/host/bridge-host.cjs +50204 -7258
- 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 +3541 -448
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/jump-to-source-native.cjs +1 -1
- package/dist-lib/kotlin-host.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 +2931 -104
- package/dist-lib/native-app-identity-project.cjs +289 -49
- package/dist-lib/native-app-identity-project.mjs +289 -49
- package/dist-lib/native-app-identity.cjs +77 -26
- package/dist-lib/native-app-identity.mjs +76 -26
- package/dist-lib/native-firebase-metadata.cjs +1 -1
- package/dist-lib/native-firebase-metadata.mjs +1 -1
- package/dist-lib/plist.cjs +1 -1
- package/dist-lib/plist.mjs +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 +6 -2
- package/dist-lib/react-native-host-modules.mjs +6 -2
- package/dist-lib/scripts/dev-server-scanner.cjs +22 -8
- package/dist-lib/sdk.cjs +27 -11
- package/dist-lib/sdk.mjs +27 -10
- package/dist-lib/skills.cjs +72201 -26208
- package/dist-lib/swift-compile.cjs +1007 -140
- package/dist-lib/swift-compile.mjs +1049 -174
- package/dist-lib/swift-package.cjs +12 -3
- package/dist-lib/swift-package.mjs +12 -3
- package/dist-lib/swift-submit.cjs +13 -5
- package/dist-lib/swift-submit.mjs +11 -5
- package/dist-lib/swift-toolchain.cjs +423 -33
- package/dist-lib/swift.cjs +1 -1
- package/dist-lib/vite-swift.cjs +8628 -5268
- package/dist-lib/vite.cjs +2859 -12
- package/dist-lib/worklet-executable.cjs +14 -2
- package/dist-lib/worklet-executable.mjs +14 -2
- package/kotlin/AppSources/dev/peach/app/PeachDrawable.kt +21 -14
- package/kotlin/Sources/androidx/annotation/DrawableRes.kt +14 -0
- package/package.json +1 -1
- package/scripts/dev-server-scanner.ts +2 -1
- package/skills/peach-perf/SKILL.md +11 -5
- package/skills/peach-setup/SKILL.md +16 -19
- package/src/backend-origin.ts +13 -6
- package/src/bridge-constants.ts +11 -0
- package/src/bridge-contract.ts +22 -34
- package/src/config.ts +15 -0
- package/src/desktop-host-delivery.ts +5 -1
- package/src/dev-bundle-resolution.ts +2 -1
- package/src/expo-manifest-body.ts +13 -0
- package/src/expo-swift-host.ts +196 -49
- package/src/expo-swift-ready.ts +78 -0
- package/src/expoKotlinContract.ts +4 -7
- package/src/expoSwiftContract.ts +17 -6
- package/src/home-paths.ts +48 -2
- package/src/host/bridge-host.ts +5 -2
- package/src/host/staged-host-files.ts +9 -0
- package/src/native-app-identity-project.ts +27 -6
- package/src/native-app-identity.ts +91 -4
- package/src/native-firebase-config.ts +209 -13
- package/src/nativeAppIconContract.ts +1 -4
- package/src/output-lock-stdio.ts +36 -0
- package/src/process-group.ts +25 -2
- package/src/react-native-bridge-modules.ts +431 -0
- package/src/react-native-host-modules.ts +8 -1
- package/src/settings-bundle.ts +1 -4
- package/src/swift-binary-modules.ts +2 -2
- package/src/swift-compile.ts +918 -109
- package/src/swift-compiler-digest.ts +14 -0
- package/src/swift-distribution.lock.json +24 -12
- package/src/swift-distribution.ts +103 -4
- package/src/swift-host.ts +180 -55
- package/src/swift-icu.ts +27 -13
- package/src/swift-objc-compile.ts +355 -0
- package/src/swift-objc-sdk.ts +821 -27
- package/src/swift-objc-standin.ts +148 -14
- package/src/swift-package-manifest.ts +60 -7
- package/src/swift-package.ts +15 -1
- package/src/swift-packages.ts +154 -81
- package/src/swift-standin-emit.ts +96 -15
- package/src/swift-standins.ts +530 -116
- package/src/swift-submit.ts +13 -5
- package/src/swift-toolchain.ts +500 -42
- package/src/vite-plugin.ts +12 -1
- package/src/worker-system-chunks.ts +11 -12
- package/src/worklet-executable.ts +19 -1
- package/src/xcode-project.ts +270 -30
package/cli/commands/inspect.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// peach inspect — dump node tree, accessibility info, query nodes
|
|
2
2
|
// connects to the peach WS bridge (no Playwright)
|
|
3
3
|
|
|
4
|
+
import { Script } from 'node:vm'
|
|
4
5
|
import { getCliInspectVerbMeta } from '@peach/skills/cli'
|
|
5
6
|
import { renderCliCommandHelp, renderCliGroupHelp } from '@peach/skills/cli/renderers'
|
|
6
7
|
import { DEFAULT_SOOTSIM_BRIDGE_PORT } from '../../src/bridge-constants'
|
|
@@ -34,6 +35,7 @@ import {
|
|
|
34
35
|
parseBridgeCliArgs,
|
|
35
36
|
printBridgeWorldNotice,
|
|
36
37
|
type WsBridge,
|
|
38
|
+
type BridgeSimInfo,
|
|
37
39
|
} from '../ws-bridge'
|
|
38
40
|
import {
|
|
39
41
|
isTapSuccess,
|
|
@@ -193,6 +195,8 @@ async function performSingleStep(
|
|
|
193
195
|
}
|
|
194
196
|
}
|
|
195
197
|
|
|
198
|
+
// runs after a command already spent its whole budget, so an explicit --timeout
|
|
199
|
+
// must not make the liveness check wait that long a second time
|
|
196
200
|
async function probeSimResponsive(bridge: WsBridge): Promise<boolean> {
|
|
197
201
|
try {
|
|
198
202
|
await bridge.send({ type: 'evaluate', code: '1' }, { timeoutMs: 3000 })
|
|
@@ -209,6 +213,18 @@ async function reportMissingTarget(
|
|
|
209
213
|
verb: 'tap' | 'double-tap' | 'long-press',
|
|
210
214
|
target: { mode: 'testid' | 'text'; value: string },
|
|
211
215
|
): Promise<void> {
|
|
216
|
+
if (target.mode === 'testid') {
|
|
217
|
+
// the tree can hold the id while the engine has no point to press: the node
|
|
218
|
+
// is not painted yet, or was replaced since the query. say so, so the
|
|
219
|
+
// suggestion list never names the id that was just reported missing.
|
|
220
|
+
const exact = await inspectFind(bridge, { testId: target.value }).catch(() => null)
|
|
221
|
+
if (exact?.result) {
|
|
222
|
+
console.error(
|
|
223
|
+
` found ${target.value} in the tree but it has no tap target yet; it may be unpainted, hidden or covered: wait for it with peach wait selector ${target.value}`,
|
|
224
|
+
)
|
|
225
|
+
return
|
|
226
|
+
}
|
|
227
|
+
}
|
|
212
228
|
console.error(` not found: ${target.value}`)
|
|
213
229
|
if (target.mode !== 'testid') return
|
|
214
230
|
const byText = await inspectFind(bridge, { text: target.value }).catch(() => null)
|
|
@@ -596,13 +612,13 @@ export function printShellPerfReport(result: Record<string, any>) {
|
|
|
596
612
|
console.log(``)
|
|
597
613
|
console.log(` cadence (frame delivery, ${cadence.vsyncTicks} vsync ticks):`)
|
|
598
614
|
console.log(
|
|
599
|
-
` display clock: ${cadence.displayHz}hz · host rAF interval p50 ${cadence.hostIntervalP50} / p95 ${cadence.hostIntervalP95} / max ${cadence.hostIntervalMax} ms${cadence.hostGaps ? ` · ${cadence.hostGaps} gaps >1.5x (host rAF starved)` : ''}`,
|
|
615
|
+
` display clock: ${cadence.displayHz}hz · host rAF interval p50 ${cadence.hostIntervalP50} / p95 ${cadence.hostIntervalP95} / max ${cadence.hostIntervalMax} ms${cadence.hostGaps ? ` · ${cadence.hostGaps} gaps >1.5x (host rAF starved)` : ''}${cadence.idleBreaks ? ` · ${cadence.idleBreaks} host idle breaks excluded` : ''}`,
|
|
600
616
|
)
|
|
601
617
|
console.log(
|
|
602
618
|
` delivery lag: p50 ${cadence.deliveryLagP50} / p95 ${cadence.deliveryLagP95} / max ${cadence.deliveryLagMax} ms (vsync postMessage → shell receipt)`,
|
|
603
619
|
)
|
|
604
620
|
console.log(
|
|
605
|
-
`
|
|
621
|
+
` present interval: p50 ${cadence.paintIntervalP50} / p95 ${cadence.paintIntervalP95} / max ${cadence.paintIntervalMax} ms (during motion, ${cadence.paintIntervals} intervals, including native frames)`,
|
|
606
622
|
)
|
|
607
623
|
}
|
|
608
624
|
const compositorCadence = result.compositorCadence
|
|
@@ -857,11 +873,10 @@ async function waitForKeyboardVisible(
|
|
|
857
873
|
}
|
|
858
874
|
|
|
859
875
|
/**
|
|
860
|
-
*
|
|
861
|
-
*
|
|
862
|
-
* whatever holds focus and fails the batch when nothing does.
|
|
876
|
+
* dispatch through the shared input executor, which checks the requested
|
|
877
|
+
* node identity before targeted typing and carries refusals out of the CLI.
|
|
863
878
|
*/
|
|
864
|
-
async function
|
|
879
|
+
async function performKeys(
|
|
865
880
|
bridge: WsBridge,
|
|
866
881
|
steps: Array<Extract<PerformStep, { type: 'type' | 'typeInto' | 'key' }>>,
|
|
867
882
|
actionLabel: string,
|
|
@@ -1247,7 +1262,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
|
|
|
1247
1262
|
return
|
|
1248
1263
|
}
|
|
1249
1264
|
|
|
1250
|
-
//
|
|
1265
|
+
// driver discovery works offline; a live bridge also reports each running renderer.
|
|
1251
1266
|
// short-circuit here so users can discover drivers even when no peach
|
|
1252
1267
|
// session is running.
|
|
1253
1268
|
if (
|
|
@@ -1256,6 +1271,19 @@ export async function runInspect(args: string[], opts: InspectOptions) {
|
|
|
1256
1271
|
) {
|
|
1257
1272
|
const { buildDriverListRows } = await import('../drivers')
|
|
1258
1273
|
const rows = buildDriverListRows()
|
|
1274
|
+
const bridge = (opts.createBridge ?? createBridgeFromParsed)(parsed)
|
|
1275
|
+
let sims: BridgeSimInfo[] = []
|
|
1276
|
+
try {
|
|
1277
|
+
sims = await bridge.listSims()
|
|
1278
|
+
} catch {
|
|
1279
|
+
// local availability remains useful when no bridge is running.
|
|
1280
|
+
} finally {
|
|
1281
|
+
bridge.close()
|
|
1282
|
+
}
|
|
1283
|
+
if (wantsJson(effectiveArgs)) {
|
|
1284
|
+
printJson({ drivers: rows, sims })
|
|
1285
|
+
return
|
|
1286
|
+
}
|
|
1259
1287
|
console.log(` available drivers (${rows.length}):\n`)
|
|
1260
1288
|
const idWidth = Math.max(...rows.map((r) => r.id.length), 6)
|
|
1261
1289
|
const kindWidth = Math.max(...rows.map((r) => r.kind.length), 4)
|
|
@@ -1270,6 +1298,15 @@ export async function runInspect(args: string[], opts: InspectOptions) {
|
|
|
1270
1298
|
console.log(` unavailable: ${row.reason}`)
|
|
1271
1299
|
}
|
|
1272
1300
|
}
|
|
1301
|
+
for (const sim of sims) {
|
|
1302
|
+
const renderer = sim.meta?.peachRenderer
|
|
1303
|
+
const executable = sim.meta?.peachHostExecutable
|
|
1304
|
+
if (sim.readyState === 'open' && typeof renderer === 'string') {
|
|
1305
|
+
console.log(
|
|
1306
|
+
` in use: ${sim.id} · ${String(sim.meta?.sootsimHostDriver)} · ${renderer}${typeof executable === 'string' ? ` @ ${executable}` : ''}`,
|
|
1307
|
+
)
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1273
1310
|
return
|
|
1274
1311
|
}
|
|
1275
1312
|
|
|
@@ -2439,7 +2476,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
|
|
|
2439
2476
|
console.error(' type-into requires a resolved text input node identity')
|
|
2440
2477
|
peachExit(1)
|
|
2441
2478
|
}
|
|
2442
|
-
const typed = await
|
|
2479
|
+
const typed = await performKeys(
|
|
2443
2480
|
bridge,
|
|
2444
2481
|
[{ type: 'typeInto', text, nodeId }],
|
|
2445
2482
|
'type-into',
|
|
@@ -2477,6 +2514,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
|
|
|
2477
2514
|
return {
|
|
2478
2515
|
cx: n.absolutePosition.x + (n.layout.width || 0) / 2,
|
|
2479
2516
|
cy: n.absolutePosition.y + (n.layout.height || 0) / 2,
|
|
2517
|
+
nodeId: n.nodeId,
|
|
2480
2518
|
id: n.id,
|
|
2481
2519
|
testID: n.testID,
|
|
2482
2520
|
type: n.type,
|
|
@@ -2491,7 +2529,13 @@ export async function runInspect(args: string[], opts: InspectOptions) {
|
|
|
2491
2529
|
peachExit(1)
|
|
2492
2530
|
}
|
|
2493
2531
|
if (!tiPayload.isTextInput) {
|
|
2494
|
-
console.error(`
|
|
2532
|
+
console.error(` ${targetId} is not a text input (isTextInput: false)`)
|
|
2533
|
+
peachExit(1)
|
|
2534
|
+
}
|
|
2535
|
+
const nodeId: unknown = tiPayload.nodeId
|
|
2536
|
+
if (typeof nodeId !== 'number' || !Number.isSafeInteger(nodeId) || nodeId === 0) {
|
|
2537
|
+
console.error(' type-into requires a resolved text input node identity')
|
|
2538
|
+
peachExit(1)
|
|
2495
2539
|
}
|
|
2496
2540
|
// step 2: tap to focus
|
|
2497
2541
|
const tapResult = await bridge.send({
|
|
@@ -2499,12 +2543,17 @@ export async function runInspect(args: string[], opts: InspectOptions) {
|
|
|
2499
2543
|
x: tiPayload.cx,
|
|
2500
2544
|
y: tiPayload.cy,
|
|
2501
2545
|
target: {
|
|
2546
|
+
nodeId,
|
|
2502
2547
|
id: tiPayload.id ?? targetId,
|
|
2503
2548
|
testID: tiPayload.testID ?? targetId,
|
|
2504
2549
|
text: null,
|
|
2505
2550
|
type: tiPayload.type ?? null,
|
|
2506
2551
|
},
|
|
2507
2552
|
})
|
|
2553
|
+
if (!isTapSuccess(tapResult)) {
|
|
2554
|
+
console.error(` type-into could not tap ${targetId}`)
|
|
2555
|
+
peachExit(1)
|
|
2556
|
+
}
|
|
2508
2557
|
const targetSecureTextEntry = tiPayload.secureTextEntry === true
|
|
2509
2558
|
const keyboardState = await waitForKeyboardVisible(bridge, 1000, {
|
|
2510
2559
|
targetId,
|
|
@@ -2535,38 +2584,26 @@ export async function runInspect(args: string[], opts: InspectOptions) {
|
|
|
2535
2584
|
peachExit(1)
|
|
2536
2585
|
}
|
|
2537
2586
|
}
|
|
2538
|
-
//
|
|
2539
|
-
//
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
peachExit(1)
|
|
2553
|
-
}
|
|
2554
|
-
return count
|
|
2555
|
-
}
|
|
2556
|
-
const existingCharacters = await drawnCharacters()
|
|
2557
|
-
for (let i = 0; i < existingCharacters; i++) {
|
|
2558
|
-
await bridge.send({ type: 'keyboard', action: 'press', text: 'delete' })
|
|
2559
|
-
}
|
|
2560
|
-
// backspace clears only what precedes the caret the tap placed
|
|
2561
|
-
if (existingCharacters > 0 && (await drawnCharacters()) > 0) {
|
|
2562
|
-
console.error(
|
|
2563
|
-
` type-into could not clear ${targetId}: text remains after the caret the tap placed`,
|
|
2564
|
-
)
|
|
2587
|
+
// type at the live selection through the same guarded operation as
|
|
2588
|
+
// chained and cloud typing; clearing first changes controlled values.
|
|
2589
|
+
const typed = await performKeys(
|
|
2590
|
+
bridge,
|
|
2591
|
+
[{ type: 'typeInto', text, nodeId }],
|
|
2592
|
+
'type-into',
|
|
2593
|
+
)
|
|
2594
|
+
const receipt: unknown = typed.steps[0]?.value
|
|
2595
|
+
if (
|
|
2596
|
+
!receipt ||
|
|
2597
|
+
typeof receipt !== 'object' ||
|
|
2598
|
+
typeof Reflect.get(receipt, 'secureTextEntry') !== 'boolean'
|
|
2599
|
+
) {
|
|
2600
|
+
console.error(' type-into did not return its matched input secure state')
|
|
2565
2601
|
peachExit(1)
|
|
2566
2602
|
}
|
|
2567
|
-
await bridge.send({ type: 'keyboard', action: 'type', text })
|
|
2568
2603
|
const secureTextEntry =
|
|
2569
|
-
targetSecureTextEntry ||
|
|
2604
|
+
targetSecureTextEntry ||
|
|
2605
|
+
Reflect.get(receipt, 'secureTextEntry') === true ||
|
|
2606
|
+
isSecureKeyboardState(keyboardState as any)
|
|
2570
2607
|
const outputText = secureTextEntry
|
|
2571
2608
|
? SECURE_TEXT_REDACTION
|
|
2572
2609
|
: redactTextForKeyboardState(text, keyboardState as any)
|
|
@@ -2606,7 +2643,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
|
|
|
2606
2643
|
peachExit(1)
|
|
2607
2644
|
}
|
|
2608
2645
|
if (bridge.plane === 'cloud') {
|
|
2609
|
-
await
|
|
2646
|
+
await performKeys(bridge, [{ type: 'type', text }], 'type')
|
|
2610
2647
|
await recordInspectAction(
|
|
2611
2648
|
'inspect type',
|
|
2612
2649
|
{ inputText: text },
|
|
@@ -2640,7 +2677,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
|
|
|
2640
2677
|
peachExit(1)
|
|
2641
2678
|
}
|
|
2642
2679
|
if (bridge.plane === 'cloud') {
|
|
2643
|
-
await
|
|
2680
|
+
await performKeys(bridge, [{ type: 'key', key: name }], 'key')
|
|
2644
2681
|
} else {
|
|
2645
2682
|
await requireVisualKeyboard(bridge, 'key')
|
|
2646
2683
|
await bridge.send({ type: 'keyboard', action: 'press', text: name })
|
|
@@ -2657,7 +2694,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
|
|
|
2657
2694
|
peachExit(1)
|
|
2658
2695
|
}
|
|
2659
2696
|
if (bridge.plane === 'cloud') {
|
|
2660
|
-
await
|
|
2697
|
+
await performKeys(
|
|
2661
2698
|
bridge,
|
|
2662
2699
|
keys.map((key) => ({ type: 'key' as const, key })),
|
|
2663
2700
|
'key-sequence',
|
|
@@ -3694,13 +3731,17 @@ export async function runInspect(args: string[], opts: InspectOptions) {
|
|
|
3694
3731
|
// wrap in async to support await in worker mode. no identifier
|
|
3695
3732
|
// rewriting — the snippet runs verbatim so tokens like `root`,
|
|
3696
3733
|
// `debug`, `test` refer to whatever the user actually typed.
|
|
3697
|
-
//
|
|
3698
|
-
//
|
|
3734
|
+
// compile without executing to distinguish expressions from statements;
|
|
3735
|
+
// punctuation inside callbacks, literals and comments is not a separator.
|
|
3699
3736
|
let expandedCode = code
|
|
3700
3737
|
if (!expandedCode.startsWith('(async')) {
|
|
3701
|
-
expandedCode =
|
|
3702
|
-
|
|
3703
|
-
|
|
3738
|
+
expandedCode = `(async () => (\n${code}\n))()`
|
|
3739
|
+
try {
|
|
3740
|
+
new Script(expandedCode)
|
|
3741
|
+
} catch (error) {
|
|
3742
|
+
if (!(error instanceof SyntaxError)) throw error
|
|
3743
|
+
expandedCode = `(async () => {\n${code}\n})()`
|
|
3744
|
+
}
|
|
3704
3745
|
}
|
|
3705
3746
|
const result = await bridge.send({ type: 'evaluate', code: expandedCode })
|
|
3706
3747
|
printJson(result)
|
|
@@ -3936,11 +3977,11 @@ export async function runInspect(args: string[], opts: InspectOptions) {
|
|
|
3936
3977
|
case 'start': {
|
|
3937
3978
|
const result = await bridge.send({
|
|
3938
3979
|
type: 'evaluate',
|
|
3939
|
-
code: `(() => {
|
|
3980
|
+
code: `(async () => {
|
|
3940
3981
|
if (!window.__sootsimShellPerf) {
|
|
3941
3982
|
return { error: 'shell frame profile unavailable (__sootsimShellPerf missing on the page)' }
|
|
3942
3983
|
}
|
|
3943
|
-
window.__sootsimShellPerf.start()
|
|
3984
|
+
await window.__sootsimShellPerf.start()
|
|
3944
3985
|
return { started: true }
|
|
3945
3986
|
})()`,
|
|
3946
3987
|
})
|
|
@@ -4015,7 +4056,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
|
|
|
4015
4056
|
if (!window.__sootsimShellPerf) {
|
|
4016
4057
|
return { error: 'shell frame profile unavailable (__sootsimShellPerf missing on the page)' }
|
|
4017
4058
|
}
|
|
4018
|
-
window.__sootsimShellPerf.start()
|
|
4059
|
+
await window.__sootsimShellPerf.start()
|
|
4019
4060
|
// give a frame for profiling to engage before the event
|
|
4020
4061
|
await new Promise(r => requestAnimationFrame(() => r(undefined)))
|
|
4021
4062
|
window.dispatchEvent(new Event('${eventName}'))
|
|
@@ -4603,7 +4644,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
|
|
|
4603
4644
|
// re-incurs the full per-command timeout. three sequential probes turn a
|
|
4604
4645
|
// single 15s timeout into ~60s of apparent hang (QA F19-1). there is
|
|
4605
4646
|
// nothing useful to fetch from a sim that just timed out, so skip them.
|
|
4606
|
-
const timedOut = /^command timed out after (\d+)s$/.exec(message)
|
|
4647
|
+
const timedOut = /^command timed out after (\d+(?:\.\d+)?)s$/.exec(message)
|
|
4607
4648
|
// a disconnected bridge is genuinely gone; recover is right and probing
|
|
4608
4649
|
// it would just re-incur a full timeout. no bridge at all means there is
|
|
4609
4650
|
// no sim to recover, only one to open.
|
|
@@ -126,6 +126,7 @@ async function installLinuxDeb(packagePath: string): Promise<string> {
|
|
|
126
126
|
async function installWindowsExe(exePath: string): Promise<string> {
|
|
127
127
|
// nsis installer prompts and self-elevates; run it and let it take over.
|
|
128
128
|
await new Promise<void>((resolvePromise, rejectPromise) => {
|
|
129
|
+
// runner-toolchain-platform: win32
|
|
129
130
|
const child = spawn('cmd', ['/c', 'start', '""', '/wait', exePath], {
|
|
130
131
|
stdio: 'inherit',
|
|
131
132
|
})
|
|
@@ -8,6 +8,7 @@ import * as fs from 'fs'
|
|
|
8
8
|
import * as os from 'os'
|
|
9
9
|
import * as path from 'path'
|
|
10
10
|
import { devices } from 'peach-engine/settings/devices'
|
|
11
|
+
import { PNG } from 'pngjs'
|
|
11
12
|
import { rnxHomeDir } from '../../src/home-paths'
|
|
12
13
|
import { parseFlowFile } from '../flow-file'
|
|
13
14
|
import {
|
|
@@ -17,7 +18,8 @@ import {
|
|
|
17
18
|
comparePassed,
|
|
18
19
|
instrumentFlowSource,
|
|
19
20
|
nativeStepsFromCommandsJson,
|
|
20
|
-
|
|
21
|
+
nativeFlowVerdictsFromJunit,
|
|
22
|
+
junitFlowKey,
|
|
21
23
|
peachStepsFromCommandsJson,
|
|
22
24
|
screensOverThreshold,
|
|
23
25
|
type CompareCapture,
|
|
@@ -161,12 +163,44 @@ function statusBarPx(png: Buffer): number {
|
|
|
161
163
|
const height = png.readUInt32BE(20)
|
|
162
164
|
const spec = Object.values(devices).find(
|
|
163
165
|
(d) =>
|
|
164
|
-
|
|
165
|
-
Math.round(d.
|
|
166
|
+
// maestro can truncate fractional point bounds one pixel below simctl.
|
|
167
|
+
Math.abs(Math.round(d.width * d.scale) - width) <= 1 &&
|
|
168
|
+
Math.abs(Math.round(d.height * d.scale) - height) <= 1,
|
|
166
169
|
)
|
|
167
170
|
return spec ? spec.statusBarHeight * spec.scale : 0
|
|
168
171
|
}
|
|
169
172
|
|
|
173
|
+
// maestro's xctest shot on an ios 27 simulator drops the rightmost column of
|
|
174
|
+
// an odd framebuffer (1179 becomes 1178). diffing at that short width
|
|
175
|
+
// resamples peach off the pixel grid, so restore the column first.
|
|
176
|
+
function alignNativeCapture(png: Buffer): Buffer {
|
|
177
|
+
const width = png.readUInt32BE(16)
|
|
178
|
+
const height = png.readUInt32BE(20)
|
|
179
|
+
const spec = Object.values(devices).find(
|
|
180
|
+
(d) =>
|
|
181
|
+
Math.abs(Math.round(d.width * d.scale) - width) <= 1 &&
|
|
182
|
+
Math.abs(Math.round(d.height * d.scale) - height) <= 1,
|
|
183
|
+
)
|
|
184
|
+
if (!spec) return png
|
|
185
|
+
const expectedW = Math.round(spec.width * spec.scale)
|
|
186
|
+
const expectedH = Math.round(spec.height * spec.scale)
|
|
187
|
+
if (expectedH !== height || expectedW !== width + 1) return png
|
|
188
|
+
const src = PNG.sync.read(png)
|
|
189
|
+
const dst = new PNG({ width: expectedW, height })
|
|
190
|
+
for (let y = 0; y < height; y++) {
|
|
191
|
+
const srow = y * src.width * 4
|
|
192
|
+
const drow = y * expectedW * 4
|
|
193
|
+
src.data.copy(dst.data, drow, srow, srow + src.width * 4)
|
|
194
|
+
const last = srow + (src.width - 1) * 4
|
|
195
|
+
const o = drow + src.width * 4
|
|
196
|
+
dst.data[o] = src.data[last] ?? 0
|
|
197
|
+
dst.data[o + 1] = src.data[last + 1] ?? 0
|
|
198
|
+
dst.data[o + 2] = src.data[last + 2] ?? 0
|
|
199
|
+
dst.data[o + 3] = 255
|
|
200
|
+
}
|
|
201
|
+
return PNG.sync.write(dst)
|
|
202
|
+
}
|
|
203
|
+
|
|
170
204
|
// the flow as its author would name it: relative inside the working
|
|
171
205
|
// directory, home-relative or absolute outside it.
|
|
172
206
|
function displayPath(cwd: string, file: string): string {
|
|
@@ -201,22 +235,6 @@ function formatFailedRequest(entry: unknown): string {
|
|
|
201
235
|
return `request failed: ${e.method ?? 'GET'} ${e.url ?? '?'} ${e.status ?? e.error ?? ''}`.trim()
|
|
202
236
|
}
|
|
203
237
|
|
|
204
|
-
// peach names each flow's output dir after the file stem, suffixing repeats
|
|
205
|
-
// in run order (commands/maestro.ts uniqueFlowDir).
|
|
206
|
-
function peachFlowDirs(root: string, files: string[]): string[] {
|
|
207
|
-
const seen = new Map<string, number>()
|
|
208
|
-
return files.map((file) => {
|
|
209
|
-
const slug =
|
|
210
|
-
path
|
|
211
|
-
.basename(file)
|
|
212
|
-
.replace(/\.(ya?ml)$/i, '')
|
|
213
|
-
.replace(/[^A-Za-z0-9._-]+/g, '_') || 'flow'
|
|
214
|
-
const n = (seen.get(slug) ?? 0) + 1
|
|
215
|
-
seen.set(slug, n)
|
|
216
|
-
return path.join(root, n === 1 ? slug : `${slug}-${n}`)
|
|
217
|
-
})
|
|
218
|
-
}
|
|
219
|
-
|
|
220
238
|
// the flow's appId as maestro resolves it: its `${...}` templates evaluate
|
|
221
239
|
// against --env values (in process.env) and the flow's own env block.
|
|
222
240
|
function resolveFlowAppId(frontmatter: Record<string, unknown>): string | undefined {
|
|
@@ -424,6 +442,10 @@ export async function runMaestroCompare(
|
|
|
424
442
|
'--test-output-dir',
|
|
425
443
|
nativeOut,
|
|
426
444
|
'--no-ansi',
|
|
445
|
+
'--format',
|
|
446
|
+
'junit',
|
|
447
|
+
'--output',
|
|
448
|
+
path.join(out, 'ios-report.xml'),
|
|
427
449
|
...shared,
|
|
428
450
|
...envPairs,
|
|
429
451
|
],
|
|
@@ -433,18 +455,23 @@ export async function runMaestroCompare(
|
|
|
433
455
|
spawnSync('xcrun', ['simctl', 'status_bar', udid, 'clear'], { stdio: 'ignore' })
|
|
434
456
|
}
|
|
435
457
|
console.log(`\n ios │ maestro exited ${nativeExit}\n`)
|
|
436
|
-
const
|
|
458
|
+
const nativeVerdicts = nativeFlowVerdictsFromJunit(
|
|
459
|
+
fs.readFileSync(path.join(out, 'ios-report.xml'), 'utf8'),
|
|
460
|
+
)
|
|
437
461
|
|
|
438
462
|
// 2. peach, same instrumented flows (its --env already applied above)
|
|
439
463
|
const peachOut = path.join(out, 'peach')
|
|
440
|
-
const peachResults = new Map<
|
|
464
|
+
const peachResults = new Map<
|
|
465
|
+
string,
|
|
466
|
+
{ exit: number; playbackFailure: string; outputDir: string | null }
|
|
467
|
+
>()
|
|
441
468
|
await runMaestro(
|
|
442
469
|
['test', runTarget, '--test-output-dir', peachOut, ...shared, ...peachArgs],
|
|
443
470
|
{
|
|
444
471
|
...opts,
|
|
445
472
|
captureConsole: true,
|
|
446
|
-
onFlowResult: ({ file, exit, playbackFailure }) => {
|
|
447
|
-
peachResults.set(path.resolve(file), { exit, playbackFailure })
|
|
473
|
+
onFlowResult: ({ file, exit, playbackFailure, outputDir }) => {
|
|
474
|
+
peachResults.set(path.resolve(file), { exit, playbackFailure, outputDir })
|
|
448
475
|
},
|
|
449
476
|
},
|
|
450
477
|
)
|
|
@@ -458,26 +485,56 @@ export async function runMaestroCompare(
|
|
|
458
485
|
.sort()
|
|
459
486
|
.pop()
|
|
460
487
|
: undefined
|
|
461
|
-
const
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
488
|
+
const nativeOutputs = new Map<string, Array<{ dir: string; commands: unknown }>>()
|
|
489
|
+
for (const entry of nativeRun
|
|
490
|
+
? fs.readdirSync(nativeRun, { withFileTypes: true })
|
|
491
|
+
: []) {
|
|
492
|
+
if (!entry.isDirectory()) continue
|
|
493
|
+
const dir = path.join(nativeRun!, entry.name)
|
|
494
|
+
const commands = readJson(path.join(dir, 'commands.json'))
|
|
495
|
+
if (!Array.isArray(commands)) continue
|
|
496
|
+
// display names are not paths: maestro replaces slashes and suffixes collisions.
|
|
497
|
+
// the top-level bookkeeping retains the original flow identity.
|
|
498
|
+
let name: string | undefined
|
|
499
|
+
let fileStem: string | undefined
|
|
500
|
+
for (const command of commands) {
|
|
501
|
+
if (command?.metadata?.depth !== 0) continue
|
|
502
|
+
const config = command?.command?.applyConfigurationCommand?.config
|
|
503
|
+
if (typeof config?.name === 'string') name ??= config.name
|
|
504
|
+
const env = command?.command?.defineVariablesCommand?.env
|
|
505
|
+
if (typeof env?.MAESTRO_FILENAME === 'string') fileStem ??= env.MAESTRO_FILENAME
|
|
506
|
+
}
|
|
507
|
+
if (fileStem === undefined) continue
|
|
508
|
+
const key = JSON.stringify([name ?? fileStem, fileStem])
|
|
509
|
+
const outputs = nativeOutputs.get(key) ?? []
|
|
510
|
+
outputs.push({ dir, commands })
|
|
511
|
+
nativeOutputs.set(key, outputs)
|
|
512
|
+
}
|
|
465
513
|
const imagesDir = path.join(out, 'images')
|
|
466
514
|
const flows: CompareFlow[] = []
|
|
467
515
|
for (const [i, flow] of instrumented.entries()) {
|
|
468
|
-
const
|
|
469
|
-
const
|
|
470
|
-
const
|
|
516
|
+
const fileStem = path.basename(flow.copy).replace(/\.(ya?ml)$/i, '')
|
|
517
|
+
const nativeOutput = nativeOutputs.get(JSON.stringify([flow.name, fileStem]))?.shift()
|
|
518
|
+
const nativeDir = nativeOutput?.dir
|
|
519
|
+
const nativeRan = Boolean(nativeOutput)
|
|
471
520
|
const peachResult = peachResults.get(path.resolve(flow.copy))
|
|
472
|
-
const
|
|
473
|
-
const
|
|
521
|
+
const peachDir = peachResult?.outputDir
|
|
522
|
+
const verdicts = nativeVerdicts.get(junitFlowKey(flow.name, fileStem))
|
|
523
|
+
const nativeFailure = verdicts?.length
|
|
524
|
+
? verdicts.shift()
|
|
525
|
+
: 'no native verdict in ios-report.xml'
|
|
526
|
+
const peachRan = Boolean(
|
|
527
|
+
peachDir && fs.existsSync(path.join(peachDir, 'commands.json')),
|
|
528
|
+
)
|
|
474
529
|
const nativeSteps = nativeRan
|
|
475
|
-
? nativeStepsFromCommandsJson(
|
|
530
|
+
? nativeStepsFromCommandsJson(nativeOutput?.commands)
|
|
476
531
|
: []
|
|
477
532
|
const peachSteps = peachRan
|
|
478
|
-
? peachStepsFromCommandsJson(readJson(path.join(peachDir
|
|
533
|
+
? peachStepsFromCommandsJson(readJson(path.join(peachDir!, 'commands.json')))
|
|
479
534
|
: []
|
|
480
|
-
const consoleJson =
|
|
535
|
+
const consoleJson = (
|
|
536
|
+
peachDir ? readJson(path.join(peachDir, 'logs', 'console.json')) : null
|
|
537
|
+
) as {
|
|
481
538
|
errors?: unknown[]
|
|
482
539
|
requests?: unknown[]
|
|
483
540
|
} | null
|
|
@@ -492,15 +549,17 @@ export async function runMaestroCompare(
|
|
|
492
549
|
const nativePng = nativeDir
|
|
493
550
|
? path.join(nativeDir, 'takeScreenshot', `${capture.name}.png`)
|
|
494
551
|
: ''
|
|
495
|
-
const peachPng =
|
|
552
|
+
const peachPng = peachDir
|
|
553
|
+
? path.join(peachDir, 'screenshots', `${capture.name}.png`)
|
|
554
|
+
: ''
|
|
496
555
|
const hasNative = Boolean(nativePng) && fs.existsSync(nativePng)
|
|
497
|
-
const hasPeach = fs.existsSync(peachPng)
|
|
556
|
+
const hasPeach = Boolean(peachPng) && fs.existsSync(peachPng)
|
|
498
557
|
const imageRel = path.join(
|
|
499
558
|
'images',
|
|
500
559
|
`${i + 1}-${capture.name.split('/').pop()}.png`,
|
|
501
560
|
)
|
|
502
561
|
if (hasNative && hasPeach) {
|
|
503
|
-
const nativeBytes = fs.readFileSync(nativePng)
|
|
562
|
+
const nativeBytes = alignNativeCapture(fs.readFileSync(nativePng))
|
|
504
563
|
const { diff, triptych } = compareScreens(
|
|
505
564
|
nativeBytes,
|
|
506
565
|
fs.readFileSync(peachPng),
|
package/cli/commands/maestro.ts
CHANGED
|
@@ -51,7 +51,12 @@ export interface RunMaestroOptions {
|
|
|
51
51
|
// (maestro compare reads it for the errors Peach saw)
|
|
52
52
|
captureConsole?: boolean
|
|
53
53
|
// each flow's final verdict, including a failure before any step ran
|
|
54
|
-
onFlowResult?: (result: {
|
|
54
|
+
onFlowResult?: (result: {
|
|
55
|
+
file: string
|
|
56
|
+
exit: number
|
|
57
|
+
playbackFailure: string
|
|
58
|
+
outputDir: string | null
|
|
59
|
+
}) => void
|
|
55
60
|
}
|
|
56
61
|
|
|
57
62
|
const MAESTRO_DIRS = ['.maestro', 'maestro']
|
|
@@ -518,6 +523,7 @@ export async function runMaestro(
|
|
|
518
523
|
let worstExit = 0
|
|
519
524
|
const results: Array<{
|
|
520
525
|
file: string
|
|
526
|
+
outputDir: string | null
|
|
521
527
|
exit: number
|
|
522
528
|
durationMs: number
|
|
523
529
|
steps: ReturnType<typeof getLastFlowTraceSteps>
|
|
@@ -537,6 +543,7 @@ export async function runMaestro(
|
|
|
537
543
|
console.error(` error: ${playbackFailure}`)
|
|
538
544
|
results.push({
|
|
539
545
|
file,
|
|
546
|
+
outputDir: flowTestDir,
|
|
540
547
|
exit: 1,
|
|
541
548
|
durationMs: 0,
|
|
542
549
|
steps: [],
|
|
@@ -610,6 +617,7 @@ export async function runMaestro(
|
|
|
610
617
|
}
|
|
611
618
|
const result = {
|
|
612
619
|
file,
|
|
620
|
+
outputDir: flowTestDir,
|
|
613
621
|
exit,
|
|
614
622
|
durationMs,
|
|
615
623
|
steps: traceSteps,
|
package/cli/commands/preview.ts
CHANGED
|
@@ -88,7 +88,7 @@ export async function runPreview(args: string[]): Promise<number> {
|
|
|
88
88
|
bundleSizeBytes: artifact.bytes.byteLength,
|
|
89
89
|
bundleContentType: 'application/javascript',
|
|
90
90
|
bundleEncoding: 'br',
|
|
91
|
-
bundleMinified:
|
|
91
|
+
bundleMinified: artifact.minified,
|
|
92
92
|
bundleOrigin: null,
|
|
93
93
|
entry: null,
|
|
94
94
|
isTransformed: false,
|