peachsim 0.1.939 → 0.1.1399
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/README.md +28 -22
- package/cli/app-catalog.ts +29 -14
- package/cli/app-fonts.ts +5 -3
- package/cli/bridge-flow-runner.ts +40 -29
- package/cli/cloud-client.ts +57 -21
- package/cli/cloud-test-tunnel.ts +12 -18
- package/cli/commands/agent.ts +1 -1
- package/cli/commands/app-fonts.ts +3 -3
- package/cli/commands/assert.ts +8 -8
- package/cli/commands/auth.ts +8 -3
- package/cli/commands/box/bundle-plane.ts +13 -1
- package/cli/commands/box.ts +16 -5
- package/cli/commands/compat.ts +1 -1
- package/cli/commands/config.ts +2 -2
- package/cli/commands/control.ts +125 -71
- package/cli/commands/cpu-profile.ts +45 -1
- package/cli/commands/daemon.ts +131 -33
- package/cli/commands/debug.ts +24 -17
- package/cli/commands/desktop.ts +7 -1
- package/cli/commands/detect.ts +101 -0
- package/cli/commands/detox.ts +6 -6
- package/cli/commands/device.ts +12 -12
- package/cli/commands/electron.ts +32 -14
- package/cli/commands/flow.ts +49 -41
- package/cli/commands/guest-host.ts +39 -1
- package/cli/commands/inspect/core.ts +14 -8
- package/cli/commands/inspect/list.ts +1 -1
- package/cli/commands/inspect/settle.ts +5 -3
- package/cli/commands/inspect/settling.ts +229 -44
- package/cli/commands/inspect/wait-idle.ts +3 -2
- package/cli/commands/inspect.ts +105 -93
- package/cli/commands/install-cli.ts +36 -15
- package/cli/commands/install-desktop.ts +68 -195
- package/cli/commands/kotlin.ts +6 -5
- package/cli/commands/login.ts +5 -5
- package/cli/commands/maestro-compare.ts +554 -0
- package/cli/commands/maestro-generate.ts +6 -3
- package/cli/commands/maestro.ts +22 -0
- package/cli/commands/mode.ts +2 -2
- package/cli/commands/no-bridge-hint.ts +6 -2
- package/cli/commands/platform.ts +35 -6
- package/cli/commands/preview.ts +6 -0
- package/cli/commands/project-report.ts +10 -12
- package/cli/commands/record.ts +37 -37
- package/cli/commands/runtime.ts +9 -9
- package/cli/commands/screenshot-layers.ts +4 -4
- package/cli/commands/screenshot-mode.ts +2 -2
- package/cli/commands/screenshot.ts +8 -8
- package/cli/commands/screenshots-capture.ts +2 -2
- package/cli/commands/serve.ts +9 -12
- package/cli/commands/setup.ts +29 -14
- package/cli/commands/shell-boolean-mode.ts +2 -2
- package/cli/commands/show-touches-mode.ts +2 -2
- package/cli/commands/skills.ts +11 -11
- package/cli/commands/swift.ts +29 -11
- package/cli/commands/telemetry.ts +2 -2
- package/cli/commands/test.ts +5 -0
- package/cli/commands/three-mode.ts +3 -3
- package/cli/commands/timeline.ts +4 -4
- package/cli/commands/upgrade.ts +2 -2
- package/cli/commands/upload.ts +15 -15
- package/cli/commands/what-happened.ts +2 -2
- package/cli/current-sim.ts +6 -3
- package/cli/desktop-companion.ts +55 -149
- package/cli/dev-build-freshness.ts +2 -2
- package/cli/drivers/desktop.ts +17 -9
- package/cli/drivers/electron.ts +1 -2
- package/cli/drivers/playwright-sim-host.ts +11 -2
- package/cli/drivers/playwright.ts +35 -2
- package/cli/drivers/types.ts +8 -0
- package/cli/maestro-compare.ts +670 -0
- package/cli/outbound-endpoints.ts +46 -8
- package/cli/recording-access.ts +6 -6
- package/cli/run-registry.ts +1 -1
- package/cli/runtime-compat.ts +5 -1
- package/cli/send-to-box.ts +3 -3
- package/cli/shell-init.ts +2 -1
- package/cli/ws-bridge.ts +9 -5
- package/detox/element-types.ts +1 -0
- package/detox/gestures.ts +3 -3
- package/detox/index.ts +67 -38
- package/detox/navigation-lifecycle.ts +9 -6
- package/detox/proof-frame.cjs +6 -4
- 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 +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 +42 -17
- package/dist-lib/bridge-contract-input.mjs +41 -16
- package/dist-lib/bridge-contract.cjs +1 -4
- package/dist-lib/bridge-contract.mjs +1 -3
- package/dist-lib/bundle-channel.cjs +2918 -550
- package/dist-lib/capture-contract.cjs +65 -24
- package/dist-lib/capture-contract.mjs +63 -24
- package/dist-lib/cloud-contract.cjs +2 -2
- package/dist-lib/cloud-contract.mjs +2 -2
- package/dist-lib/cloud-test-args.cjs +1 -1
- package/dist-lib/cloud-test-args.mjs +1 -1
- package/dist-lib/cloud.cjs +3 -3
- package/dist-lib/cloud.mjs +3 -3
- package/dist-lib/config.cjs +15 -5
- package/dist-lib/config.mjs +154 -0
- package/dist-lib/detox/index.cjs +495 -139
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/expo-swift-bundle.cjs +1168 -238
- package/dist-lib/expo-swift-metadata.cjs +34 -23
- package/dist-lib/expo-swift-metadata.mjs +32 -23
- package/dist-lib/home-paths.cjs +2 -2
- package/dist-lib/host/bridge-host.cjs +2938 -654
- package/dist-lib/host/fetch-proxy-handler.cjs +14 -1
- package/dist-lib/host/fetch-proxy-overrides.cjs +7 -1
- package/dist-lib/host/fetch-proxy-overrides.mjs +5 -1
- package/dist-lib/host/replacement-module-handler.cjs +2 -2
- package/dist-lib/host/websocket-proxy.cjs +1 -1
- package/dist-lib/index.cjs +999 -353
- 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 +332 -19
- 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 +7 -7
- package/dist-lib/metro-production-bundle.mjs +5 -5
- package/dist-lib/metro.cjs +688 -250
- package/dist-lib/native-app-identity-project.cjs +2647 -0
- package/dist-lib/native-app-identity-project.mjs +1257 -0
- package/dist-lib/native-app-identity.cjs +325 -25
- package/dist-lib/native-app-identity.mjs +323 -25
- 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 +5 -1
- package/dist-lib/react-native-host-modules.mjs +5 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +57 -32
- package/dist-lib/sdk.cjs +231 -51
- package/dist-lib/sdk.mjs +231 -51
- package/dist-lib/skills.cjs +4107 -1374
- package/dist-lib/swift-compile.cjs +290 -30
- package/dist-lib/swift-compile.mjs +290 -30
- package/dist-lib/swift-package.cjs +143 -12
- package/dist-lib/swift-package.mjs +137 -11
- package/dist-lib/swift-submit.cjs +47 -5
- package/dist-lib/swift-submit.mjs +42 -5
- package/dist-lib/swift-toolchain.cjs +7 -2
- package/dist-lib/swift.cjs +1 -1
- package/dist-lib/vite-swift.cjs +1392 -458
- package/dist-lib/vite.cjs +13 -17
- package/dist-lib/worklet-executable.cjs +248 -33
- package/dist-lib/worklet-executable.mjs +248 -33
- package/install.sh +10 -1
- package/kotlin/{AppSources → AndroidViewSources}/android/content/res/Resources.kt +9 -9
- package/kotlin/AndroidViewSources/android/graphics/Canvas.kt +20 -0
- package/kotlin/AndroidViewSources/android/view/View.kt +211 -0
- package/kotlin/AndroidViewSources/android/widget/FrameLayout.kt +61 -0
- package/kotlin/AndroidViewSources/android/widget/LinearLayout.kt +49 -0
- package/kotlin/AppSources/android/view/Window.kt +36 -0
- package/kotlin/AppSources/android/view/WindowInsets.kt +9 -0
- package/kotlin/AppSources/android/view/WindowInsetsController.kt +5 -0
- package/kotlin/AppSources/android/window/OnBackInvokedDispatcher.kt +48 -0
- package/kotlin/AppSources/androidx/activity/OnBackPressedDispatcher.kt +20 -5
- package/kotlin/AppSources/androidx/compose/foundation/layout/WindowInsets.kt +15 -0
- package/kotlin/AppSources/androidx/compose/ui/platform/ComposeView.kt +2 -1
- package/kotlin/AppSources/dev/peach/app/PeachApp.kt +42 -50
- package/kotlin/ComposeSources/PeachComposeHost.kt +200 -84
- package/kotlin/ExpoComposeSources/PeachAndroidView.kt +87 -0
- package/kotlin/ExpoComposeSources/PeachExpoViewMount.kt +24 -0
- package/kotlin/ExpoComposeSources/dev/peach/expo/PeachExpoCompose.kt +28 -0
- package/kotlin/ExpoComposeSources/expo/modules/kotlin/views/ExpoComposeView.kt +70 -0
- package/kotlin/ExpoSources/dev/peach/expo/PeachExpoRuntime.kt +513 -0
- package/kotlin/ExpoSources/dev/peach/expo/PeachExpoTypes.kt +173 -0
- package/kotlin/ExpoSources/expo/modules/kotlin/AppContext.kt +20 -0
- package/kotlin/ExpoSources/expo/modules/kotlin/Promise.kt +19 -0
- package/kotlin/ExpoSources/expo/modules/kotlin/classcomponent/ClassComponentBuilder.kt +198 -0
- package/kotlin/ExpoSources/expo/modules/kotlin/exception/CodedException.kt +126 -0
- package/kotlin/ExpoSources/expo/modules/kotlin/functions/Functions.kt +132 -0
- package/kotlin/ExpoSources/expo/modules/kotlin/jni/JavaScriptTypedArray.kt +81 -0
- package/kotlin/ExpoSources/expo/modules/kotlin/modules/Module.kt +98 -0
- package/kotlin/ExpoSources/expo/modules/kotlin/objects/ObjectDefinitionBuilder.kt +236 -0
- package/kotlin/ExpoSources/expo/modules/kotlin/records/Record.kt +12 -0
- package/kotlin/ExpoSources/expo/modules/kotlin/runtime/Runtime.kt +9 -0
- package/kotlin/ExpoSources/expo/modules/kotlin/sharedobjects/SharedObject.kt +50 -0
- package/kotlin/ExpoSources/expo/modules/kotlin/sharedobjects/SharedObjectRegistry.kt +57 -0
- package/kotlin/ExpoSources/expo/modules/kotlin/sharedobjects/SharedRef.kt +19 -0
- package/kotlin/ExpoSources/expo/modules/kotlin/typedarray/ConcreteTypedArrays.kt +158 -0
- package/kotlin/ExpoSources/expo/modules/kotlin/typedarray/GenericTypedArray.kt +8 -0
- package/kotlin/ExpoSources/expo/modules/kotlin/typedarray/TypedArray.kt +29 -0
- package/kotlin/ExpoSources/expo/modules/kotlin/typedarray/TypedArrayIterator.kt +9 -0
- package/kotlin/ExpoSources/expo/modules/kotlin/types/AnyType.kt +155 -0
- package/kotlin/ExpoSources/expo/modules/kotlin/types/Either.kt +91 -0
- package/kotlin/ExpoSources/expo/modules/kotlin/types/Enumerable.kt +6 -0
- package/kotlin/ExpoSources/expo/modules/kotlin/viewevent/ViewEvent.kt +54 -0
- package/kotlin/ExpoSources/expo/modules/kotlin/views/ExpoView.kt +23 -0
- package/kotlin/ExpoSources/expo/modules/kotlin/views/ViewDefinitionBuilder.kt +71 -0
- package/kotlin/ExpoSources/expo/modules/kotlin/views/ViewProps.kt +35 -0
- package/kotlin/README.md +13 -3
- package/kotlin/Sources/android/os/Build.kt +17 -0
- package/kotlin/Sources/android/os/Handler.kt +35 -0
- package/kotlin/Sources/android/os/Looper.kt +17 -0
- package/kotlin/Sources/android/os/MessageQueue.kt +56 -0
- package/kotlin/Sources/android/os/SystemClock.kt +14 -0
- package/kotlin/Sources/android/util/Log.kt +64 -0
- package/kotlin/Sources/java/security/MessageDigest.kt +114 -0
- package/kotlin/Sources/kotlin/text/StringFormat.kt +9 -0
- package/kotlin/Sources/org/json/JSON.kt +44 -0
- package/kotlin/fixtures/counter/Counter.d.kt.ts +1 -0
- package/kotlin/fixtures/counter/Counter.kt +10 -3
- package/kotlin/fixtures/counter/CounterApp.native.tsx +1 -1
- package/kotlin/fixtures/counter/vite.config.ts +2 -2
- package/kotlin/fixtures/headless-counter/Headless.kt +7 -6
- package/kotlin/fixtures/import-animation/Animation.d.kt.ts +7 -0
- package/kotlin/fixtures/import-animation/Animation.kt +80 -0
- package/kotlin/fixtures/import-animation/ImportAnimation.native.tsx +13 -0
- package/kotlin/fixtures/import-animation/README.md +24 -0
- package/kotlin/fixtures/import-animation/app/_layout.tsx +4 -0
- package/kotlin/fixtures/import-animation/app/index+spa.tsx +1 -0
- package/kotlin/fixtures/import-animation/app/routes.d.ts +18 -0
- package/kotlin/fixtures/import-animation/package.json +23 -0
- package/kotlin/fixtures/import-animation/tsconfig.json +20 -0
- package/kotlin/fixtures/import-animation/vite.config.ts +20 -0
- package/kotlin/fixtures/import-editor/Editor.d.kt.ts +7 -0
- package/kotlin/fixtures/import-editor/Editor.kt +59 -0
- package/kotlin/fixtures/import-editor/ImportEditor.native.tsx +23 -0
- package/kotlin/fixtures/import-editor/README.md +27 -0
- package/kotlin/fixtures/import-editor/app/_layout.tsx +4 -0
- package/kotlin/fixtures/import-editor/app/index+spa.tsx +1 -0
- package/kotlin/fixtures/import-editor/package.json +23 -0
- package/kotlin/fixtures/import-editor/tsconfig.json +20 -0
- package/kotlin/fixtures/import-editor/vite.config.ts +20 -0
- package/kotlin/fixtures/import-effects/Effects.d.kt.ts +7 -0
- package/kotlin/fixtures/import-effects/Effects.kt +87 -0
- package/kotlin/fixtures/import-effects/ImportEffects.native.tsx +13 -0
- package/kotlin/fixtures/import-effects/README.md +19 -0
- package/kotlin/fixtures/import-effects/app/_layout.tsx +4 -0
- package/kotlin/fixtures/import-effects/app/index+spa.tsx +1 -0
- package/kotlin/fixtures/import-effects/package.json +23 -0
- package/kotlin/fixtures/import-effects/tsconfig.json +20 -0
- package/kotlin/fixtures/import-effects/vite.config.ts +20 -0
- package/kotlin/fixtures/import-layout/ImportLayout.native.tsx +13 -0
- package/kotlin/fixtures/import-layout/Layout.d.kt.ts +7 -0
- package/kotlin/fixtures/import-layout/Layout.kt +72 -0
- package/kotlin/fixtures/import-layout/README.md +24 -0
- package/kotlin/fixtures/import-layout/app/_layout.tsx +4 -0
- package/kotlin/fixtures/import-layout/app/index+spa.tsx +1 -0
- package/kotlin/fixtures/import-layout/package.json +23 -0
- package/kotlin/fixtures/import-layout/tsconfig.json +20 -0
- package/kotlin/fixtures/import-layout/vite.config.ts +20 -0
- package/kotlin/fixtures/import-navigation/ImportNavigation.native.tsx +18 -0
- package/kotlin/fixtures/import-navigation/Navigation.d.kt.ts +7 -0
- package/kotlin/fixtures/import-navigation/Navigation.kt +104 -0
- package/kotlin/fixtures/import-navigation/README.md +31 -0
- package/kotlin/fixtures/import-navigation/app/_layout.tsx +4 -0
- package/kotlin/fixtures/import-navigation/app/index+spa.tsx +1 -0
- package/kotlin/fixtures/import-navigation/package.json +23 -0
- package/kotlin/fixtures/import-navigation/tsconfig.json +20 -0
- package/kotlin/fixtures/import-navigation/vite.config.ts +20 -0
- package/kotlin/fixtures/import-overlays/ImportOverlays.native.tsx +29 -0
- package/kotlin/fixtures/import-overlays/Overlays.d.kt.ts +7 -0
- package/kotlin/fixtures/import-overlays/Overlays.kt +93 -0
- package/kotlin/fixtures/import-overlays/README.md +34 -0
- package/kotlin/fixtures/import-overlays/app/_layout.tsx +4 -0
- package/kotlin/fixtures/import-overlays/app/index+spa.tsx +1 -0
- package/kotlin/fixtures/import-overlays/package.json +23 -0
- package/kotlin/fixtures/import-overlays/tsconfig.json +20 -0
- package/kotlin/fixtures/import-overlays/vite.config.ts +20 -0
- package/kotlin/fixtures/import-scroll/ImportScroll.native.tsx +13 -0
- package/kotlin/fixtures/import-scroll/README.md +23 -0
- package/kotlin/fixtures/import-scroll/Scroll.d.kt.ts +7 -0
- package/kotlin/fixtures/import-scroll/Scroll.kt +91 -0
- package/kotlin/fixtures/import-scroll/app/_layout.tsx +4 -0
- package/kotlin/fixtures/import-scroll/app/index+spa.tsx +1 -0
- package/kotlin/fixtures/import-scroll/package.json +23 -0
- package/kotlin/fixtures/import-scroll/tsconfig.json +20 -0
- package/kotlin/fixtures/import-scroll/vite.config.ts +20 -0
- package/kotlin/fixtures/import-selection/ImportSelection.native.tsx +44 -0
- package/kotlin/fixtures/import-selection/README.md +32 -0
- package/kotlin/fixtures/import-selection/Selection.d.kt.ts +8 -0
- package/kotlin/fixtures/import-selection/Selection.kt +78 -0
- package/kotlin/fixtures/import-selection/app/_layout.tsx +4 -0
- package/kotlin/fixtures/import-selection/app/index+spa.tsx +1 -0
- package/kotlin/fixtures/import-selection/package.json +23 -0
- package/kotlin/fixtures/import-selection/tsconfig.json +20 -0
- package/kotlin/fixtures/import-selection/vite.config.ts +20 -0
- package/kotlin/fixtures/import-surfaces/ImportSurfaces.native.tsx +13 -0
- package/kotlin/fixtures/import-surfaces/README.md +22 -0
- package/kotlin/fixtures/import-surfaces/Surfaces.d.kt.ts +7 -0
- package/kotlin/fixtures/import-surfaces/Surfaces.kt +64 -0
- package/kotlin/fixtures/import-surfaces/app/_layout.tsx +4 -0
- package/kotlin/fixtures/import-surfaces/app/index+spa.tsx +1 -0
- package/kotlin/fixtures/import-surfaces/package.json +23 -0
- package/kotlin/fixtures/import-surfaces/tsconfig.json +20 -0
- package/kotlin/fixtures/import-surfaces/vite.config.ts +20 -0
- package/kotlin/fixtures/import-values/ImportValues.native.tsx +35 -0
- package/kotlin/fixtures/import-values/README.md +23 -0
- package/kotlin/fixtures/import-values/Values.d.kt.ts +11 -0
- package/kotlin/fixtures/import-values/Values.kt +66 -0
- package/kotlin/fixtures/import-values/app/_layout.tsx +4 -0
- package/kotlin/fixtures/import-values/app/index+spa.tsx +1 -0
- package/kotlin/fixtures/import-values/package.json +23 -0
- package/kotlin/fixtures/import-values/tsconfig.json +20 -0
- package/kotlin/fixtures/import-values/vite.config.ts +20 -0
- package/kotlin/fixtures/kotlin-app-variants/BarBehavior.kt +38 -0
- package/kotlin/fixtures/kotlin-app-variants/BarVisibility.kt +36 -0
- package/kotlin/fixtures/kotlin-app-variants/Ime.kt +3 -1
- package/kotlin/fixtures/kotlin-app-variants/TransientInsets.kt +50 -0
- package/kotlin/fork/NOTICE +6 -1
- package/kotlin/fork/README.md +15 -7
- package/kotlin/fork/build.sh +2 -0
- package/kotlin/fork/compose-worker-globals.patch +186 -4
- package/kotlin/fork/fork.json +2 -2
- package/kotlin/fork/skiko-worker-clock.patch +12 -0
- package/kotlin/toolchain.json +12 -12
- package/kotlin/tools/expo-types.main.kts +115 -0
- package/package.json +17 -23
- package/scripts/dev-server-scanner.ts +92 -66
- package/skills/contrast/SKILL.md +2 -2
- package/skills/peach-debug/SKILL.md +1 -1
- package/skills/peach-perf/SKILL.md +1 -1
- package/skills/peach-setup/SKILL.md +2 -1
- package/skills/peach-test/SKILL.md +8 -0
- package/src/assetCatalogContract.ts +43 -1
- package/src/bridge-contract-input.ts +46 -19
- package/src/bridge-contract.ts +0 -1
- package/src/bundle-channel.mjs +197 -84
- package/src/bundleGlobalFooter.ts +38 -0
- package/src/capture-contract.ts +107 -33
- package/src/cloud-contract.ts +5 -1
- package/src/cloud.ts +5 -2
- package/src/config.ts +22 -3
- package/src/configured-bundle-producer.ts +1 -1
- package/src/desktop-host-delivery.ts +95 -1
- package/src/expo-kotlin-bundle.ts +29 -0
- package/src/expo-kotlin-host.ts +201 -0
- package/src/expo-swift-host.ts +96 -76
- package/src/expoKotlinContract.ts +70 -0
- package/src/expoSwiftContract.ts +4 -25
- package/src/home-paths.ts +7 -4
- package/src/host/bridge-host.ts +23 -3
- package/src/host/fetch-proxy-handler.ts +13 -0
- package/src/host/fetch-proxy-overrides.ts +11 -0
- package/src/host/replacement-module-handler.ts +5 -2
- package/src/index.ts +3 -1
- package/src/kotlin-host.ts +463 -19
- package/src/metro-plugin.ts +56 -28
- package/src/metro-production-bundle.ts +2 -2
- package/src/native-app-identity-project.ts +190 -0
- package/src/native-app-identity.ts +398 -34
- package/src/native-firebase-config.ts +69 -15
- package/src/native-seam-manifest.ts +6 -0
- package/src/one-widget-discovery.ts +2 -1
- package/src/openstep-plist.ts +107 -0
- package/src/react-native-host-modules.ts +4 -0
- package/src/runtime-assets.ts +2 -2
- package/src/sim-client.ts +2 -2
- package/src/swift-binary-modules.ts +3 -1
- package/src/swift-compile.ts +286 -38
- package/src/swift-distribution-contract.ts +4 -0
- package/src/swift-distribution.lock.json +123 -12
- package/src/swift-distribution.ts +7 -3
- package/src/swift-host.ts +298 -83
- package/src/swift-package-manifest.ts +29 -2
- package/src/swift-package.ts +180 -10
- package/src/swift-packages.ts +4 -0
- package/src/swift-standin-emit.ts +4 -1
- package/src/swift-standins.ts +274 -15
- package/src/swift-submit.ts +62 -7
- package/src/swift-toolchain.ts +6 -1
- package/src/swiftContract.ts +126 -3
- package/src/vite-plugin-one.ts +13 -16
- package/src/vite-plugin-swift.ts +4 -5
- package/src/vite-plugin.ts +360 -144
- package/src/vite.d.ts +1 -1
- package/src/worker-system-chunks.ts +221 -0
- package/src/worklet-executable.ts +348 -38
- package/src/worklets-babel.ts +1 -1
- package/src/xcode-project.ts +243 -127
- package/dist-lib/agent-identity.cjs +0 -39
- package/dist-lib/agent-sessions.cjs +0 -760
- package/dist-lib/cli-constants.cjs +0 -32
- package/dist-lib/render-mode.cjs +0 -44
- package/kotlin/AppSources/android/view/View.kt +0 -74
- package/kotlin/AppSources/android/widget/FrameLayout.kt +0 -6
- package/src/render-mode.ts +0 -15
- /package/kotlin/{AppSources → AndroidViewSources}/android/content/Context.kt +0 -0
- /package/kotlin/{AppSources → AndroidViewSources}/android/content/res/Configuration.kt +0 -0
- /package/kotlin/{AppSources → AndroidViewSources}/android/graphics/Color.kt +0 -0
- /package/kotlin/{AppSources → AndroidViewSources}/android/util/DisplayMetrics.kt +0 -0
- /package/kotlin/{AppSources → AndroidViewSources}/dev/peach/app/JavaFormat.kt +0 -0
- /package/kotlin/{AppSources → Sources}/java/lang/Runnable.kt +0 -0
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
// peach maestro compare <flow-or-dir>: run a partner's existing maestro flows
|
|
2
|
+
// on their own iOS simulator under real maestro, then on peach, and write one
|
|
3
|
+
// html report pairing every screen (iOS, peach, pixel diff), both sides'
|
|
4
|
+
// errors and per-step timing. maestro-compare.ts holds the report pieces.
|
|
5
|
+
|
|
6
|
+
import { spawn, spawnSync } from 'child_process'
|
|
7
|
+
import * as fs from 'fs'
|
|
8
|
+
import * as os from 'os'
|
|
9
|
+
import * as path from 'path'
|
|
10
|
+
import { devices } from 'peach-engine/settings/devices'
|
|
11
|
+
import { rnxHomeDir } from '../../src/home-paths'
|
|
12
|
+
import { parseFlowFile } from '../flow-file'
|
|
13
|
+
import {
|
|
14
|
+
buildCompareHtml,
|
|
15
|
+
compareScreens,
|
|
16
|
+
comparePassed,
|
|
17
|
+
instrumentFlowSource,
|
|
18
|
+
nativeStepsFromCommandsJson,
|
|
19
|
+
peachStepsFromCommandsJson,
|
|
20
|
+
screensOverThreshold,
|
|
21
|
+
type CompareCapture,
|
|
22
|
+
type CompareFlow,
|
|
23
|
+
type CompareReport,
|
|
24
|
+
type CompareSide,
|
|
25
|
+
} from '../maestro-compare'
|
|
26
|
+
import { collectFlowFiles, loadMaestroWorkspaceConfig } from '../maestro-workspace'
|
|
27
|
+
import { FLOW_ARG_VALUE_FLAGS } from './flow'
|
|
28
|
+
import { applyEnvFlags, runMaestro, type RunMaestroOptions } from './maestro'
|
|
29
|
+
|
|
30
|
+
function printHelp() {
|
|
31
|
+
console.log(`
|
|
32
|
+
peach maestro compare — run your maestro flows on your iOS simulator and on
|
|
33
|
+
peach, then write an html report: every screen side by side with a pixel
|
|
34
|
+
diff, both sides' errors, and per-step timing.
|
|
35
|
+
|
|
36
|
+
usage:
|
|
37
|
+
peach maestro compare <flow-or-dir> [options]
|
|
38
|
+
|
|
39
|
+
needs real maestro (https://maestro.dev) and a booted iOS simulator with your
|
|
40
|
+
app installed. peach runs the same flows against the app it has open.
|
|
41
|
+
|
|
42
|
+
options:
|
|
43
|
+
--udid <id> iOS simulator to run maestro on (default: the one booted)
|
|
44
|
+
--out <dir> report directory (default ~/.peach/compare/<time>)
|
|
45
|
+
--threshold <percent> flag screens differing by more than this (default 0.5)
|
|
46
|
+
--maestro <path> maestro binary (default: maestro on PATH, then ~/.maestro/bin)
|
|
47
|
+
--env KEY=VALUE passed to both runs (repeatable)
|
|
48
|
+
--include-tags, --exclude-tags, --config
|
|
49
|
+
flow selection, passed to both runs
|
|
50
|
+
other flags (--sim, --device, --headless, ...) go to the peach run
|
|
51
|
+
|
|
52
|
+
exits 0 when both sides pass every flow and no screen is over the threshold.
|
|
53
|
+
`)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function takeFlag(args: string[], name: string): string | undefined {
|
|
57
|
+
const i = args.indexOf(name)
|
|
58
|
+
if (i < 0) return undefined
|
|
59
|
+
const value = args[i + 1]
|
|
60
|
+
if (!value || value.startsWith('-')) throw new Error(`${name} expects a value`)
|
|
61
|
+
args.splice(i, 2)
|
|
62
|
+
return value
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function findMaestro(explicit: string | undefined): string | null {
|
|
66
|
+
if (explicit) return fs.existsSync(explicit) ? explicit : null
|
|
67
|
+
const which = spawnSync('which', ['maestro'], { encoding: 'utf8' })
|
|
68
|
+
if (which.status === 0 && which.stdout.trim()) return which.stdout.trim()
|
|
69
|
+
const home = path.join(os.homedir(), '.maestro', 'bin', 'maestro')
|
|
70
|
+
return fs.existsSync(home) ? home : null
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function bootedSimulators(): Array<{ udid: string; name: string }> {
|
|
74
|
+
const out = spawnSync('xcrun', ['simctl', 'list', 'devices', 'booted', '-j'], {
|
|
75
|
+
encoding: 'utf8',
|
|
76
|
+
})
|
|
77
|
+
if (out.status !== 0) return []
|
|
78
|
+
const devices = JSON.parse(out.stdout).devices as Record<
|
|
79
|
+
string,
|
|
80
|
+
Array<{ udid: string; name: string; state: string }>
|
|
81
|
+
>
|
|
82
|
+
return Object.values(devices)
|
|
83
|
+
.flat()
|
|
84
|
+
.filter((d) => d.state === 'Booted')
|
|
85
|
+
.map(({ udid, name }) => ({ udid, name }))
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function simulatorName(udid: string): string {
|
|
89
|
+
return bootedSimulators().find((d) => d.udid === udid)?.name ?? udid
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function appInstalled(udid: string, appId: string): boolean {
|
|
93
|
+
return (
|
|
94
|
+
spawnSync('xcrun', ['simctl', 'get_app_container', udid, appId], { stdio: 'ignore' })
|
|
95
|
+
.status === 0
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// peach draws the iOS marketing status bar (9:41, full signal and battery);
|
|
100
|
+
// the simulator shows the real clock unless told otherwise.
|
|
101
|
+
function overrideStatusBar(udid: string) {
|
|
102
|
+
spawnSync(
|
|
103
|
+
'xcrun',
|
|
104
|
+
[
|
|
105
|
+
'simctl',
|
|
106
|
+
'status_bar',
|
|
107
|
+
udid,
|
|
108
|
+
'override',
|
|
109
|
+
'--time',
|
|
110
|
+
'9:41',
|
|
111
|
+
'--dataNetwork',
|
|
112
|
+
'wifi',
|
|
113
|
+
'--wifiMode',
|
|
114
|
+
'active',
|
|
115
|
+
'--wifiBars',
|
|
116
|
+
'3',
|
|
117
|
+
'--cellularMode',
|
|
118
|
+
'active',
|
|
119
|
+
'--cellularBars',
|
|
120
|
+
'4',
|
|
121
|
+
'--batteryState',
|
|
122
|
+
'charged',
|
|
123
|
+
'--batteryLevel',
|
|
124
|
+
'100',
|
|
125
|
+
],
|
|
126
|
+
{ stdio: 'ignore' },
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function runLogged(bin: string, args: string[], logPath: string): Promise<number> {
|
|
131
|
+
return new Promise((resolve) => {
|
|
132
|
+
const log = fs.createWriteStream(logPath)
|
|
133
|
+
const child = spawn(bin, args, { stdio: ['ignore', 'pipe', 'pipe'] })
|
|
134
|
+
const forward = (chunk: Buffer) => {
|
|
135
|
+
log.write(chunk)
|
|
136
|
+
for (const line of chunk.toString().split('\n')) {
|
|
137
|
+
if (line.trim()) console.log(` ios │ ${line}`)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
child.stdout.on('data', forward)
|
|
141
|
+
child.stderr.on('data', forward)
|
|
142
|
+
child.on('close', (code) => {
|
|
143
|
+
log.end()
|
|
144
|
+
resolve(code ?? 1)
|
|
145
|
+
})
|
|
146
|
+
child.on('error', (err) => {
|
|
147
|
+
log.end()
|
|
148
|
+
console.error(` error: could not start maestro: ${err.message}`)
|
|
149
|
+
resolve(1)
|
|
150
|
+
})
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// the status bar band of the device whose screen matches the iOS capture's
|
|
155
|
+
// pixel size (peach's own device table); 0 for a size no device has.
|
|
156
|
+
function statusBarPx(png: Buffer): number {
|
|
157
|
+
// png ihdr: width and height are the big-endian words at bytes 16 and 20
|
|
158
|
+
const width = png.readUInt32BE(16)
|
|
159
|
+
const height = png.readUInt32BE(20)
|
|
160
|
+
const spec = Object.values(devices).find(
|
|
161
|
+
(d) =>
|
|
162
|
+
Math.round(d.width * d.scale) === width &&
|
|
163
|
+
Math.round(d.height * d.scale) === height,
|
|
164
|
+
)
|
|
165
|
+
return spec ? spec.statusBarHeight * spec.scale : 0
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// the flow as its author would name it: relative inside the working
|
|
169
|
+
// directory, home-relative or absolute outside it.
|
|
170
|
+
function displayPath(cwd: string, file: string): string {
|
|
171
|
+
const rel = path.relative(cwd, file)
|
|
172
|
+
if (rel && !rel.startsWith('..') && !path.isAbsolute(rel)) return rel
|
|
173
|
+
const home = os.homedir()
|
|
174
|
+
return file.startsWith(home + path.sep) ? `~${file.slice(home.length)}` : file
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function readJson(file: string): unknown {
|
|
178
|
+
try {
|
|
179
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
180
|
+
} catch {
|
|
181
|
+
return null
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function formatConsoleEntry(entry: unknown): string {
|
|
186
|
+
if (typeof entry === 'string') return entry
|
|
187
|
+
if (entry && typeof entry === 'object') {
|
|
188
|
+
const e = entry as Record<string, unknown>
|
|
189
|
+
const text = e.message ?? e.text ?? e.args
|
|
190
|
+
if (typeof text === 'string') return text
|
|
191
|
+
if (Array.isArray(text)) return text.map(String).join(' ')
|
|
192
|
+
}
|
|
193
|
+
return JSON.stringify(entry).slice(0, 2000)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function formatFailedRequest(entry: unknown): string {
|
|
197
|
+
if (!entry || typeof entry !== 'object') return `request failed: ${String(entry)}`
|
|
198
|
+
const e = entry as Record<string, unknown>
|
|
199
|
+
return `request failed: ${e.method ?? 'GET'} ${e.url ?? '?'} ${e.status ?? e.error ?? ''}`.trim()
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function sideFromSteps(
|
|
203
|
+
steps: CompareSide['steps'],
|
|
204
|
+
ran: boolean,
|
|
205
|
+
extraErrors: string[],
|
|
206
|
+
): CompareSide {
|
|
207
|
+
const failed = steps.find((s) => s.status === 'failed')
|
|
208
|
+
return {
|
|
209
|
+
status: !ran ? 'not run' : failed ? 'failed' : 'passed',
|
|
210
|
+
durationMs: steps.reduce((sum, s) => sum + s.durationMs, 0),
|
|
211
|
+
steps,
|
|
212
|
+
errors: [
|
|
213
|
+
...(failed
|
|
214
|
+
? [`step "${failed.label}" failed: ${failed.error ?? 'no message'}`]
|
|
215
|
+
: []),
|
|
216
|
+
...extraErrors,
|
|
217
|
+
],
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// peach names each flow's output dir after the file stem, suffixing repeats
|
|
222
|
+
// in run order (commands/maestro.ts uniqueFlowDir).
|
|
223
|
+
function peachFlowDirs(root: string, files: string[]): string[] {
|
|
224
|
+
const seen = new Map<string, number>()
|
|
225
|
+
return files.map((file) => {
|
|
226
|
+
const slug =
|
|
227
|
+
path
|
|
228
|
+
.basename(file)
|
|
229
|
+
.replace(/\.(ya?ml)$/i, '')
|
|
230
|
+
.replace(/[^A-Za-z0-9._-]+/g, '_') || 'flow'
|
|
231
|
+
const n = (seen.get(slug) ?? 0) + 1
|
|
232
|
+
seen.set(slug, n)
|
|
233
|
+
return path.join(root, n === 1 ? slug : `${slug}-${n}`)
|
|
234
|
+
})
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export async function runMaestroCompare(
|
|
238
|
+
argv: string[],
|
|
239
|
+
opts: RunMaestroOptions = {},
|
|
240
|
+
): Promise<number> {
|
|
241
|
+
if (argv.includes('--help') || argv.includes('-h')) {
|
|
242
|
+
printHelp()
|
|
243
|
+
return 0
|
|
244
|
+
}
|
|
245
|
+
let args: string[]
|
|
246
|
+
const envPairs: string[] = []
|
|
247
|
+
for (let i = 0; i < argv.length; i++) {
|
|
248
|
+
if (argv[i] === '--env' || argv[i] === '-e') envPairs.push('--env', argv[i + 1] ?? '')
|
|
249
|
+
else if (argv[i]!.startsWith('--env=')) envPairs.push('--env', argv[i]!.slice(6))
|
|
250
|
+
}
|
|
251
|
+
try {
|
|
252
|
+
// sets process.env for the peach run and drops the flags from its argv
|
|
253
|
+
args = applyEnvFlags(argv)
|
|
254
|
+
} catch (err) {
|
|
255
|
+
console.error(` error: ${(err as Error).message}`)
|
|
256
|
+
return 1
|
|
257
|
+
}
|
|
258
|
+
let udid: string | undefined
|
|
259
|
+
let outFlag: string | undefined
|
|
260
|
+
let thresholdFlag: string | undefined
|
|
261
|
+
let maestroFlag: string | undefined
|
|
262
|
+
const shared: string[] = []
|
|
263
|
+
try {
|
|
264
|
+
udid = takeFlag(args, '--udid')
|
|
265
|
+
outFlag = takeFlag(args, '--out')
|
|
266
|
+
thresholdFlag = takeFlag(args, '--threshold')
|
|
267
|
+
maestroFlag = takeFlag(args, '--maestro')
|
|
268
|
+
for (const name of ['--include-tags', '--exclude-tags', '--config']) {
|
|
269
|
+
const value = takeFlag(args, name)
|
|
270
|
+
if (value) shared.push(name, value)
|
|
271
|
+
}
|
|
272
|
+
} catch (err) {
|
|
273
|
+
console.error(` error: ${(err as Error).message}`)
|
|
274
|
+
return 1
|
|
275
|
+
}
|
|
276
|
+
const threshold = thresholdFlag === undefined ? 0.5 : Number(thresholdFlag)
|
|
277
|
+
if (!Number.isFinite(threshold) || threshold < 0) {
|
|
278
|
+
console.error(' error: --threshold expects a percentage, for example 0.5')
|
|
279
|
+
return 1
|
|
280
|
+
}
|
|
281
|
+
const positional = args.findIndex(
|
|
282
|
+
(a, i) =>
|
|
283
|
+
!a.startsWith('-') && !(i > 0 && FLOW_ARG_VALUE_FLAGS.includes(args[i - 1]!)),
|
|
284
|
+
)
|
|
285
|
+
if (positional < 0) {
|
|
286
|
+
console.error(' error: pass the flow file or directory to compare')
|
|
287
|
+
printHelp()
|
|
288
|
+
return 1
|
|
289
|
+
}
|
|
290
|
+
const cwd = process.cwd()
|
|
291
|
+
const target = path.resolve(cwd, args[positional]!)
|
|
292
|
+
const peachArgs = args.filter((_, i) => i !== positional)
|
|
293
|
+
if (!fs.existsSync(target)) {
|
|
294
|
+
console.error(` error: ${target} not found`)
|
|
295
|
+
return 1
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const maestro = findMaestro(maestroFlag)
|
|
299
|
+
if (!maestro) {
|
|
300
|
+
console.error(` error: maestro not found. install it with:
|
|
301
|
+
curl -fsSL "https://get.maestro.mobile.dev" | bash
|
|
302
|
+
or pass --maestro <path>`)
|
|
303
|
+
return 1
|
|
304
|
+
}
|
|
305
|
+
if (!udid) {
|
|
306
|
+
const booted = bootedSimulators()
|
|
307
|
+
if (booted.length !== 1) {
|
|
308
|
+
console.error(
|
|
309
|
+
booted.length === 0
|
|
310
|
+
? ' error: no iOS simulator is booted. boot the one with your app installed, or pass --udid'
|
|
311
|
+
: ` error: ${booted.length} simulators are booted; pass --udid with one of:\n${booted.map((d) => ` ${d.udid} ${d.name}`).join('\n')}`,
|
|
312
|
+
)
|
|
313
|
+
return 1
|
|
314
|
+
}
|
|
315
|
+
udid = booted[0]!.udid
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// the same selection maestro's planner makes, then an instrumented copy of
|
|
319
|
+
// each selected flow at the same relative path.
|
|
320
|
+
const targetIsDir = fs.statSync(target).isDirectory()
|
|
321
|
+
const root = targetIsDir ? target : path.dirname(target)
|
|
322
|
+
let files: string[]
|
|
323
|
+
try {
|
|
324
|
+
const sharedValue = (name: string) => {
|
|
325
|
+
const i = shared.indexOf(name)
|
|
326
|
+
return i >= 0 ? shared[i + 1] : undefined
|
|
327
|
+
}
|
|
328
|
+
const tags = (name: string) =>
|
|
329
|
+
sharedValue(name)
|
|
330
|
+
?.split(',')
|
|
331
|
+
.map((t) => t.trim())
|
|
332
|
+
.filter(Boolean) ?? null
|
|
333
|
+
files = targetIsDir
|
|
334
|
+
? collectFlowFiles(target, {
|
|
335
|
+
configPath: sharedValue('--config'),
|
|
336
|
+
includeTags: tags('--include-tags'),
|
|
337
|
+
excludeTags: tags('--exclude-tags'),
|
|
338
|
+
})
|
|
339
|
+
: [target]
|
|
340
|
+
} catch (err) {
|
|
341
|
+
console.error(` error: ${(err as Error).message}`)
|
|
342
|
+
return 1
|
|
343
|
+
}
|
|
344
|
+
if (files.length === 0) {
|
|
345
|
+
console.error(` error: no flows selected in ${target}`)
|
|
346
|
+
return 1
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-')
|
|
350
|
+
const out = path.resolve(cwd, outFlag ?? path.join(rnxHomeDir(), 'compare', stamp))
|
|
351
|
+
const flowsDir = path.join(out, 'flows')
|
|
352
|
+
fs.mkdirSync(flowsDir, { recursive: true })
|
|
353
|
+
const instrumented: Array<{
|
|
354
|
+
file: string
|
|
355
|
+
copy: string
|
|
356
|
+
name: string
|
|
357
|
+
appId?: string
|
|
358
|
+
captures: CompareCapture[]
|
|
359
|
+
}> = []
|
|
360
|
+
for (const file of files) {
|
|
361
|
+
const source = fs.readFileSync(file, 'utf8')
|
|
362
|
+
let result: ReturnType<typeof instrumentFlowSource>
|
|
363
|
+
try {
|
|
364
|
+
result = instrumentFlowSource(source, path.dirname(file))
|
|
365
|
+
} catch (err) {
|
|
366
|
+
console.error(` error: ${path.relative(cwd, file)}: ${(err as Error).message}`)
|
|
367
|
+
return 1
|
|
368
|
+
}
|
|
369
|
+
const copy = path.join(flowsDir, path.relative(root, file))
|
|
370
|
+
fs.mkdirSync(path.dirname(copy), { recursive: true })
|
|
371
|
+
fs.writeFileSync(copy, result.source)
|
|
372
|
+
const frontmatter = parseFlowFile(source).frontmatter as Record<string, unknown>
|
|
373
|
+
instrumented.push({
|
|
374
|
+
file,
|
|
375
|
+
copy,
|
|
376
|
+
name:
|
|
377
|
+
typeof frontmatter.name === 'string'
|
|
378
|
+
? frontmatter.name
|
|
379
|
+
: path.basename(file).replace(/\.(ya?ml)$/i, ''),
|
|
380
|
+
appId: typeof frontmatter.appId === 'string' ? frontmatter.appId : undefined,
|
|
381
|
+
captures: result.captures,
|
|
382
|
+
})
|
|
383
|
+
}
|
|
384
|
+
if (targetIsDir) {
|
|
385
|
+
const configAt = shared.indexOf('--config')
|
|
386
|
+
const explicitConfig = configAt >= 0 ? shared[configAt + 1] : undefined
|
|
387
|
+
const config = loadMaestroWorkspaceConfig(target, explicitConfig)
|
|
388
|
+
if (config.path && !explicitConfig) {
|
|
389
|
+
fs.copyFileSync(config.path, path.join(flowsDir, path.relative(root, config.path)))
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
const runTarget = targetIsDir ? flowsDir : instrumented[0]!.copy
|
|
393
|
+
|
|
394
|
+
for (const appId of new Set(instrumented.map((f) => f.appId).filter(Boolean))) {
|
|
395
|
+
if (!appInstalled(udid, appId!)) {
|
|
396
|
+
console.error(
|
|
397
|
+
` error: ${appId} is not installed on simulator ${udid}. build and install your app there first (for example \`npx expo run:ios\`).`,
|
|
398
|
+
)
|
|
399
|
+
return 1
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const device = simulatorName(udid)
|
|
404
|
+
console.log(`
|
|
405
|
+
peach maestro compare — ${files.length} flow${files.length === 1 ? '' : 's'}
|
|
406
|
+
iOS: ${device} (${udid})
|
|
407
|
+
report: ${out}
|
|
408
|
+
`)
|
|
409
|
+
|
|
410
|
+
// 1. real maestro on the iOS simulator
|
|
411
|
+
overrideStatusBar(udid)
|
|
412
|
+
const nativeOut = path.join(out, 'ios')
|
|
413
|
+
let nativeExit: number
|
|
414
|
+
try {
|
|
415
|
+
nativeExit = await runLogged(
|
|
416
|
+
maestro,
|
|
417
|
+
[
|
|
418
|
+
'--udid',
|
|
419
|
+
udid,
|
|
420
|
+
'test',
|
|
421
|
+
runTarget,
|
|
422
|
+
'--test-output-dir',
|
|
423
|
+
nativeOut,
|
|
424
|
+
'--no-ansi',
|
|
425
|
+
...shared,
|
|
426
|
+
...envPairs,
|
|
427
|
+
],
|
|
428
|
+
path.join(out, 'ios.log'),
|
|
429
|
+
)
|
|
430
|
+
} finally {
|
|
431
|
+
spawnSync('xcrun', ['simctl', 'status_bar', udid, 'clear'], { stdio: 'ignore' })
|
|
432
|
+
}
|
|
433
|
+
console.log(`\n ios │ maestro exited ${nativeExit}\n`)
|
|
434
|
+
|
|
435
|
+
// 2. peach, same instrumented flows (its --env already applied above)
|
|
436
|
+
const peachOut = path.join(out, 'peach')
|
|
437
|
+
await runMaestro(
|
|
438
|
+
['test', runTarget, '--test-output-dir', peachOut, ...shared, ...peachArgs],
|
|
439
|
+
{
|
|
440
|
+
...opts,
|
|
441
|
+
captureConsole: true,
|
|
442
|
+
},
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
// 3. pair and diff
|
|
446
|
+
const nativeRun = fs.existsSync(nativeOut)
|
|
447
|
+
? fs
|
|
448
|
+
.readdirSync(nativeOut)
|
|
449
|
+
.map((d) => path.join(nativeOut, d))
|
|
450
|
+
.filter((d) => fs.statSync(d).isDirectory())
|
|
451
|
+
.sort()
|
|
452
|
+
.pop()
|
|
453
|
+
: undefined
|
|
454
|
+
const peachDirs = peachFlowDirs(
|
|
455
|
+
peachOut,
|
|
456
|
+
instrumented.map((f) => f.copy),
|
|
457
|
+
)
|
|
458
|
+
const imagesDir = path.join(out, 'images')
|
|
459
|
+
const flows: CompareFlow[] = []
|
|
460
|
+
for (const [i, flow] of instrumented.entries()) {
|
|
461
|
+
const nativeDir = nativeRun ? path.join(nativeRun, flow.name) : null
|
|
462
|
+
const peachDir = peachDirs[i]!
|
|
463
|
+
const nativeRan = Boolean(nativeDir && fs.existsSync(nativeDir))
|
|
464
|
+
const peachRan = fs.existsSync(path.join(peachDir, 'commands.json'))
|
|
465
|
+
const nativeSteps = nativeRan
|
|
466
|
+
? nativeStepsFromCommandsJson(readJson(path.join(nativeDir!, 'commands.json')))
|
|
467
|
+
: []
|
|
468
|
+
const peachSteps = peachRan
|
|
469
|
+
? peachStepsFromCommandsJson(readJson(path.join(peachDir, 'commands.json')))
|
|
470
|
+
: []
|
|
471
|
+
const consoleJson = readJson(path.join(peachDir, 'logs', 'console.json')) as {
|
|
472
|
+
errors?: unknown[]
|
|
473
|
+
requests?: unknown[]
|
|
474
|
+
} | null
|
|
475
|
+
const peachConsole = [
|
|
476
|
+
...[...new Set((consoleJson?.errors ?? []).map(formatConsoleEntry))].map(
|
|
477
|
+
(e) => `console error: ${e}`,
|
|
478
|
+
),
|
|
479
|
+
...(consoleJson?.requests ?? []).map(formatFailedRequest),
|
|
480
|
+
]
|
|
481
|
+
const screens = []
|
|
482
|
+
for (const capture of flow.captures) {
|
|
483
|
+
const nativePng = nativeDir
|
|
484
|
+
? path.join(nativeDir, 'takeScreenshot', `${capture.name}.png`)
|
|
485
|
+
: ''
|
|
486
|
+
const peachPng = path.join(peachDir, 'screenshots', `${capture.name}.png`)
|
|
487
|
+
const hasNative = Boolean(nativePng) && fs.existsSync(nativePng)
|
|
488
|
+
const hasPeach = fs.existsSync(peachPng)
|
|
489
|
+
const imageRel = path.join(
|
|
490
|
+
'images',
|
|
491
|
+
`${i + 1}-${capture.name.split('/').pop()}.png`,
|
|
492
|
+
)
|
|
493
|
+
if (hasNative && hasPeach) {
|
|
494
|
+
const nativeBytes = fs.readFileSync(nativePng)
|
|
495
|
+
const { diff, triptych } = compareScreens(
|
|
496
|
+
nativeBytes,
|
|
497
|
+
fs.readFileSync(peachPng),
|
|
498
|
+
{
|
|
499
|
+
maskTopPx: statusBarPx(nativeBytes),
|
|
500
|
+
},
|
|
501
|
+
)
|
|
502
|
+
fs.mkdirSync(imagesDir, { recursive: true })
|
|
503
|
+
fs.writeFileSync(path.join(out, imageRel), triptych)
|
|
504
|
+
screens.push({ ...capture, image: imageRel, diff })
|
|
505
|
+
} else if (hasNative || hasPeach) {
|
|
506
|
+
const only = path.relative(out, hasNative ? nativePng : peachPng)
|
|
507
|
+
screens.push({
|
|
508
|
+
...capture,
|
|
509
|
+
...(hasNative ? { nativeOnly: only } : { peachOnly: only }),
|
|
510
|
+
})
|
|
511
|
+
} else {
|
|
512
|
+
screens.push({ ...capture })
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
flows.push({
|
|
516
|
+
name: flow.name,
|
|
517
|
+
file: displayPath(cwd, flow.file),
|
|
518
|
+
native: sideFromSteps(
|
|
519
|
+
nativeSteps,
|
|
520
|
+
nativeRan,
|
|
521
|
+
nativeRan
|
|
522
|
+
? []
|
|
523
|
+
: [
|
|
524
|
+
`maestro produced no output for this flow (exit ${nativeExit}); see ios.log`,
|
|
525
|
+
],
|
|
526
|
+
),
|
|
527
|
+
peach: sideFromSteps(peachSteps, peachRan, peachConsole),
|
|
528
|
+
screens,
|
|
529
|
+
})
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const report: CompareReport = {
|
|
533
|
+
createdAt: new Date().toISOString(),
|
|
534
|
+
device,
|
|
535
|
+
thresholdPercent: threshold,
|
|
536
|
+
flows,
|
|
537
|
+
}
|
|
538
|
+
fs.writeFileSync(path.join(out, 'report.json'), `${JSON.stringify(report, null, 2)}\n`)
|
|
539
|
+
const htmlPath = path.join(out, 'report.html')
|
|
540
|
+
fs.writeFileSync(htmlPath, buildCompareHtml(report))
|
|
541
|
+
|
|
542
|
+
const over = screensOverThreshold(report)
|
|
543
|
+
const compared = flows.flatMap((f) => f.screens).filter((s) => s.diff).length
|
|
544
|
+
console.log(`
|
|
545
|
+
compare summary:`)
|
|
546
|
+
for (const flow of flows) {
|
|
547
|
+
console.log(
|
|
548
|
+
` ${flow.name}: iOS ${flow.native.status}, Peach ${flow.peach.status}, ${flow.screens.filter((s) => s.diff).length}/${flow.screens.length} screens paired`,
|
|
549
|
+
)
|
|
550
|
+
}
|
|
551
|
+
console.log(` ${compared} screens compared, ${over.length} over ${threshold}%`)
|
|
552
|
+
console.log(` report: ${htmlPath}\n`)
|
|
553
|
+
return comparePassed(report) ? 0 : 1
|
|
554
|
+
}
|
|
@@ -222,9 +222,12 @@ function parseObservedSelector(
|
|
|
222
222
|
throw new Error(`generated selector was never observed: ${JSON.stringify(raw)}`)
|
|
223
223
|
}
|
|
224
224
|
const selector: ObservedSelector & { timeout?: number } = {}
|
|
225
|
-
// id selectors are regexes
|
|
226
|
-
|
|
227
|
-
|
|
225
|
+
// maestro id and text selectors are regexes; generated observations are literals.
|
|
226
|
+
for (const key of ['id', 'text'] satisfies Array<'id' | 'text'>) {
|
|
227
|
+
const literal = raw[key]
|
|
228
|
+
if (typeof literal === 'string')
|
|
229
|
+
selector[key] = literal.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
230
|
+
}
|
|
228
231
|
if (raw.index !== undefined) {
|
|
229
232
|
if (
|
|
230
233
|
typeof raw.index !== 'number' ||
|
package/cli/commands/maestro.ts
CHANGED
|
@@ -46,6 +46,9 @@ import { resolveDefaultUploadOrigin } from './upload'
|
|
|
46
46
|
export interface RunMaestroOptions {
|
|
47
47
|
port?: number
|
|
48
48
|
verbose?: boolean
|
|
49
|
+
// write each flow's app console to <test-output>/<flow>/logs/console.json
|
|
50
|
+
// (maestro compare reads it for the errors Peach saw)
|
|
51
|
+
captureConsole?: boolean
|
|
49
52
|
}
|
|
50
53
|
|
|
51
54
|
const MAESTRO_DIRS = ['.maestro', 'maestro']
|
|
@@ -59,6 +62,8 @@ usage:
|
|
|
59
62
|
peach maestro test <flow-or-dir> # mirrors "maestro test"
|
|
60
63
|
peach maestro load <flow> # load flow into runner without running (--dry-run)
|
|
61
64
|
peach maestro test .maestro/ # workspace globs, root flows by default
|
|
65
|
+
peach maestro compare <flow-or-dir> # run on your iOS simulator and on peach,
|
|
66
|
+
# html report: screens, pixel diff, errors, timing
|
|
62
67
|
peach maestro generate "<goal>" # generate, run, and upload a Maestro flow
|
|
63
68
|
peach maestro start|keep|end # author a flow from live CLI actions
|
|
64
69
|
peach maestro validate <flow> # validate a Maestro YAML file
|
|
@@ -307,6 +312,11 @@ export async function runMaestro(
|
|
|
307
312
|
if (sub === 'init') {
|
|
308
313
|
return runInit(cwd)
|
|
309
314
|
}
|
|
315
|
+
if (sub === 'compare') {
|
|
316
|
+
const { runMaestroCompare } = await import('./maestro-compare')
|
|
317
|
+
// compare forwards --env to real maestro too, so it reads the raw argv
|
|
318
|
+
return runMaestroCompare(hoistLeadingSimFlag([...args]).slice(1), opts)
|
|
319
|
+
}
|
|
310
320
|
if (sub === 'generate') {
|
|
311
321
|
const { runMaestroGenerate } = await import('./maestro-generate')
|
|
312
322
|
return runMaestroGenerate(remaining.slice(1))
|
|
@@ -544,10 +554,22 @@ export async function runMaestro(
|
|
|
544
554
|
let exit = 0
|
|
545
555
|
const startedAt = Date.now()
|
|
546
556
|
try {
|
|
557
|
+
const consoleDir =
|
|
558
|
+
opts.captureConsole && flowTestDir ? path.join(flowTestDir, 'logs') : null
|
|
547
559
|
exit = await runFlowPlayback(playbackArgs, {
|
|
548
560
|
beforeSteps: async () => {
|
|
549
561
|
console.log(`\n ▶ ${path.relative(cwd, file)}`)
|
|
550
562
|
},
|
|
563
|
+
...(consoleDir
|
|
564
|
+
? {
|
|
565
|
+
afterSteps: async (driver) => {
|
|
566
|
+
fs.writeFileSync(
|
|
567
|
+
path.join(consoleDir, 'console.json'),
|
|
568
|
+
`${JSON.stringify(await driver.readConsole(), null, 2)}\n`,
|
|
569
|
+
)
|
|
570
|
+
},
|
|
571
|
+
}
|
|
572
|
+
: {}),
|
|
551
573
|
})
|
|
552
574
|
} catch (err) {
|
|
553
575
|
if (err instanceof DisconnectedSimError) {
|
package/cli/commands/mode.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { peachExit } from '../run-rnx'
|
|
2
2
|
import { runScreenshotMode } from './screenshot-mode'
|
|
3
3
|
import { runShowTouchesMode } from './show-touches-mode'
|
|
4
4
|
import { runThreeMode } from './three-mode'
|
|
@@ -44,7 +44,7 @@ export async function runMode(args: string[], opts: ModeOptions): Promise<void>
|
|
|
44
44
|
if (!sub) {
|
|
45
45
|
printHelp()
|
|
46
46
|
if (!args.includes('--help') && !args.includes('-h')) {
|
|
47
|
-
|
|
47
|
+
peachExit(1)
|
|
48
48
|
}
|
|
49
49
|
return
|
|
50
50
|
}
|
|
@@ -3,10 +3,14 @@
|
|
|
3
3
|
import type { BridgeSimInfo } from '../ws-bridge'
|
|
4
4
|
|
|
5
5
|
export function printMissingCompanionHint(native = false) {
|
|
6
|
-
console.error(
|
|
6
|
+
console.error(
|
|
7
|
+
native
|
|
8
|
+
? ' no native peach desktop host found.'
|
|
9
|
+
: ' no peach desktop companion found.',
|
|
10
|
+
)
|
|
7
11
|
console.error('')
|
|
8
12
|
console.error(' install the desktop app:')
|
|
9
|
-
console.error(
|
|
13
|
+
console.error(' peach desktop install')
|
|
10
14
|
console.error('')
|
|
11
15
|
console.error(' or install the recommended background service:')
|
|
12
16
|
console.error(' peach daemon install')
|