peachsim 0.1.1620 → 0.1.1635
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 +45 -6
- package/cli/app-config.ts +4 -1
- package/cli/app-fonts.ts +39 -18
- package/cli/app-stubs.ts +224 -9
- package/cli/bridge-flow-runner.ts +248 -156
- package/cli/cloud-client.ts +68 -57
- package/cli/command-registry.ts +1 -0
- package/cli/commands/box/search-command.ts +3 -2
- package/cli/commands/box.ts +10 -29
- package/cli/commands/control.ts +391 -92
- package/cli/commands/doctor.ts +17 -184
- package/cli/commands/flow.ts +79 -11
- package/cli/commands/inspect/core.ts +10 -5
- package/cli/commands/inspect/settling.ts +71 -22
- package/cli/commands/inspect/wait-ready.ts +12 -0
- package/cli/commands/inspect.ts +26 -3
- package/cli/commands/install-desktop.ts +1 -0
- package/cli/commands/maestro-compare.ts +105 -67
- package/cli/commands/maestro.ts +9 -1
- package/cli/commands/platform.ts +71 -36
- package/cli/commands/record.ts +21 -1
- 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 +5 -0
- package/cli/commands/upload.ts +51 -8
- package/cli/desktop-companion.ts +102 -17
- package/cli/dev-build-freshness.ts +37 -2
- package/cli/drivers/desktop.ts +14 -1
- package/cli/drivers/playwright-sim-host.ts +44 -8
- package/cli/drivers/playwright.ts +9 -8
- package/cli/drivers/types.ts +5 -0
- package/cli/maestro-accessibility.ts +53 -0
- package/cli/maestro-compare.ts +108 -12
- package/cli/main.ts +2 -1
- package/cli/outbound-endpoints.ts +35 -6
- package/cli/parent-pid.ts +0 -9
- package/cli/pause-aware-timeout.ts +102 -0
- package/cli/remote-simulators.ts +112 -0
- package/cli/remoteSimulatorContract.ts +21 -0
- package/cli/scene-video.ts +89 -0
- package/cli/ws-bridge.ts +23 -14
- package/detox/attempt-events.cjs +4 -0
- package/detox/expectations.ts +5 -1
- package/detox/index.ts +241 -181
- package/detox/motion-change.cjs +73 -0
- package/detox/motion-change.d.cts +21 -0
- package/detox/navigation-lifecycle.ts +22 -4
- package/dist-lib/agent-daemon-client.cjs +3 -2
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/attached-projects.cjs +1 -1
- package/dist-lib/auth/shared-session.cjs +3 -2
- 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 +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 +10480 -7962
- 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 +3 -2
- package/dist-lib/cloud.mjs +3 -2
- package/dist-lib/config.cjs +10 -6
- package/dist-lib/config.mjs +10 -6
- package/dist-lib/detox/index.cjs +1854 -966
- package/dist-lib/dev-bundle-resolution.cjs +64 -24
- package/dist-lib/expo-swift-bundle.cjs +8715 -6271
- package/dist-lib/expo-swift-metadata.cjs +7 -4
- package/dist-lib/expo-swift-metadata.mjs +7 -4
- package/dist-lib/home-paths.cjs +32 -3
- package/dist-lib/host/bridge-host.cjs +10135 -7060
- 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 +1843 -408
- 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 +3 -2
- package/dist-lib/menu.mjs +3 -2
- 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 +5 -4
- package/dist-lib/metro-production-bundle.mjs +5 -4
- package/dist-lib/metro.cjs +1182 -418
- package/dist-lib/native-app-identity-project.cjs +782 -23
- package/dist-lib/native-app-identity-project.mjs +781 -23
- package/dist-lib/native-app-identity.cjs +276 -9
- package/dist-lib/native-app-identity.mjs +273 -8
- package/dist-lib/native-firebase-metadata.cjs +5 -4
- package/dist-lib/native-firebase-metadata.mjs +5 -4
- 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 +3 -2
- package/dist-lib/react-native-host-modules.cjs +8 -1
- package/dist-lib/react-native-host-modules.mjs +8 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +106 -28
- package/dist-lib/sdk.cjs +115 -34
- package/dist-lib/sdk.mjs +115 -33
- package/dist-lib/skills.cjs +15274 -9904
- package/dist-lib/swift-compile.cjs +1165 -173
- package/dist-lib/swift-compile.mjs +1212 -203
- package/dist-lib/swift-package.cjs +12 -3
- package/dist-lib/swift-package.mjs +12 -3
- package/dist-lib/swift-submit.cjs +7 -1
- package/dist-lib/swift-submit.mjs +5 -1
- package/dist-lib/swift-toolchain.cjs +133 -18
- package/dist-lib/swift.cjs +1 -1
- package/dist-lib/vite-swift.cjs +2648 -862
- package/dist-lib/vite.cjs +70 -14
- package/dist-lib/worklet-executable.cjs +25 -2
- package/dist-lib/worklet-executable.mjs +25 -2
- package/kotlin/AppSources/dev/peach/app/PeachDrawable.kt +21 -14
- package/kotlin/Sources/androidx/annotation/DrawableRes.kt +14 -0
- package/package.json +2 -2
- package/scripts/dev-server-scanner.ts +35 -14
- package/skills/peach-perf/SKILL.md +11 -5
- package/skills/peach-setup/SKILL.md +16 -19
- package/src/admitted-mac-build.ts +34 -8
- package/src/android-app-identity.ts +146 -0
- package/src/android-autolinking.ts +75 -0
- package/src/android-string-value.ts +36 -0
- package/src/android-strings.ts +5 -36
- package/src/androidAutolinkingContract.ts +40 -0
- package/src/bridge-constants.ts +11 -0
- package/src/bridge-contract.ts +22 -34
- package/src/bundleGlobalFooter.ts +26 -7
- package/src/codex-client.ts +3 -2
- package/src/config.ts +15 -0
- package/src/desktop-host-delivery.ts +5 -1
- package/src/dev-bundle-resolution.ts +60 -26
- package/src/expo-kotlin-host.ts +6 -36
- package/src/expo-manifest-body.ts +13 -0
- package/src/expo-swift-bundle.ts +17 -12
- package/src/expo-swift-host.ts +640 -192
- package/src/expo-swift-ready.ts +78 -0
- package/src/expoSwiftContract.ts +19 -0
- package/src/home-paths.ts +48 -2
- package/src/host/bridge-host.ts +12 -57
- package/src/host/staged-host-files.ts +29 -2
- package/src/installed-package-versions.ts +54 -14
- package/src/launchMetadataContract.ts +24 -0
- package/src/metro-plugin.ts +37 -42
- package/src/native-app-identity-project.ts +142 -5
- package/src/native-app-identity.ts +176 -11
- package/src/native-firebase-config.ts +209 -13
- package/src/nitro-compile.ts +2 -5
- package/src/nitro-source.ts +147 -63
- package/src/process-group.ts +114 -61
- package/src/process-output.ts +0 -9
- package/src/public-brand.ts +1 -0
- package/src/react-native-bridge-modules.ts +416 -0
- package/src/react-native-host-modules.ts +7 -0
- package/src/sim-client.ts +6 -4
- package/src/swift-binary-modules.ts +2 -2
- package/src/swift-compile.ts +1007 -108
- package/src/swift-compiler-digest.ts +14 -0
- package/src/swift-distribution.lock.json +40 -12
- package/src/swift-distribution.ts +61 -9
- package/src/swift-host.ts +149 -56
- package/src/swift-icu.ts +14 -6
- package/src/swift-objc-sdk.ts +133 -10
- package/src/swift-objc-standin.ts +38 -3
- package/src/swift-package-manifest.ts +21 -6
- package/src/swift-package.ts +15 -1
- package/src/swift-packages.ts +119 -60
- package/src/swift-standin-emit.ts +57 -9
- package/src/swift-standins.ts +185 -25
- package/src/swift-submit.ts +10 -0
- package/src/swift-toolchain.ts +149 -23
- package/src/vite-plugin.ts +20 -20
- package/src/worklet-executable.ts +30 -3
- package/src/worklets-babel.ts +35 -14
- package/src/xcode-project.ts +10 -0
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
accessibilityBounds,
|
|
3
|
+
sameNativeAccessibilityShape,
|
|
4
|
+
} from '../../maestro-accessibility'
|
|
1
5
|
import {
|
|
2
6
|
inspectReadySnapshot,
|
|
3
7
|
inspectWaitReady,
|
|
@@ -16,9 +20,11 @@ export type WaitForSootsimIdleOptions = {
|
|
|
16
20
|
// in fewer polls instead of paying the count at its slower rate.
|
|
17
21
|
stablePolls?: number
|
|
18
22
|
strict?: boolean
|
|
19
|
-
// compare consecutive full-screen pixels
|
|
23
|
+
// compare consecutive full-screen pixels; explicit animation waits use this.
|
|
20
24
|
// frame publications and caret blinks do not require a continuous window.
|
|
21
25
|
paintStill?: boolean
|
|
26
|
+
// native maestro ios: pixel stability first, then native hierarchy equality.
|
|
27
|
+
maestro?: boolean
|
|
22
28
|
// settle on motion alone, for the waits around a gesture: image loads and
|
|
23
29
|
// network traffic move nothing on screen, and the hierarchy walk costs
|
|
24
30
|
// seconds per poll on a large tree under load.
|
|
@@ -75,6 +81,7 @@ export async function waitForSootsimIdle({
|
|
|
75
81
|
stablePolls = 3,
|
|
76
82
|
strict = false,
|
|
77
83
|
paintStill = false,
|
|
84
|
+
maestro = false,
|
|
78
85
|
motionOnly = false,
|
|
79
86
|
transitionStartWindowMs = 80,
|
|
80
87
|
transitionTimeoutMs = 4000,
|
|
@@ -108,12 +115,15 @@ export async function waitForSootsimIdle({
|
|
|
108
115
|
const pollMs = ${Math.max(1, Math.round(pollMs))}
|
|
109
116
|
const strict = ${strict ? 'true' : 'false'}
|
|
110
117
|
const paintStill = ${paintStill ? 'true' : 'false'}
|
|
118
|
+
const maestro = ${maestro ? 'true' : 'false'}
|
|
119
|
+
const accessibilityBounds = ${accessibilityBounds.toString()}
|
|
120
|
+
const sameNativeAccessibilityShape = ${sameNativeAccessibilityShape.toString()}
|
|
111
121
|
const motionOnly = ${motionOnly ? 'true' : 'false'}
|
|
112
122
|
// full mounted-geometry observation needs at least two hierarchy reads
|
|
113
123
|
// plus the stillness window, which a sub-second budget cannot afford:
|
|
114
124
|
// tight budgets (auto-wait) settle on worker stillness plus root
|
|
115
125
|
// membership, generous ones confirm the geometry too.
|
|
116
|
-
const fullGeometry = ${!motionOnly && maxMs >= 1000 ? 'true' : 'false'}
|
|
126
|
+
const fullGeometry = ${!maestro && !motionOnly && maxMs >= 1000 ? 'true' : 'false'}
|
|
117
127
|
const requiredStillMs = paintStill ? 0 : ${Math.max(1, Math.round(stablePolls)) * Math.max(1, Math.round(pollMs))}
|
|
118
128
|
const minimumStablePolls = paintStill ? 1 : ${Math.min(2, Math.max(1, Math.round(stablePolls)))}
|
|
119
129
|
const composite = paintStill ? window.SootSim?.bridges?.liveComposite : null
|
|
@@ -235,6 +245,9 @@ export async function waitForSootsimIdle({
|
|
|
235
245
|
// strict also waits out shell motion: a system alert, sheet or menu
|
|
236
246
|
// animates in the shell worker while the app's own stats read still,
|
|
237
247
|
// so its presentation is complete only once the shell's list empties.
|
|
248
|
+
const accessibilityRead = maestro
|
|
249
|
+
? answer(() => window.__sootsimTest.captureAccessibilityTree()).catch(() => null)
|
|
250
|
+
: null
|
|
238
251
|
const motionRead = strict
|
|
239
252
|
? answer(() => window.__sootsimTest?.listAnimations?.()).catch(() => null)
|
|
240
253
|
: null
|
|
@@ -244,9 +257,12 @@ export async function waitForSootsimIdle({
|
|
|
244
257
|
// the compositor rejects a stats request it is too busy to answer.
|
|
245
258
|
const compositorRead = answer(() => window.__sootsimCompositor.getStats()).catch(() => BUSY)
|
|
246
259
|
const countsRead = motionOnly ? null : answer(() => window.__sootsimTest?.getRequestCounts?.()).catch(() => null)
|
|
247
|
-
// mounted-root membership: the
|
|
248
|
-
// mounts, unmounts or recycles, in
|
|
249
|
-
|
|
260
|
+
// mounted-root membership: the count of nodes that can reach the
|
|
261
|
+
// window moves while the screen mounts, unmounts or recycles, in
|
|
262
|
+
// either render mode. a screen a pushed one covers or an inactive
|
|
263
|
+
// tab stays mounted out of the window, so what it mounts is not
|
|
264
|
+
// screen motion.
|
|
265
|
+
const nodeCountRead = answer(() => window.__sootsimTest?.getWindowNodeCount?.()).catch(() => null)
|
|
250
266
|
// the mounted hierarchy in render-worker mode comes from the test
|
|
251
267
|
// bridge: window.__sootsimRoot is an empty placeholder there, so a
|
|
252
268
|
// walk over it hashes every screen to the same empty string while
|
|
@@ -276,7 +292,7 @@ export async function waitForSootsimIdle({
|
|
|
276
292
|
const motion = await motionRead
|
|
277
293
|
if (motion === BUSY) workerBusy = true
|
|
278
294
|
else if (Array.isArray(motion)) {
|
|
279
|
-
const blocking = motion.filter((anim) => anim.realm === 'shell' && anim.priority !== 'low')
|
|
295
|
+
const blocking = motion.filter((anim) => anim.realm === 'shell' && anim.priority !== 'low' && anim.visible !== false)
|
|
280
296
|
shellAnimating = blocking.length > 0
|
|
281
297
|
// name what holds the settle so a timeout points at its owner.
|
|
282
298
|
shellBlockers = blocking
|
|
@@ -308,6 +324,8 @@ export async function waitForSootsimIdle({
|
|
|
308
324
|
stats.hasActiveAnims === true ||
|
|
309
325
|
stats.hasActiveBlockingNativeAnimations === true ||
|
|
310
326
|
(!paintStill && stats.hasPendingBlockingAnimationFrames === true)
|
|
327
|
+
// the runtime reports only layout work that can reach the window:
|
|
328
|
+
// a covered screen or inactive tab committing does not hold it.
|
|
311
329
|
layoutDirty =
|
|
312
330
|
stats.layoutDirty === true && stats.renderRequested === true
|
|
313
331
|
// a freshly-pushed screen showing a skeleton is layout-stable
|
|
@@ -340,6 +358,11 @@ export async function waitForSootsimIdle({
|
|
|
340
358
|
const total = await nodeCountRead
|
|
341
359
|
if (total === BUSY) workerBusy = true
|
|
342
360
|
nodeCount = typeof total === 'number' ? total : null
|
|
361
|
+
const accessibility = accessibilityRead ? await accessibilityRead : null
|
|
362
|
+
const nativeHierarchy = Array.isArray(accessibility) && accessibility.length > 0
|
|
363
|
+
? accessibility
|
|
364
|
+
: null
|
|
365
|
+
if (accessibility === BUSY) workerBusy = true
|
|
343
366
|
let hierarchy = hierarchyRead ? await hierarchyRead : null
|
|
344
367
|
if (hierarchy === BUSY) {
|
|
345
368
|
workerBusy = true
|
|
@@ -399,9 +422,15 @@ export async function waitForSootsimIdle({
|
|
|
399
422
|
webViewStarting = true
|
|
400
423
|
webViewBlocker = 'preview progress element displayed'
|
|
401
424
|
} else {
|
|
402
|
-
|
|
425
|
+
// a loading document with no area (an app's hidden worker or
|
|
426
|
+
// executor frame) paints nothing, and a screenshot on device
|
|
427
|
+
// does not wait for it.
|
|
428
|
+
const unreadyWebView = [...document.querySelectorAll(
|
|
403
429
|
'[data-sootsim-webview-loading="true"], [data-sootsim-webview-mode="placeholder"]'
|
|
404
|
-
)
|
|
430
|
+
)].find((view) => {
|
|
431
|
+
const frame = view.querySelector('iframe')
|
|
432
|
+
return !frame || frame.clientWidth > 0 || frame.clientHeight > 0
|
|
433
|
+
})
|
|
405
434
|
if (unreadyWebView) {
|
|
406
435
|
webViewStarting = true
|
|
407
436
|
webViewBlocker = 'webView document still loading'
|
|
@@ -420,7 +449,7 @@ export async function waitForSootsimIdle({
|
|
|
420
449
|
pixelFrameToken = token
|
|
421
450
|
}
|
|
422
451
|
}
|
|
423
|
-
return { layout: nodes.join(';'), screenPixels, animating, shellAnimating, shellBlockers, layoutDirty, pendingFetches, workerBusy, webViewStarting, webViewBlocker, renderStatsAvailable, requestTotal, requestInFlight, appCommits, nodeCount }
|
|
452
|
+
return { layout: nodes.join(';'), nativeHierarchy, screenPixels, animating, shellAnimating, shellBlockers, layoutDirty, pendingFetches, workerBusy, webViewStarting, webViewBlocker, renderStatsAvailable, requestTotal, requestInFlight, appCommits, nodeCount }
|
|
424
453
|
}
|
|
425
454
|
|
|
426
455
|
// how long generic background network is allowed to keep us waiting AFTER
|
|
@@ -434,6 +463,7 @@ export async function waitForSootsimIdle({
|
|
|
434
463
|
// so the generic request counters are a courtesy, not a gate: honor them
|
|
435
464
|
// only within this grace once the screen is visually still.
|
|
436
465
|
const networkQuietGraceMs = 1200
|
|
466
|
+
let lastNativeHierarchy = null
|
|
437
467
|
let lastLayout = ''
|
|
438
468
|
let lastScreenPixels = null
|
|
439
469
|
let screenChangedPolls = 0
|
|
@@ -461,6 +491,9 @@ export async function waitForSootsimIdle({
|
|
|
461
491
|
}
|
|
462
492
|
return true
|
|
463
493
|
}
|
|
494
|
+
const hierarchyUnchanged = (current, previous) =>
|
|
495
|
+
current !== null && previous !== null && current.length === previous.length &&
|
|
496
|
+
current.every((node, index) => sameNativeAccessibilityShape(node, previous[index], accessibilityBounds))
|
|
464
497
|
const IDLE_HELD = { idleHeld: true }
|
|
465
498
|
while (true) {
|
|
466
499
|
if (clockNow() >= deadline) {
|
|
@@ -506,7 +539,7 @@ export async function waitForSootsimIdle({
|
|
|
506
539
|
// the loop head drains a transition still in flight before it ends.
|
|
507
540
|
if (snapshot.workerBusy && clockNow() >= deadline && report) continue
|
|
508
541
|
polls++
|
|
509
|
-
report = { snapshot, layout: lastLayout, screenPixels: lastScreenPixels, requestTotal: lastRequestTotal, appCommits: lastAppCommits, nodeCount: lastNodeCount }
|
|
542
|
+
report = { snapshot, nativeHierarchy: lastNativeHierarchy, layout: lastLayout, screenPixels: lastScreenPixels, requestTotal: lastRequestTotal, appCommits: lastAppCommits, nodeCount: lastNodeCount }
|
|
510
543
|
if (snapshot.layoutDirty) dirtyPolls++
|
|
511
544
|
if (snapshot.animating || snapshot.shellAnimating) animatingPolls++
|
|
512
545
|
if (snapshot.webViewStarting) webViewStartingPolls++
|
|
@@ -527,7 +560,15 @@ export async function waitForSootsimIdle({
|
|
|
527
560
|
// strictOk and adds shell motion plus the blocking-stats floor. a
|
|
528
561
|
// null count or commit reading means the runtime does not report
|
|
529
562
|
// it, which reads as stable the way a missing stats block does.
|
|
530
|
-
|
|
563
|
+
// native screen checks observe output, not animation ownership. after
|
|
564
|
+
// its 3 s pixel phase, iOS accepts two equal native TreeNodes even
|
|
565
|
+
// while paint-only playback continues. overlap hierarchy reads so
|
|
566
|
+
// neither the gesture nor screenshot deadline grows.
|
|
567
|
+
const maestroStill = snapshot.screenPixels !== null && !snapshot.workerBusy && !snapshot.webViewStarting && (
|
|
568
|
+
!screenChanged || (clockNow() - start >= 3000 &&
|
|
569
|
+
hierarchyUnchanged(snapshot.nativeHierarchy, lastNativeHierarchy))
|
|
570
|
+
)
|
|
571
|
+
const visuallyStill = maestro ? maestroStill :
|
|
531
572
|
strictOk &&
|
|
532
573
|
!snapshot.animating &&
|
|
533
574
|
!snapshot.layoutDirty &&
|
|
@@ -555,7 +596,7 @@ export async function waitForSootsimIdle({
|
|
|
555
596
|
// grace (continuous background traffic must not block forever).
|
|
556
597
|
if (
|
|
557
598
|
stillProven() &&
|
|
558
|
-
(motionOnly || networkQuiet || clockNow() - visuallyStillSince >= networkQuietGraceMs)
|
|
599
|
+
(maestro || motionOnly || networkQuiet || clockNow() - visuallyStillSince >= networkQuietGraceMs)
|
|
559
600
|
) {
|
|
560
601
|
if (transitionWait) {
|
|
561
602
|
await transitionWait
|
|
@@ -567,6 +608,7 @@ export async function waitForSootsimIdle({
|
|
|
567
608
|
stillPolls = []
|
|
568
609
|
visuallyStillSince = 0
|
|
569
610
|
}
|
|
611
|
+
lastNativeHierarchy = snapshot.nativeHierarchy
|
|
570
612
|
lastLayout = snapshot.layout
|
|
571
613
|
lastScreenPixels = snapshot.screenPixels
|
|
572
614
|
lastRequestTotal = snapshot.requestTotal
|
|
@@ -589,20 +631,21 @@ export async function waitForSootsimIdle({
|
|
|
589
631
|
}
|
|
590
632
|
const last = report.snapshot
|
|
591
633
|
const blockers = []
|
|
592
|
-
if (strict && !last.renderStatsAvailable) blockers.push('render stats unavailable')
|
|
593
|
-
if (last.animating) blockers.push('animations still running')
|
|
594
|
-
if (last.shellAnimating) blockers.push('shell animations still running (' + last.shellBlockers + ')')
|
|
595
|
-
if (last.layoutDirty) blockers.push('layout still dirty')
|
|
596
|
-
if (last.appCommits !== null && report.appCommits !== null && last.appCommits !== report.appCommits) blockers.push('layout commits still landing')
|
|
597
|
-
if (last.nodeCount !== null && report.nodeCount !== null && last.nodeCount !== report.nodeCount) blockers.push('node count still changing')
|
|
598
|
-
if (!motionOnly && last.pendingFetches > 0) blockers.push(last.pendingFetches + ' image fetches pending')
|
|
634
|
+
if (!maestro && strict && !last.renderStatsAvailable) blockers.push('render stats unavailable')
|
|
635
|
+
if (!maestro && last.animating) blockers.push('animations still running')
|
|
636
|
+
if (!maestro && last.shellAnimating) blockers.push('shell animations still running (' + last.shellBlockers + ')')
|
|
637
|
+
if (!maestro && last.layoutDirty) blockers.push('layout still dirty')
|
|
638
|
+
if (!maestro && last.appCommits !== null && report.appCommits !== null && last.appCommits !== report.appCommits) blockers.push('layout commits still landing')
|
|
639
|
+
if (!maestro && last.nodeCount !== null && report.nodeCount !== null && last.nodeCount !== report.nodeCount) blockers.push('node count still changing')
|
|
640
|
+
if (!maestro && !motionOnly && last.pendingFetches > 0) blockers.push(last.pendingFetches + ' image fetches pending')
|
|
599
641
|
if (last.workerBusy) blockers.push('a render worker too busy to report')
|
|
600
642
|
if (last.webViewStarting) blockers.push(last.webViewBlocker || 'webView still starting')
|
|
601
|
-
if (last.layout !== report.layout) blockers.push('layout still changing')
|
|
643
|
+
if (!maestro && last.layout !== report.layout) blockers.push('layout still changing')
|
|
644
|
+
if (maestro && !hierarchyUnchanged(last.nativeHierarchy, report.nativeHierarchy)) blockers.push(last.nativeHierarchy === null ? 'native accessibility hierarchy unavailable' : 'native accessibility hierarchy still changing')
|
|
602
645
|
if (paintStill && !last.screenPixels) blockers.push('screen pixels unavailable')
|
|
603
646
|
else if (paintStill && !screenUnchanged(last.screenPixels, report.screenPixels)) blockers.push('screen pixels still changing')
|
|
604
|
-
if (!motionOnly && last.requestInFlight > 0) blockers.push(last.requestInFlight + ' requests in flight')
|
|
605
|
-
else if (!motionOnly && last.requestTotal !== report.requestTotal) blockers.push('new requests issued')
|
|
647
|
+
if (!maestro && !motionOnly && last.requestInFlight > 0) blockers.push(last.requestInFlight + ' requests in flight')
|
|
648
|
+
else if (!maestro && !motionOnly && last.requestTotal !== report.requestTotal) blockers.push('new requests issued')
|
|
606
649
|
if (polls < 2) blockers.push('sim too busy to poll twice')
|
|
607
650
|
// the last read is still: motion ended too near the deadline for the
|
|
608
651
|
// stillness window to hold before it.
|
|
@@ -654,6 +697,9 @@ export type SettledWaitReady = {
|
|
|
654
697
|
// the last readiness read: the settled tree once the app went idle,
|
|
655
698
|
// otherwise the read that ended the wait.
|
|
656
699
|
status: WaitReadyStatus
|
|
700
|
+
// the first read that counted as ready, before the settle: the probe the
|
|
701
|
+
// wait accepted. null when the app never became ready.
|
|
702
|
+
accepted: WaitReadyStatus | null
|
|
657
703
|
// the last settle; null when the app never became ready.
|
|
658
704
|
idle: WaitForSootsimIdleResult | null
|
|
659
705
|
elapsedMs: number
|
|
@@ -672,8 +718,10 @@ export async function waitReadyAndSettle(
|
|
|
672
718
|
): Promise<SettledWaitReady> {
|
|
673
719
|
const start = Date.now()
|
|
674
720
|
const remaining = () => Math.max(0, start + timeoutMs - Date.now())
|
|
721
|
+
let accepted: WaitReadyStatus | null = null
|
|
675
722
|
const result = (status: WaitReadyStatus, idle: WaitForSootsimIdleResult | null) => ({
|
|
676
723
|
status,
|
|
724
|
+
accepted,
|
|
677
725
|
idle,
|
|
678
726
|
elapsedMs: Date.now() - start,
|
|
679
727
|
})
|
|
@@ -682,6 +730,7 @@ export async function waitReadyAndSettle(
|
|
|
682
730
|
idle = null
|
|
683
731
|
const ready = await inspectWaitReady(bridge, remaining(), options)
|
|
684
732
|
if (!ready.ready) return result(ready, idle)
|
|
733
|
+
accepted = ready
|
|
685
734
|
idle = await waitForSootsimIdle({
|
|
686
735
|
bridge,
|
|
687
736
|
simId: options.simId,
|
|
@@ -26,10 +26,12 @@ export async function runWaitReadySubcommand(opts: {
|
|
|
26
26
|
if (bridge.plane === 'cloud') {
|
|
27
27
|
const status = await inspectWaitReady(bridge, timeoutMs)
|
|
28
28
|
if (!status.ready) timedOut(status, status.elapsedMs)
|
|
29
|
+
printAcceptedProbe(status)
|
|
29
30
|
printReady(status, status.elapsedMs)
|
|
30
31
|
return
|
|
31
32
|
}
|
|
32
33
|
let status: Awaited<ReturnType<typeof waitReadyAndSettle>>['status']
|
|
34
|
+
let accepted: Awaited<ReturnType<typeof waitReadyAndSettle>>['accepted']
|
|
33
35
|
let idle: Awaited<ReturnType<typeof waitReadyAndSettle>>['idle']
|
|
34
36
|
let elapsedMs: number
|
|
35
37
|
try {
|
|
@@ -41,6 +43,7 @@ export async function runWaitReadySubcommand(opts: {
|
|
|
41
43
|
},
|
|
42
44
|
})
|
|
43
45
|
status = settledWait.status
|
|
46
|
+
accepted = settledWait.accepted
|
|
44
47
|
idle = settledWait.idle
|
|
45
48
|
elapsedMs = settledWait.elapsedMs
|
|
46
49
|
} catch (error) {
|
|
@@ -51,6 +54,7 @@ export async function runWaitReadySubcommand(opts: {
|
|
|
51
54
|
throw error
|
|
52
55
|
}
|
|
53
56
|
if (!idle) timedOut(status, elapsedMs)
|
|
57
|
+
if (accepted) printAcceptedProbe(accepted)
|
|
54
58
|
if (!idle.settled) {
|
|
55
59
|
console.error(
|
|
56
60
|
` ⚠ wait ready timed out after ${elapsedMs}ms — app mounted but did not settle: ${idle.blockedBy} (nodes: ${status.nodes}, targets: ${status.targets})`,
|
|
@@ -66,6 +70,14 @@ export async function runWaitReadySubcommand(opts: {
|
|
|
66
70
|
printReady(status, elapsedMs)
|
|
67
71
|
}
|
|
68
72
|
|
|
73
|
+
// the probe the wait accepted, kept so a ready that later proves wrong can be
|
|
74
|
+
// read from the command's own output.
|
|
75
|
+
function printAcceptedProbe(status: WaitReadyStatus): void {
|
|
76
|
+
console.error(
|
|
77
|
+
` accepted probe at ${Math.round(status.elapsedMs)}ms: basis ${status.readyBasis}, flag ${String(status.flag)}, ${status.nodes} nodes (quiet ${Math.round(status.treeQuietMs)}ms), ${status.targets} targets, painted ${status.paintedAppContent}, live frame ${status.liveFrameActive} (${status.liveFrameChannels} channels, ${status.liveFramePublishes} publishes), browser surfaces ${status.browserSurfaceCount}`,
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
69
81
|
function printReady(status: WaitReadyStatus, elapsedMs: number): void {
|
|
70
82
|
console.log(
|
|
71
83
|
` ready in ${elapsedMs}ms: ${status.nodes} nodes, ${status.targets} targets${status.liveFrameActive ? `, ${status.liveFrameChannels} live surfaces` : ''}${status.browserSurfaceCount ? `, ${status.browserSurfaceCount} browser surfaces` : ''}`,
|
package/cli/commands/inspect.ts
CHANGED
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
parseBridgeCliArgs,
|
|
35
35
|
printBridgeWorldNotice,
|
|
36
36
|
type WsBridge,
|
|
37
|
+
type BridgeSimInfo,
|
|
37
38
|
} from '../ws-bridge'
|
|
38
39
|
import {
|
|
39
40
|
isTapSuccess,
|
|
@@ -596,13 +597,13 @@ export function printShellPerfReport(result: Record<string, any>) {
|
|
|
596
597
|
console.log(``)
|
|
597
598
|
console.log(` cadence (frame delivery, ${cadence.vsyncTicks} vsync ticks):`)
|
|
598
599
|
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)` : ''}`,
|
|
600
|
+
` 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
601
|
)
|
|
601
602
|
console.log(
|
|
602
603
|
` delivery lag: p50 ${cadence.deliveryLagP50} / p95 ${cadence.deliveryLagP95} / max ${cadence.deliveryLagMax} ms (vsync postMessage → shell receipt)`,
|
|
603
604
|
)
|
|
604
605
|
console.log(
|
|
605
|
-
`
|
|
606
|
+
` present interval: p50 ${cadence.paintIntervalP50} / p95 ${cadence.paintIntervalP95} / max ${cadence.paintIntervalMax} ms (during motion, ${cadence.paintIntervals} intervals, including native frames)`,
|
|
606
607
|
)
|
|
607
608
|
}
|
|
608
609
|
const compositorCadence = result.compositorCadence
|
|
@@ -1247,7 +1248,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
|
|
|
1247
1248
|
return
|
|
1248
1249
|
}
|
|
1249
1250
|
|
|
1250
|
-
//
|
|
1251
|
+
// driver discovery works offline; a live bridge also reports each running renderer.
|
|
1251
1252
|
// short-circuit here so users can discover drivers even when no peach
|
|
1252
1253
|
// session is running.
|
|
1253
1254
|
if (
|
|
@@ -1256,6 +1257,19 @@ export async function runInspect(args: string[], opts: InspectOptions) {
|
|
|
1256
1257
|
) {
|
|
1257
1258
|
const { buildDriverListRows } = await import('../drivers')
|
|
1258
1259
|
const rows = buildDriverListRows()
|
|
1260
|
+
const bridge = (opts.createBridge ?? createBridgeFromParsed)(parsed)
|
|
1261
|
+
let sims: BridgeSimInfo[] = []
|
|
1262
|
+
try {
|
|
1263
|
+
sims = await bridge.listSims()
|
|
1264
|
+
} catch {
|
|
1265
|
+
// local availability remains useful when no bridge is running.
|
|
1266
|
+
} finally {
|
|
1267
|
+
bridge.close()
|
|
1268
|
+
}
|
|
1269
|
+
if (wantsJson(effectiveArgs)) {
|
|
1270
|
+
printJson({ drivers: rows, sims })
|
|
1271
|
+
return
|
|
1272
|
+
}
|
|
1259
1273
|
console.log(` available drivers (${rows.length}):\n`)
|
|
1260
1274
|
const idWidth = Math.max(...rows.map((r) => r.id.length), 6)
|
|
1261
1275
|
const kindWidth = Math.max(...rows.map((r) => r.kind.length), 4)
|
|
@@ -1270,6 +1284,15 @@ export async function runInspect(args: string[], opts: InspectOptions) {
|
|
|
1270
1284
|
console.log(` unavailable: ${row.reason}`)
|
|
1271
1285
|
}
|
|
1272
1286
|
}
|
|
1287
|
+
for (const sim of sims) {
|
|
1288
|
+
const renderer = sim.meta?.peachRenderer
|
|
1289
|
+
const executable = sim.meta?.peachHostExecutable
|
|
1290
|
+
if (sim.readyState === 'open' && typeof renderer === 'string') {
|
|
1291
|
+
console.log(
|
|
1292
|
+
` in use: ${sim.id} · ${String(sim.meta?.sootsimHostDriver)} · ${renderer}${typeof executable === 'string' ? ` @ ${executable}` : ''}`,
|
|
1293
|
+
)
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1273
1296
|
return
|
|
1274
1297
|
}
|
|
1275
1298
|
|
|
@@ -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,21 +8,23 @@ 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 {
|
|
14
15
|
buildCompareHtml,
|
|
16
|
+
buildCompareSide,
|
|
15
17
|
compareScreens,
|
|
16
18
|
comparePassed,
|
|
17
19
|
instrumentFlowSource,
|
|
18
20
|
nativeStepsFromCommandsJson,
|
|
19
|
-
|
|
21
|
+
nativeFlowVerdictsFromJunit,
|
|
22
|
+
junitFlowKey,
|
|
20
23
|
peachStepsFromCommandsJson,
|
|
21
24
|
screensOverThreshold,
|
|
22
25
|
type CompareCapture,
|
|
23
26
|
type CompareFlow,
|
|
24
27
|
type CompareReport,
|
|
25
|
-
type CompareSide,
|
|
26
28
|
} from '../maestro-compare'
|
|
27
29
|
import { MaestroJsContext } from '../maestro-js'
|
|
28
30
|
import { collectFlowFiles, loadMaestroWorkspaceConfig } from '../maestro-workspace'
|
|
@@ -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,45 +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
|
-
function sideFromSteps(
|
|
205
|
-
steps: CompareSide['steps'],
|
|
206
|
-
ran: boolean,
|
|
207
|
-
extraErrors: string[],
|
|
208
|
-
// a run that failed outside its steps (the sim or app never came up)
|
|
209
|
-
runFailure?: string,
|
|
210
|
-
): CompareSide {
|
|
211
|
-
const failed = steps.find((s) => s.status === 'failed')
|
|
212
|
-
return {
|
|
213
|
-
status: !ran ? 'not run' : failed || runFailure ? 'failed' : 'passed',
|
|
214
|
-
durationMs: steps.reduce((sum, s) => sum + s.durationMs, 0),
|
|
215
|
-
steps,
|
|
216
|
-
errors: [
|
|
217
|
-
...(failed
|
|
218
|
-
? [`step "${failed.label}" failed: ${failed.error ?? 'no message'}`]
|
|
219
|
-
: runFailure
|
|
220
|
-
? [runFailure]
|
|
221
|
-
: []),
|
|
222
|
-
...extraErrors,
|
|
223
|
-
],
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// peach names each flow's output dir after the file stem, suffixing repeats
|
|
228
|
-
// in run order (commands/maestro.ts uniqueFlowDir).
|
|
229
|
-
function peachFlowDirs(root: string, files: string[]): string[] {
|
|
230
|
-
const seen = new Map<string, number>()
|
|
231
|
-
return files.map((file) => {
|
|
232
|
-
const slug =
|
|
233
|
-
path
|
|
234
|
-
.basename(file)
|
|
235
|
-
.replace(/\.(ya?ml)$/i, '')
|
|
236
|
-
.replace(/[^A-Za-z0-9._-]+/g, '_') || 'flow'
|
|
237
|
-
const n = (seen.get(slug) ?? 0) + 1
|
|
238
|
-
seen.set(slug, n)
|
|
239
|
-
return path.join(root, n === 1 ? slug : `${slug}-${n}`)
|
|
240
|
-
})
|
|
241
|
-
}
|
|
242
|
-
|
|
243
238
|
// the flow's appId as maestro resolves it: its `${...}` templates evaluate
|
|
244
239
|
// against --env values (in process.env) and the flow's own env block.
|
|
245
240
|
function resolveFlowAppId(frontmatter: Record<string, unknown>): string | undefined {
|
|
@@ -447,6 +442,10 @@ export async function runMaestroCompare(
|
|
|
447
442
|
'--test-output-dir',
|
|
448
443
|
nativeOut,
|
|
449
444
|
'--no-ansi',
|
|
445
|
+
'--format',
|
|
446
|
+
'junit',
|
|
447
|
+
'--output',
|
|
448
|
+
path.join(out, 'ios-report.xml'),
|
|
450
449
|
...shared,
|
|
451
450
|
...envPairs,
|
|
452
451
|
],
|
|
@@ -456,18 +455,23 @@ export async function runMaestroCompare(
|
|
|
456
455
|
spawnSync('xcrun', ['simctl', 'status_bar', udid, 'clear'], { stdio: 'ignore' })
|
|
457
456
|
}
|
|
458
457
|
console.log(`\n ios │ maestro exited ${nativeExit}\n`)
|
|
459
|
-
const
|
|
458
|
+
const nativeVerdicts = nativeFlowVerdictsFromJunit(
|
|
459
|
+
fs.readFileSync(path.join(out, 'ios-report.xml'), 'utf8'),
|
|
460
|
+
)
|
|
460
461
|
|
|
461
462
|
// 2. peach, same instrumented flows (its --env already applied above)
|
|
462
463
|
const peachOut = path.join(out, 'peach')
|
|
463
|
-
const
|
|
464
|
+
const peachResults = new Map<
|
|
465
|
+
string,
|
|
466
|
+
{ exit: number; playbackFailure: string; outputDir: string | null }
|
|
467
|
+
>()
|
|
464
468
|
await runMaestro(
|
|
465
469
|
['test', runTarget, '--test-output-dir', peachOut, ...shared, ...peachArgs],
|
|
466
470
|
{
|
|
467
471
|
...opts,
|
|
468
472
|
captureConsole: true,
|
|
469
|
-
onFlowResult: ({ file, exit, playbackFailure }) => {
|
|
470
|
-
|
|
473
|
+
onFlowResult: ({ file, exit, playbackFailure, outputDir }) => {
|
|
474
|
+
peachResults.set(path.resolve(file), { exit, playbackFailure, outputDir })
|
|
471
475
|
},
|
|
472
476
|
},
|
|
473
477
|
)
|
|
@@ -481,24 +485,56 @@ export async function runMaestroCompare(
|
|
|
481
485
|
.sort()
|
|
482
486
|
.pop()
|
|
483
487
|
: undefined
|
|
484
|
-
const
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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
|
+
}
|
|
488
513
|
const imagesDir = path.join(out, 'images')
|
|
489
514
|
const flows: CompareFlow[] = []
|
|
490
515
|
for (const [i, flow] of instrumented.entries()) {
|
|
491
|
-
const
|
|
492
|
-
const
|
|
493
|
-
const
|
|
494
|
-
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)
|
|
520
|
+
const peachResult = peachResults.get(path.resolve(flow.copy))
|
|
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
|
+
)
|
|
495
529
|
const nativeSteps = nativeRan
|
|
496
|
-
? nativeStepsFromCommandsJson(
|
|
530
|
+
? nativeStepsFromCommandsJson(nativeOutput?.commands)
|
|
497
531
|
: []
|
|
498
532
|
const peachSteps = peachRan
|
|
499
|
-
? peachStepsFromCommandsJson(readJson(path.join(peachDir
|
|
533
|
+
? peachStepsFromCommandsJson(readJson(path.join(peachDir!, 'commands.json')))
|
|
500
534
|
: []
|
|
501
|
-
const consoleJson =
|
|
535
|
+
const consoleJson = (
|
|
536
|
+
peachDir ? readJson(path.join(peachDir, 'logs', 'console.json')) : null
|
|
537
|
+
) as {
|
|
502
538
|
errors?: unknown[]
|
|
503
539
|
requests?: unknown[]
|
|
504
540
|
} | null
|
|
@@ -513,15 +549,17 @@ export async function runMaestroCompare(
|
|
|
513
549
|
const nativePng = nativeDir
|
|
514
550
|
? path.join(nativeDir, 'takeScreenshot', `${capture.name}.png`)
|
|
515
551
|
: ''
|
|
516
|
-
const peachPng =
|
|
552
|
+
const peachPng = peachDir
|
|
553
|
+
? path.join(peachDir, 'screenshots', `${capture.name}.png`)
|
|
554
|
+
: ''
|
|
517
555
|
const hasNative = Boolean(nativePng) && fs.existsSync(nativePng)
|
|
518
|
-
const hasPeach = fs.existsSync(peachPng)
|
|
556
|
+
const hasPeach = Boolean(peachPng) && fs.existsSync(peachPng)
|
|
519
557
|
const imageRel = path.join(
|
|
520
558
|
'images',
|
|
521
559
|
`${i + 1}-${capture.name.split('/').pop()}.png`,
|
|
522
560
|
)
|
|
523
561
|
if (hasNative && hasPeach) {
|
|
524
|
-
const nativeBytes = fs.readFileSync(nativePng)
|
|
562
|
+
const nativeBytes = alignNativeCapture(fs.readFileSync(nativePng))
|
|
525
563
|
const { diff, triptych } = compareScreens(
|
|
526
564
|
nativeBytes,
|
|
527
565
|
fs.readFileSync(peachPng),
|
|
@@ -545,21 +583,21 @@ export async function runMaestroCompare(
|
|
|
545
583
|
flows.push({
|
|
546
584
|
name: flow.name,
|
|
547
585
|
file: displayPath(cwd, flow.file),
|
|
548
|
-
native:
|
|
586
|
+
native: buildCompareSide(
|
|
549
587
|
nativeSteps,
|
|
550
|
-
nativeRan,
|
|
588
|
+
nativeFailure ? 'failed' : nativeRan ? 'passed' : 'not run',
|
|
551
589
|
nativeRan
|
|
552
590
|
? []
|
|
553
591
|
: [
|
|
554
592
|
`maestro produced no output for this flow (exit ${nativeExit}); see ios.log`,
|
|
555
593
|
],
|
|
556
|
-
|
|
594
|
+
nativeFailure,
|
|
557
595
|
),
|
|
558
|
-
peach:
|
|
596
|
+
peach: buildCompareSide(
|
|
559
597
|
peachSteps,
|
|
560
|
-
|
|
598
|
+
peachResult ? (peachResult.exit === 0 ? 'passed' : 'failed') : 'not run',
|
|
561
599
|
peachConsole,
|
|
562
|
-
|
|
600
|
+
peachResult?.exit ? peachResult.playbackFailure : undefined,
|
|
563
601
|
),
|
|
564
602
|
screens,
|
|
565
603
|
})
|