peachsim 0.1.1594 → 0.1.1619
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 +34 -11
- package/cli/app-catalog.ts +7 -5
- package/cli/app-config.ts +6 -2
- package/cli/app-fonts.ts +74 -35
- package/cli/app-stubs.ts +173 -42
- package/cli/bin.ts +9 -0
- package/cli/bridge-flow-runner.ts +189 -84
- package/cli/cloud-client.ts +26 -25
- package/cli/commands/agent-indicator-mode.ts +39 -0
- package/cli/commands/box.ts +13 -3
- package/cli/commands/control.ts +132 -108
- package/cli/commands/daemon.ts +99 -24
- package/cli/commands/debug.ts +29 -1
- package/cli/commands/detect.ts +1 -1
- package/cli/commands/device.ts +6 -8
- package/cli/commands/diagnose.ts +2 -0
- package/cli/commands/doctor.ts +97 -34
- package/cli/commands/flow.ts +22 -12
- package/cli/commands/inspect/actions.ts +2 -0
- package/cli/commands/inspect/core.ts +106 -40
- package/cli/commands/inspect/list.ts +9 -3
- package/cli/commands/inspect/memory.ts +9 -0
- package/cli/commands/inspect/settling.ts +167 -49
- package/cli/commands/inspect/shared.ts +53 -12
- package/cli/commands/inspect/wait-event.ts +0 -1
- package/cli/commands/inspect/wait.ts +2 -2
- package/cli/commands/inspect.ts +19 -4
- package/cli/commands/kotlin.ts +5 -7
- package/cli/commands/maestro-compare.ts +25 -2
- package/cli/commands/mode.ts +13 -1
- package/cli/commands/perf-record.ts +296 -0
- package/cli/commands/perf.ts +6 -1
- package/cli/commands/platform.ts +5 -4
- package/cli/commands/preview.ts +1 -1
- package/cli/commands/record.ts +28 -5
- package/cli/commands/runtime.ts +9 -11
- package/cli/commands/screenshots.ts +4 -3
- package/cli/commands/setup.ts +17 -5
- package/cli/commands/skills.ts +16 -43
- package/cli/commands/swift.ts +18 -3
- package/cli/commands/upgrade.ts +7 -6
- package/cli/commands/upload.ts +27 -51
- package/cli/commands/window.ts +449 -0
- package/cli/desktop-companion.ts +84 -10
- package/cli/drivers/desktop.ts +2 -0
- package/cli/flow-file.ts +1 -0
- package/cli/maestro-compare.ts +19 -3
- package/cli/main.ts +22 -1
- package/cli/material-symbol-fonts.ts +171 -0
- package/cli/outbound-endpoints.ts +27 -13
- package/cli/preview-capture.ts +2 -2
- package/cli/run-registry.ts +5 -2
- package/cli/telemetry.ts +6 -3
- package/cli/ws-bridge.ts +15 -19
- package/detox/attempt-events.cjs +63 -7
- package/detox/attempt-events.d.cts +7 -0
- package/detox/gestures.ts +115 -2
- package/detox/index.ts +313 -411
- package/detox/jest-environment.cjs +86 -0
- package/detox/jest-preset.cjs +3 -1
- package/detox/jest-setup-after-env.cjs +1 -0
- package/detox/matchers.ts +3 -6
- package/detox/motion-change.cjs +9 -1
- package/detox/proof-frame.cjs +83 -9
- 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 +15 -5
- package/dist-lib/backend-origin.cjs +1 -1
- package/dist-lib/beta.cjs +1 -1
- package/dist-lib/beta.mjs +1 -1
- package/dist-lib/bridge-constants.cjs +1 -1
- package/dist-lib/bridge-contract-input.cjs +1 -1
- package/dist-lib/bridge-contract-input.mjs +1 -1
- package/dist-lib/bridge-contract.cjs +1 -1
- package/dist-lib/bridge-contract.mjs +1 -1
- package/dist-lib/bundle-channel.cjs +4000 -1362
- package/dist-lib/capture-contract.cjs +1 -1
- package/dist-lib/capture-contract.mjs +1 -1
- package/dist-lib/cloud-contract.cjs +6 -1
- package/dist-lib/cloud-contract.mjs +5 -1
- package/dist-lib/cloud-test-args.cjs +1 -1
- package/dist-lib/cloud-test-args.mjs +1 -1
- package/dist-lib/cloud.cjs +1 -1
- package/dist-lib/cloud.mjs +1 -1
- package/dist-lib/config.cjs +26 -7
- package/dist-lib/config.mjs +26 -7
- package/dist-lib/detox/index.cjs +1908 -539
- package/dist-lib/detox/jest-environment.cjs +86 -0
- package/dist-lib/detox/jest-preset.cjs +3 -1
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/expo-swift-bundle.cjs +3612 -1016
- package/dist-lib/expo-swift-metadata.cjs +1 -1
- package/dist-lib/expo-swift-metadata.mjs +1 -1
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +4755 -1825
- package/dist-lib/host/fetch-proxy-handler.cjs +59 -9
- package/dist-lib/host/fetch-proxy-overrides.cjs +62 -2
- package/dist-lib/host/fetch-proxy-overrides.mjs +56 -2
- package/dist-lib/host/replacement-module-handler.cjs +8 -8
- package/dist-lib/host/websocket-proxy.cjs +22 -6
- package/dist-lib/index.cjs +603 -184
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/jump-to-source-native.cjs +33 -4
- package/dist-lib/kotlin-host.cjs +262 -121
- 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 +101 -7
- package/dist-lib/metro-production-bundle.mjs +106 -7
- package/dist-lib/metro.cjs +308 -87
- package/dist-lib/native-app-identity-project.cjs +99 -26
- package/dist-lib/native-app-identity-project.mjs +99 -26
- package/dist-lib/native-app-identity.cjs +25 -7
- package/dist-lib/native-app-identity.mjs +25 -7
- 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 +1 -1
- package/dist-lib/react-native-host-modules.mjs +1 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +30 -9
- package/dist-lib/sdk.cjs +236 -78
- package/dist-lib/sdk.mjs +236 -78
- package/dist-lib/skills.cjs +6713 -2597
- package/dist-lib/swift-compile.cjs +1632 -299
- package/dist-lib/swift-compile.mjs +1553 -219
- package/dist-lib/swift-package.cjs +25 -3
- package/dist-lib/swift-package.mjs +22 -2
- package/dist-lib/swift-submit.cjs +1 -1
- package/dist-lib/swift-submit.mjs +1 -1
- package/dist-lib/swift-toolchain.cjs +1 -1
- package/dist-lib/swift.cjs +1 -1
- package/dist-lib/vite-swift.cjs +3670 -1051
- package/dist-lib/vite.cjs +142 -20
- package/dist-lib/worklet-executable.cjs +1 -1
- package/dist-lib/worklet-executable.mjs +1 -1
- package/install.sh +1 -1
- package/kotlin/AppSources/android/view/Window.kt +4 -1
- package/kotlin/AppSources/android/window/BackEvent.kt +24 -0
- package/kotlin/AppSources/android/window/OnBackAnimationCallback.kt +7 -0
- package/kotlin/AppSources/android/window/OnBackInvokedDispatcher.kt +37 -1
- package/kotlin/AppSources/androidx/activity/BackEventCompat.kt +5 -0
- package/kotlin/AppSources/androidx/activity/OnBackPressedDispatcher.kt +8 -1
- package/kotlin/AppSources/dev/peach/app/PeachApp.kt +14 -0
- package/kotlin/CompilerSources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar +1 -0
- package/kotlin/CompilerSources/PeachJniIncoming.kt +243 -0
- package/kotlin/CompilerSources/PeachJniPlugin.kt +116 -0
- package/kotlin/ComposeSources/PeachComposeHost.kt +27 -20
- package/kotlin/JniPlatformSources/androidx/annotation/CallSuper.kt +5 -0
- package/kotlin/JniPlatformSources/com/facebook/proguard/annotations/DoNotStrip.kt +5 -0
- package/kotlin/JniPlatformSources/dalvik/annotation/optimization/FastNative.kt +5 -0
- package/kotlin/JniPlatformSources/kotlin/Synchronized.kt +6 -0
- package/kotlin/JniPlatformSources/kotlin/jvm/JvmField.kt +5 -0
- package/kotlin/JniPlatformSources/kotlin/jvm/JvmStatic.kt +5 -0
- package/kotlin/JniSources/com/facebook/jni/HybridData.kt +68 -0
- package/kotlin/JniSources/dev/peach/jni/IncomingBindings.kt +157 -0
- package/kotlin/JniSources/dev/peach/jni/NativeBindings.kt +79 -0
- package/kotlin/JniTransportSources/dev/peach/jni/integration/JniTransport.kt +91 -0
- package/kotlin/README.md +18 -0
- package/package.json +1 -1
- package/scripts/dev-server-scanner.ts +30 -40
- package/skills/contrast/SKILL.md +1 -1
- package/skills/peach-perf/SKILL.md +25 -0
- package/skills/peach-setup/SKILL.md +25 -1
- package/src/assetCatalogContract.ts +31 -3
- package/src/auth/shared-session.ts +3 -2
- package/src/bridge-contract.ts +5 -0
- package/src/bundle-channel.mjs +1 -1
- package/src/cloud-contract.ts +11 -0
- package/src/config.ts +16 -8
- package/src/expo-kotlin-host.ts +7 -2
- package/src/guest-bundle.ts +15 -8
- package/src/host/agent-host.ts +2 -4
- package/src/host/bridge-host.ts +52 -54
- package/src/host/fetch-proxy-handler.ts +94 -31
- package/src/host/fetch-proxy-overrides.ts +91 -0
- package/src/host/replacement-module-handler.ts +11 -10
- package/src/host/server-scan-handler.ts +1 -1
- package/src/host/staged-host-files.ts +92 -0
- package/src/host/websocket-proxy.ts +27 -6
- package/src/installed-package-versions.ts +1 -0
- package/src/jump-to-source-native.ts +3 -3
- package/src/kotlin-host.ts +143 -26
- package/src/kotlin-module.ts +52 -0
- package/src/metro-layout-parser.ts +78 -0
- package/src/metro-production-bundle.ts +59 -10
- package/src/native-app-identity-project.ts +65 -3
- package/src/native-app-identity.ts +31 -0
- package/src/native-firebase-config.ts +3 -2
- package/src/native-seam-manifest.ts +0 -44
- package/src/nativeAppIconContract.ts +11 -6
- package/src/nativeViewSourceContract.ts +49 -0
- package/src/nativeViewWebContract.ts +41 -0
- package/src/nitro-source.ts +3 -7
- package/src/one-widget-metadata.ts +23 -3
- package/src/oneWidgetContract.ts +22 -0
- package/src/process-group.ts +306 -0
- package/src/process-liveness.ts +24 -0
- package/src/process-output.ts +21 -2
- package/src/serverDiscoveryContract.ts +41 -0
- package/src/swift-compile.ts +1057 -265
- package/src/swift-design-time.ts +552 -0
- package/src/swift-distribution.lock.json +12 -11
- package/src/swift-host.ts +133 -25
- package/src/swift-objc-sdk.ts +726 -0
- package/src/swift-objc-standin.ts +441 -0
- package/src/swift-package.ts +25 -0
- package/src/swift-packages.ts +25 -10
- package/src/swift-standin-config.ts +94 -0
- package/src/swift-standin-emit.ts +5 -3
- package/src/swift-standins.ts +225 -81
- package/src/swiftContract.ts +4 -0
- package/src/target-matcher.ts +67 -0
- package/src/visit-parsed-program.ts +21 -0
- package/src/vite-plugin-one.ts +58 -16
- package/src/vite-plugin-swift.ts +1 -0
- package/src/vite-plugin.ts +105 -41
- package/src/worker-system-chunks.ts +86 -9
- package/src/worklets-babel.ts +85 -13
- package/src/xcode-project.ts +40 -15
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as fs from 'fs'
|
|
2
2
|
import * as path from 'path'
|
|
3
|
+
import { isTextInputEditCommand, type TextInputEditCommand } from '@peach/globals'
|
|
3
4
|
import {
|
|
4
5
|
isStorageSnapshot,
|
|
5
6
|
prepareStorageSnapshotForShare,
|
|
@@ -36,6 +37,7 @@ import type {
|
|
|
36
37
|
SystemPermissionKind,
|
|
37
38
|
SystemPermissionStatus,
|
|
38
39
|
} from '@peach/globals'
|
|
40
|
+
import type { ProfileFile } from '@peach/globals/profiler'
|
|
39
41
|
|
|
40
42
|
const DEFAULT_TIMEOUT = 10000
|
|
41
43
|
// how long maestro looks for a required assertion's element, counted from the
|
|
@@ -185,32 +187,35 @@ function accessibilitySubtreeNodes(
|
|
|
185
187
|
function accessibilityTextMatcher(
|
|
186
188
|
value: string,
|
|
187
189
|
kind: 'text' | 'id',
|
|
188
|
-
): (candidate: string | null) => boolean {
|
|
189
|
-
|
|
190
|
-
// for a final newline. ^ and $ inside the user's pattern still match lines.
|
|
191
|
-
let pattern: RegExp
|
|
190
|
+
): ((candidate: string | null) => boolean) & Pick<RegExp, 'source' | 'flags'> {
|
|
191
|
+
let source = value
|
|
192
192
|
try {
|
|
193
|
-
|
|
193
|
+
new RegExp(source, 'ism')
|
|
194
194
|
} catch {
|
|
195
195
|
// upstream StringUtils.toRegexSafe quotes an invalid regex as literal copy.
|
|
196
|
-
|
|
197
|
-
pattern = new RegExp(`(?<![\\s\\S])(?:${escaped})(?![\\s\\S])`, 'ism')
|
|
196
|
+
source = value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
198
197
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
198
|
+
// absolute boundaries preserve Regex.matches with multiline enabled, even
|
|
199
|
+
// for a final newline. ^ and $ inside the user's pattern still match lines.
|
|
200
|
+
const pattern = new RegExp(`(?<![\\s\\S])(?:${source})(?![\\s\\S])`, 'ism')
|
|
201
|
+
return Object.assign(
|
|
202
|
+
(candidate: string | null) => {
|
|
203
|
+
// IOSDriver emits an empty attribute for absent label, value and id.
|
|
204
|
+
const native = candidate ?? ''
|
|
205
|
+
if (kind === 'id')
|
|
206
|
+
return (
|
|
207
|
+
pattern.test(native) || pattern.test(native.slice(native.lastIndexOf('/') + 1))
|
|
208
|
+
)
|
|
209
|
+
const stripped = native.replaceAll('\n', ' ')
|
|
203
210
|
return (
|
|
204
|
-
|
|
211
|
+
native === value ||
|
|
212
|
+
stripped === value ||
|
|
213
|
+
pattern.test(native) ||
|
|
214
|
+
pattern.test(stripped)
|
|
205
215
|
)
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
stripped === value ||
|
|
210
|
-
pattern.test(native) ||
|
|
211
|
-
pattern.test(stripped)
|
|
212
|
-
)
|
|
213
|
-
}
|
|
216
|
+
},
|
|
217
|
+
{ source: pattern.source, flags: pattern.flags },
|
|
218
|
+
)
|
|
214
219
|
}
|
|
215
220
|
|
|
216
221
|
// maestro reads ios frames as integer edges (AXElement.boundsString).
|
|
@@ -613,6 +618,7 @@ export interface MaestroStep {
|
|
|
613
618
|
optional?: boolean
|
|
614
619
|
}
|
|
615
620
|
inputText?: string
|
|
621
|
+
editText?: TextInputEditCommand
|
|
616
622
|
pressKey?: string
|
|
617
623
|
dispatchKey?: string
|
|
618
624
|
swipe?: {
|
|
@@ -793,6 +799,27 @@ const MAESTRO_PERMISSION_STATUS: Record<string, SystemPermissionStatus> = {
|
|
|
793
799
|
unset: 'not-determined',
|
|
794
800
|
}
|
|
795
801
|
|
|
802
|
+
// maestro's ios location permission also takes the CLAuthorizationStatus names:
|
|
803
|
+
// always is authorized always, inuse is authorized when in use and still
|
|
804
|
+
// undetermined for always, never is denied.
|
|
805
|
+
const MAESTRO_LOCATION_STATUS: Record<
|
|
806
|
+
string,
|
|
807
|
+
readonly (readonly [SystemPermissionKind, SystemPermissionStatus])[]
|
|
808
|
+
> = {
|
|
809
|
+
always: [
|
|
810
|
+
['location-when-in-use', 'authorized'],
|
|
811
|
+
['location-always', 'authorized'],
|
|
812
|
+
],
|
|
813
|
+
inuse: [
|
|
814
|
+
['location-when-in-use', 'authorized'],
|
|
815
|
+
['location-always', 'not-determined'],
|
|
816
|
+
],
|
|
817
|
+
never: [
|
|
818
|
+
['location-when-in-use', 'denied'],
|
|
819
|
+
['location-always', 'denied'],
|
|
820
|
+
],
|
|
821
|
+
}
|
|
822
|
+
|
|
796
823
|
function maestroLaunchPermissions(
|
|
797
824
|
permissions: unknown,
|
|
798
825
|
): Map<SystemPermissionKind, SystemPermissionStatus> {
|
|
@@ -802,12 +829,24 @@ function maestroLaunchPermissions(
|
|
|
802
829
|
const statuses = new Map<SystemPermissionKind, SystemPermissionStatus>()
|
|
803
830
|
// `all` first so a named permission overrides it
|
|
804
831
|
entries.sort(([a], [b]) => Number(b === 'all') - Number(a === 'all'))
|
|
805
|
-
for (const [name,
|
|
806
|
-
const
|
|
832
|
+
for (const [name, rawValue] of entries) {
|
|
833
|
+
const permission = name.toLowerCase()
|
|
834
|
+
const value = String(rawValue).toLowerCase()
|
|
835
|
+
const locationStatuses =
|
|
836
|
+
permission === 'location' ? MAESTRO_LOCATION_STATUS[value] : undefined
|
|
837
|
+
if (locationStatuses) {
|
|
838
|
+
for (const [kind, status] of locationStatuses) statuses.set(kind, status)
|
|
839
|
+
continue
|
|
840
|
+
}
|
|
841
|
+
const status = MAESTRO_PERMISSION_STATUS[value]
|
|
807
842
|
if (!status) {
|
|
808
|
-
throw new Error(
|
|
843
|
+
throw new Error(
|
|
844
|
+
permission === 'location'
|
|
845
|
+
? `launchApp.permissions.${name} must be allow, deny, unset, always, inuse, or never`
|
|
846
|
+
: `launchApp.permissions.${name} must be allow, deny, or unset`,
|
|
847
|
+
)
|
|
809
848
|
}
|
|
810
|
-
for (const kind of MAESTRO_PERMISSION_KINDS[
|
|
849
|
+
for (const kind of MAESTRO_PERMISSION_KINDS[permission] ?? []) {
|
|
811
850
|
statuses.set(kind, status)
|
|
812
851
|
}
|
|
813
852
|
}
|
|
@@ -933,6 +972,13 @@ export function parseFlowSteps(content: string): MaestroStep[] {
|
|
|
933
972
|
return parseFlowFile(content).steps
|
|
934
973
|
}
|
|
935
974
|
|
|
975
|
+
class CaptureSetupError extends Error {
|
|
976
|
+
constructor(cause: unknown) {
|
|
977
|
+
super(cause instanceof Error ? cause.message : String(cause), { cause })
|
|
978
|
+
this.name = 'CaptureSetupError'
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
|
|
936
982
|
export class SootSimBridgeFlowRunner {
|
|
937
983
|
stepDelay = 0
|
|
938
984
|
|
|
@@ -952,7 +998,7 @@ export class SootSimBridgeFlowRunner {
|
|
|
952
998
|
private recordingStartedAtMs: number | null = null
|
|
953
999
|
private lastRecordingStartedAtMs: number | null = null
|
|
954
1000
|
private lastRecordingDurationMs: number | null = null
|
|
955
|
-
private
|
|
1001
|
+
private lastRecordingProfile: ProfileFile | null = null
|
|
956
1002
|
private previewStorageSnapshot: ShareableStorageSnapshot | null = null
|
|
957
1003
|
private flowTraceSteps: SootSimFlowTraceStep[] = []
|
|
958
1004
|
// when the latest step that acted on the app settled
|
|
@@ -1108,7 +1154,17 @@ export class SootSimBridgeFlowRunner {
|
|
|
1108
1154
|
throw new Error(`app not ready after ${timeout}ms: ${waitReadyReason(status)}`)
|
|
1109
1155
|
}
|
|
1110
1156
|
|
|
1157
|
+
// capture setup failures are run failures: wrapping them keeps an optional
|
|
1158
|
+
// step or a retry body from consuming them as ordinary action failures.
|
|
1111
1159
|
private async ensureRecordingStarted() {
|
|
1160
|
+
try {
|
|
1161
|
+
await this.startRecordingOnce()
|
|
1162
|
+
} catch (error) {
|
|
1163
|
+
throw error instanceof CaptureSetupError ? error : new CaptureSetupError(error)
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
private async startRecordingOnce() {
|
|
1112
1168
|
if (!this.opts.recordingOutputDir) {
|
|
1113
1169
|
throw new Error('recording output directory not configured')
|
|
1114
1170
|
}
|
|
@@ -1153,10 +1209,25 @@ export class SootSimBridgeFlowRunner {
|
|
|
1153
1209
|
throw new Error(result?.error || 'recording unavailable on this page')
|
|
1154
1210
|
}
|
|
1155
1211
|
if (!this.recordingStartedAtMs) {
|
|
1156
|
-
|
|
1212
|
+
const startedAtMs =
|
|
1157
1213
|
typeof result.startedAtMs === 'number' && Number.isFinite(result.startedAtMs)
|
|
1158
1214
|
? result.startedAtMs
|
|
1159
1215
|
: startedAt
|
|
1216
|
+
// events can start before video setup, even without a launchApp step.
|
|
1217
|
+
// rebase once at the shared first-frame boundary for every start path;
|
|
1218
|
+
// the start is ready only after the rebase lands, so a rejected rebase
|
|
1219
|
+
// stops the recorder and leaves no half-started state to skip past.
|
|
1220
|
+
try {
|
|
1221
|
+
await this.evaluate(`(() => {
|
|
1222
|
+
const recorder = window.SootSim?.bridges?.eventRecorder ?? window.__sootsimEventRecorder
|
|
1223
|
+
if (!recorder?.isRecording?.()) return
|
|
1224
|
+
return recorder.markVisibleStart(${JSON.stringify(startedAtMs)})
|
|
1225
|
+
})()`)
|
|
1226
|
+
} catch (error) {
|
|
1227
|
+
await this.evaluate(`window.__sootsimRecorder.stop()`).catch(() => {})
|
|
1228
|
+
throw error
|
|
1229
|
+
}
|
|
1230
|
+
this.recordingStartedAtMs = startedAtMs
|
|
1160
1231
|
}
|
|
1161
1232
|
}
|
|
1162
1233
|
|
|
@@ -1177,8 +1248,8 @@ export class SootSimBridgeFlowRunner {
|
|
|
1177
1248
|
return this.lastRecordingStartedAtMs
|
|
1178
1249
|
}
|
|
1179
1250
|
|
|
1180
|
-
|
|
1181
|
-
return this.
|
|
1251
|
+
getLastRecordingProfile(): ProfileFile | null {
|
|
1252
|
+
return this.lastRecordingProfile
|
|
1182
1253
|
}
|
|
1183
1254
|
|
|
1184
1255
|
getPreviewStorageSnapshot(): ShareableStorageSnapshot | null {
|
|
@@ -1257,7 +1328,7 @@ export class SootSimBridgeFlowRunner {
|
|
|
1257
1328
|
if (this.recordingStartedAtMs == null) {
|
|
1258
1329
|
this.lastRecordingStartedAtMs = null
|
|
1259
1330
|
this.lastRecordingDurationMs = null
|
|
1260
|
-
this.
|
|
1331
|
+
this.lastRecordingProfile = null
|
|
1261
1332
|
return null
|
|
1262
1333
|
}
|
|
1263
1334
|
|
|
@@ -1267,11 +1338,11 @@ export class SootSimBridgeFlowRunner {
|
|
|
1267
1338
|
size?: number
|
|
1268
1339
|
mime?: string
|
|
1269
1340
|
durationMs: number
|
|
1270
|
-
|
|
1341
|
+
profile?: ProfileFile | null
|
|
1271
1342
|
}>(`window.__sootsimRecorder.stop()`)
|
|
1272
1343
|
if (!stopped?.ok) {
|
|
1273
1344
|
this.recordingStartedAtMs = null
|
|
1274
|
-
this.
|
|
1345
|
+
this.lastRecordingProfile = null
|
|
1275
1346
|
throw new Error(stopped?.error || 'recording stop failed')
|
|
1276
1347
|
}
|
|
1277
1348
|
if (!Number.isFinite(stopped.durationMs) || stopped.durationMs <= 0) {
|
|
@@ -1279,7 +1350,7 @@ export class SootSimBridgeFlowRunner {
|
|
|
1279
1350
|
}
|
|
1280
1351
|
this.lastRecordingStartedAtMs = this.recordingStartedAtMs
|
|
1281
1352
|
this.lastRecordingDurationMs = stopped.durationMs
|
|
1282
|
-
this.
|
|
1353
|
+
this.lastRecordingProfile = stopped.profile ?? null
|
|
1283
1354
|
this.recordingStartedAtMs = null
|
|
1284
1355
|
|
|
1285
1356
|
// drain blob in chunks
|
|
@@ -1425,7 +1496,16 @@ export class SootSimBridgeFlowRunner {
|
|
|
1425
1496
|
if (!test || !test.queryAll) return null
|
|
1426
1497
|
const selector = ${JSON.stringify(opts)}
|
|
1427
1498
|
const compile = ${accessibilityTextMatcher.toString()}
|
|
1428
|
-
|
|
1499
|
+
// relative filters need their anchors too; other id selectors only
|
|
1500
|
+
// project matching nodes, retaining every duplicate for ordering below.
|
|
1501
|
+
// patterns accepting an absent id also need unidentified nodes.
|
|
1502
|
+
const idMatcher = selector.id == null ? null : compile(selector.id, 'id')
|
|
1503
|
+
const needsAnchors = ['childOf', 'below', 'above', 'leftOf', 'rightOf']
|
|
1504
|
+
.some((key) => selector[key] != null)
|
|
1505
|
+
const query = idMatcher && !idMatcher(null) && !needsAnchors
|
|
1506
|
+
? { idPattern: { source: idMatcher.source, flags: idMatcher.flags, matchLastSegment: true } }
|
|
1507
|
+
: {}
|
|
1508
|
+
const all = ((await test.queryAll(query)) || []).filter(
|
|
1429
1509
|
(n) => n && n.absolutePosition && n.layout,
|
|
1430
1510
|
)
|
|
1431
1511
|
// maestro reads ios frames as integer edges (AXElement.boundsString)
|
|
@@ -1668,11 +1748,22 @@ export class SootSimBridgeFlowRunner {
|
|
|
1668
1748
|
})) as { ok?: boolean; value?: boolean } | null
|
|
1669
1749
|
}
|
|
1670
1750
|
|
|
1751
|
+
// the engine's inspect geometry revision: equal readings bracket a span in
|
|
1752
|
+
// which no tree, layout, scroll or prop write could move any element.
|
|
1753
|
+
private async geometryRevision(): Promise<string | null> {
|
|
1754
|
+
const revision = await this.evaluate(
|
|
1755
|
+
`(async () => window.__sootsimTest?.getGeometryRevision?.())()`,
|
|
1756
|
+
)
|
|
1757
|
+
return typeof revision === 'string' ? revision : null
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1671
1760
|
private async waitForElement(opts: FlowTargetSelector, timeoutMs = 5000) {
|
|
1672
1761
|
const deadline = Date.now() + timeoutMs
|
|
1762
|
+
let revision = await this.geometryRevision()
|
|
1673
1763
|
let element = await this.findElement(opts)
|
|
1674
1764
|
while (!element && Date.now() < deadline) {
|
|
1675
1765
|
await sleep(150)
|
|
1766
|
+
revision = await this.geometryRevision()
|
|
1676
1767
|
element = await this.findElement(opts)
|
|
1677
1768
|
}
|
|
1678
1769
|
if (!element) return element
|
|
@@ -1682,10 +1773,15 @@ export class SootSimBridgeFlowRunner {
|
|
|
1682
1773
|
// new-thread title tap opened the photo picker this way (2026-07-10).
|
|
1683
1774
|
// require two consecutive samples at the same absolute position before
|
|
1684
1775
|
// interacting, bounded so a perpetually-animating target still proceeds.
|
|
1776
|
+
// a revision unchanged since the last sample began proves the next
|
|
1777
|
+
// sample would read the same frame, so it is not read again.
|
|
1685
1778
|
const stabilityDeadline = Date.now() + 1200
|
|
1686
1779
|
let prev = element
|
|
1687
|
-
|
|
1780
|
+
do {
|
|
1688
1781
|
await sleep(120)
|
|
1782
|
+
const sampled = await this.geometryRevision()
|
|
1783
|
+
if (sampled !== null && sampled === revision) return element
|
|
1784
|
+
revision = sampled
|
|
1689
1785
|
const next = await this.findElement(opts)
|
|
1690
1786
|
if (!next) break
|
|
1691
1787
|
const moved =
|
|
@@ -1696,7 +1792,7 @@ export class SootSimBridgeFlowRunner {
|
|
|
1696
1792
|
element = next
|
|
1697
1793
|
if (!moved) return element
|
|
1698
1794
|
prev = next
|
|
1699
|
-
}
|
|
1795
|
+
} while (Date.now() < stabilityDeadline)
|
|
1700
1796
|
return element
|
|
1701
1797
|
}
|
|
1702
1798
|
|
|
@@ -1998,6 +2094,19 @@ export class SootSimBridgeFlowRunner {
|
|
|
1998
2094
|
await sleep(200)
|
|
1999
2095
|
}
|
|
2000
2096
|
|
|
2097
|
+
async editText(command: TextInputEditCommand) {
|
|
2098
|
+
if (!isTextInputEditCommand(command))
|
|
2099
|
+
throw new Error('editText: invalid range command')
|
|
2100
|
+
if (!(await this.waitForFocusedTextInput()))
|
|
2101
|
+
throw new Error('editText: no focused TextInput')
|
|
2102
|
+
await this.bridge.send({
|
|
2103
|
+
type: 'keyboard',
|
|
2104
|
+
simId: this.opts.simId,
|
|
2105
|
+
action: 'edit',
|
|
2106
|
+
edit: command,
|
|
2107
|
+
})
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2001
2110
|
// poll for a focused text input. returns true once getFocusedNode reports a
|
|
2002
2111
|
// node (focus landed), false if the budget elapses. callers use it both as a
|
|
2003
2112
|
// tap-focus confirmation (tapOn retry) and a pre-type barrier (inputText).
|
|
@@ -2031,7 +2140,7 @@ export class SootSimBridgeFlowRunner {
|
|
|
2031
2140
|
expected?: FocusedTextInputInfo | null,
|
|
2032
2141
|
): Promise<boolean> {
|
|
2033
2142
|
const deadline = Date.now() + timeoutMs
|
|
2034
|
-
|
|
2143
|
+
do {
|
|
2035
2144
|
try {
|
|
2036
2145
|
const focused = await this.callTest<FocusedTextInputInfo | null>('getFocusedNode')
|
|
2037
2146
|
if (focused && this.focusedTextInputMatches(focused, expected)) return true
|
|
@@ -2039,7 +2148,7 @@ export class SootSimBridgeFlowRunner {
|
|
|
2039
2148
|
// bridge hiccup — keep polling within the budget
|
|
2040
2149
|
}
|
|
2041
2150
|
await sleep(50)
|
|
2042
|
-
}
|
|
2151
|
+
} while (Date.now() < deadline)
|
|
2043
2152
|
return false
|
|
2044
2153
|
}
|
|
2045
2154
|
|
|
@@ -2082,7 +2191,7 @@ export class SootSimBridgeFlowRunner {
|
|
|
2082
2191
|
})
|
|
2083
2192
|
|
|
2084
2193
|
const deadline = Date.now() + 3000
|
|
2085
|
-
|
|
2194
|
+
do {
|
|
2086
2195
|
// the host test bridge proxies the tenant, so isTextInputFocused
|
|
2087
2196
|
// resolves a promise; reading it unawaited is always truthy.
|
|
2088
2197
|
const state = await this.evaluate<{
|
|
@@ -2104,7 +2213,7 @@ export class SootSimBridgeFlowRunner {
|
|
|
2104
2213
|
return
|
|
2105
2214
|
}
|
|
2106
2215
|
await sleep(80)
|
|
2107
|
-
}
|
|
2216
|
+
} while (Date.now() < deadline)
|
|
2108
2217
|
}
|
|
2109
2218
|
|
|
2110
2219
|
async eraseText(count: number) {
|
|
@@ -2124,11 +2233,11 @@ export class SootSimBridgeFlowRunner {
|
|
|
2124
2233
|
async waitFor(opts: { text?: string; id?: string; timeout?: number }) {
|
|
2125
2234
|
const { timeout, ...selector } = opts
|
|
2126
2235
|
const deadline = Date.now() + scaledTimeout(timeout || DEFAULT_TIMEOUT)
|
|
2127
|
-
|
|
2236
|
+
do {
|
|
2128
2237
|
const element = await this.findElement(selector)
|
|
2129
2238
|
if (element) return
|
|
2130
2239
|
await sleep(200)
|
|
2131
|
-
}
|
|
2240
|
+
} while (Date.now() < deadline)
|
|
2132
2241
|
// surface whether the element existed but was off-screen vs never
|
|
2133
2242
|
// showed up at all — the two failure modes need different debugging
|
|
2134
2243
|
// (scroll-position bug vs missing wait-step / wrong screen).
|
|
@@ -2569,7 +2678,7 @@ export class SootSimBridgeFlowRunner {
|
|
|
2569
2678
|
)
|
|
2570
2679
|
const deadline = Date.now() + (opts.timeout || 20000)
|
|
2571
2680
|
const direction = opts.direction?.toUpperCase() || 'DOWN'
|
|
2572
|
-
|
|
2681
|
+
do {
|
|
2573
2682
|
const screen = await this.screenBounds()
|
|
2574
2683
|
const centerX = screen.width / 2
|
|
2575
2684
|
const centerY = screen.height / 2
|
|
@@ -2608,7 +2717,7 @@ export class SootSimBridgeFlowRunner {
|
|
|
2608
2717
|
Math.max(1, Math.round(duration / 16)),
|
|
2609
2718
|
16,
|
|
2610
2719
|
)
|
|
2611
|
-
}
|
|
2720
|
+
} while (Date.now() < deadline)
|
|
2612
2721
|
|
|
2613
2722
|
throw new Error(`scrollUntilVisible: ${JSON.stringify(target)} not found`)
|
|
2614
2723
|
}
|
|
@@ -2619,7 +2728,8 @@ export class SootSimBridgeFlowRunner {
|
|
|
2619
2728
|
timeout?: number
|
|
2620
2729
|
}) {
|
|
2621
2730
|
const deadline = Date.now() + scaledTimeout(opts.timeout || DEFAULT_TIMEOUT)
|
|
2622
|
-
|
|
2731
|
+
// sample once even when scheduling consumes the initial lookup window.
|
|
2732
|
+
do {
|
|
2623
2733
|
const visibleOkay = opts.visible
|
|
2624
2734
|
? await this.isElementVisible(
|
|
2625
2735
|
typeof opts.visible === 'string' ? { text: opts.visible } : opts.visible,
|
|
@@ -2634,7 +2744,7 @@ export class SootSimBridgeFlowRunner {
|
|
|
2634
2744
|
: true
|
|
2635
2745
|
if (visibleOkay && notVisibleOkay) return
|
|
2636
2746
|
await sleep(200)
|
|
2637
|
-
}
|
|
2747
|
+
} while (Date.now() < deadline)
|
|
2638
2748
|
throw new Error('extendedWaitUntil timed out')
|
|
2639
2749
|
}
|
|
2640
2750
|
|
|
@@ -2679,7 +2789,7 @@ export class SootSimBridgeFlowRunner {
|
|
|
2679
2789
|
if (!this.recordingEnabled && !this.profilingEnabled) return
|
|
2680
2790
|
const deadline = Date.now() + 10000
|
|
2681
2791
|
let lastError: unknown = null
|
|
2682
|
-
|
|
2792
|
+
do {
|
|
2683
2793
|
try {
|
|
2684
2794
|
if (this.recordingEnabled) await this.ensureRecordingStarted()
|
|
2685
2795
|
if (this.profilingEnabled) await this.startProfile()
|
|
@@ -2688,7 +2798,7 @@ export class SootSimBridgeFlowRunner {
|
|
|
2688
2798
|
lastError = err
|
|
2689
2799
|
await sleep(250)
|
|
2690
2800
|
}
|
|
2691
|
-
}
|
|
2801
|
+
} while (Date.now() < deadline)
|
|
2692
2802
|
throw new Error(
|
|
2693
2803
|
`capture re-arm failed after app reload: ${
|
|
2694
2804
|
lastError instanceof Error ? lastError.message : String(lastError)
|
|
@@ -2793,17 +2903,7 @@ export class SootSimBridgeFlowRunner {
|
|
|
2793
2903
|
// begin its pixels only after launch has mounted and settled, leaving the
|
|
2794
2904
|
// loading prelude outside the playable video while its trace stays visible.
|
|
2795
2905
|
await this.waitForVisualSettle()
|
|
2796
|
-
const videoAlreadyStarted = this.recordingStartedAtMs != null
|
|
2797
2906
|
await this.ensureRecordingStarted()
|
|
2798
|
-
if (!videoAlreadyStarted && this.recordingStartedAtMs != null) {
|
|
2799
|
-
// the preview event recorder has been running since before launch,
|
|
2800
|
-
// so its clock includes the boot prelude the video just skipped.
|
|
2801
|
-
// rebase it to the video's first frame — without this every event
|
|
2802
|
-
// timestamp overshoots the playable video and preview playback can
|
|
2803
|
-
// never align (play stuck disabled). no-op unless the preview
|
|
2804
|
-
// event recorder is running.
|
|
2805
|
-
await this.rebaseEventRecorderToVideoStart(this.recordingStartedAtMs)
|
|
2806
|
-
}
|
|
2807
2907
|
}
|
|
2808
2908
|
}
|
|
2809
2909
|
|
|
@@ -2819,22 +2919,6 @@ export class SootSimBridgeFlowRunner {
|
|
|
2819
2919
|
}
|
|
2820
2920
|
}
|
|
2821
2921
|
|
|
2822
|
-
private async rebaseEventRecorderToVideoStart(videoStartedAtMs: number) {
|
|
2823
|
-
try {
|
|
2824
|
-
await this.evaluate(
|
|
2825
|
-
`(() => {
|
|
2826
|
-
const recorder = window.SootSim?.bridges?.eventRecorder ?? window.__sootsimEventRecorder
|
|
2827
|
-
if (!recorder?.isRecording?.()) return 'not-recording'
|
|
2828
|
-
return recorder.markVisibleStart?.(${JSON.stringify(videoStartedAtMs)}) ?? 'no-mark'
|
|
2829
|
-
})()`,
|
|
2830
|
-
)
|
|
2831
|
-
} catch (err) {
|
|
2832
|
-
console.warn(
|
|
2833
|
-
` warn: event-recorder rebase to video start failed: ${err instanceof Error ? err.message : err}`,
|
|
2834
|
-
)
|
|
2835
|
-
}
|
|
2836
|
-
}
|
|
2837
|
-
|
|
2838
2922
|
// maestro waits around taps for the screen to stop changing, up to
|
|
2839
2923
|
// its ios driver's 3 s settle budget, comparing screenshots, so an action
|
|
2840
2924
|
// still in flight (a submit spinner, a sheet closing) finishes before the
|
|
@@ -2883,6 +2967,15 @@ export class SootSimBridgeFlowRunner {
|
|
|
2883
2967
|
paintStill: true,
|
|
2884
2968
|
})
|
|
2885
2969
|
if (!result.settled) {
|
|
2970
|
+
// an app loop that never ends (a pulsing status dot) leaves only the
|
|
2971
|
+
// pixels moving. maestro on ios captures that frame and continues, so
|
|
2972
|
+
// do the same; any other blocker is real unfinished work and still fails.
|
|
2973
|
+
if (result.blockedBy.startsWith('screen pixels still changing over ')) {
|
|
2974
|
+
console.warn(
|
|
2975
|
+
` [flow] screen never became still in ${result.elapsed}ms; capturing as maestro does`,
|
|
2976
|
+
)
|
|
2977
|
+
return
|
|
2978
|
+
}
|
|
2886
2979
|
throw new Error(
|
|
2887
2980
|
`visual state did not settle within ${result.elapsed}ms (${result.blockedBy})`,
|
|
2888
2981
|
)
|
|
@@ -2901,7 +2994,7 @@ export class SootSimBridgeFlowRunner {
|
|
|
2901
2994
|
private async waitForShellAppLaunched(appId: string, timeoutMs: number) {
|
|
2902
2995
|
const deadline = Date.now() + timeoutMs
|
|
2903
2996
|
let lastState: ShellState | null = null
|
|
2904
|
-
|
|
2997
|
+
do {
|
|
2905
2998
|
try {
|
|
2906
2999
|
lastState = await getShellState(this.bridge)
|
|
2907
3000
|
} catch {
|
|
@@ -2917,7 +3010,7 @@ export class SootSimBridgeFlowRunner {
|
|
|
2917
3010
|
return
|
|
2918
3011
|
}
|
|
2919
3012
|
await sleep(16)
|
|
2920
|
-
}
|
|
3013
|
+
} while (Date.now() < deadline)
|
|
2921
3014
|
}
|
|
2922
3015
|
|
|
2923
3016
|
private async launchShellAppFromHomeIfNeeded(opts: unknown): Promise<boolean> {
|
|
@@ -3152,7 +3245,11 @@ export class SootSimBridgeFlowRunner {
|
|
|
3152
3245
|
)
|
|
3153
3246
|
}
|
|
3154
3247
|
|
|
3155
|
-
async runStep(
|
|
3248
|
+
async runStep(
|
|
3249
|
+
rawStepInput: MaestroStep | string,
|
|
3250
|
+
stepIndex: number,
|
|
3251
|
+
onStepStart?: () => Promise<void>,
|
|
3252
|
+
) {
|
|
3156
3253
|
await this.liveStatus?.waitWhilePaused()
|
|
3157
3254
|
// maestro allows several commands in BARE-STRING form (`- back`,
|
|
3158
3255
|
// `- hideKeyboard`, `- waitForAnimationToEnd`, `- scrollUp`, …). yaml parses
|
|
@@ -3166,7 +3263,7 @@ export class SootSimBridgeFlowRunner {
|
|
|
3166
3263
|
const stepName =
|
|
3167
3264
|
Object.keys(rawStep).find((k) => k !== 'when') || Object.keys(rawStep)[0]
|
|
3168
3265
|
console.log(`[flow] step ${stepIndex + 1}: ${stepName}`)
|
|
3169
|
-
|
|
3266
|
+
let startedAtMs = Date.now()
|
|
3170
3267
|
let targetLabel = this.flowStepTargetLabel(rawStep, stepName)
|
|
3171
3268
|
|
|
3172
3269
|
let step: MaestroStep
|
|
@@ -3238,6 +3335,10 @@ export class SootSimBridgeFlowRunner {
|
|
|
3238
3335
|
) {
|
|
3239
3336
|
await this.ensureRecordingStarted()
|
|
3240
3337
|
}
|
|
3338
|
+
// recording is run setup. neither clock starts until its first frame and
|
|
3339
|
+
// storage capture are ready; the run total still includes that setup.
|
|
3340
|
+
await onStepStart?.()
|
|
3341
|
+
startedAtMs = Date.now()
|
|
3241
3342
|
// simple commands have a wall-clock watchdog. bounded operations keep
|
|
3242
3343
|
// their own lookup, settle and bridge deadlines, so the watchdog cannot
|
|
3243
3344
|
// report a completed step while its action continues in the background.
|
|
@@ -3327,7 +3428,7 @@ export class SootSimBridgeFlowRunner {
|
|
|
3327
3428
|
screenshotPath,
|
|
3328
3429
|
})
|
|
3329
3430
|
} catch (error) {
|
|
3330
|
-
if (this.isOptional(step)) {
|
|
3431
|
+
if (!(error instanceof CaptureSetupError) && this.isOptional(step)) {
|
|
3331
3432
|
console.log(
|
|
3332
3433
|
`[flow] (optional, skipped: ${(error as Error).message.slice(0, 80)})`,
|
|
3333
3434
|
)
|
|
@@ -3374,6 +3475,7 @@ export class SootSimBridgeFlowRunner {
|
|
|
3374
3475
|
else if (step.assertVisible) await this.assertVisible(step.assertVisible)
|
|
3375
3476
|
else if (step.assertNotVisible) await this.assertNotVisible(step.assertNotVisible)
|
|
3376
3477
|
else if (step.inputText) await this.inputText(step.inputText)
|
|
3478
|
+
else if (step.editText) await this.editText(step.editText)
|
|
3377
3479
|
else if (step.pressKey) await this.pressKey(step.pressKey)
|
|
3378
3480
|
else if (step.dispatchKey) await this.dispatchKey(step.dispatchKey)
|
|
3379
3481
|
else if (step.waitFor) await this.waitFor(step.waitFor)
|
|
@@ -3490,7 +3592,7 @@ export class SootSimBridgeFlowRunner {
|
|
|
3490
3592
|
}
|
|
3491
3593
|
break
|
|
3492
3594
|
} catch (error) {
|
|
3493
|
-
if (attempt >= maxRetries) throw error
|
|
3595
|
+
if (error instanceof CaptureSetupError || attempt >= maxRetries) throw error
|
|
3494
3596
|
console.log(
|
|
3495
3597
|
`[flow] retry ${attempt + 1}/${maxRetries} after: ${(error as Error).message.slice(0, 120)}`,
|
|
3496
3598
|
)
|
|
@@ -3709,9 +3811,12 @@ export class SootSimBridgeFlowRunner {
|
|
|
3709
3811
|
// devtools pause never interrupts a step mid-gesture.
|
|
3710
3812
|
if (this.liveStatus) {
|
|
3711
3813
|
await this.liveStatus.waitWhilePaused()
|
|
3712
|
-
await this.liveStatus.step(i, 'running')
|
|
3713
3814
|
}
|
|
3714
|
-
|
|
3815
|
+
let liveStartedAtMs = Date.now()
|
|
3816
|
+
const onStepStart = async () => {
|
|
3817
|
+
await this.liveStatus?.step(i, 'running')
|
|
3818
|
+
liveStartedAtMs = Date.now()
|
|
3819
|
+
}
|
|
3715
3820
|
const reportOutcome = async (failed: boolean, error?: unknown) => {
|
|
3716
3821
|
if (!this.liveStatus) return
|
|
3717
3822
|
// the last trace entry after runStep settles is the top-level step's
|
|
@@ -3727,7 +3832,7 @@ export class SootSimBridgeFlowRunner {
|
|
|
3727
3832
|
? 'skipped'
|
|
3728
3833
|
: 'success',
|
|
3729
3834
|
{
|
|
3730
|
-
durationMs: Date.now() - liveStartedAtMs,
|
|
3835
|
+
durationMs: traced ? last.durationMs : Date.now() - liveStartedAtMs,
|
|
3731
3836
|
error: failed
|
|
3732
3837
|
? error instanceof Error
|
|
3733
3838
|
? error.message.slice(0, 300)
|
|
@@ -3745,10 +3850,10 @@ export class SootSimBridgeFlowRunner {
|
|
|
3745
3850
|
// outer per-step race would kill a launch that's legitimately still
|
|
3746
3851
|
// booting. let both govern their own timing.
|
|
3747
3852
|
if (step.runFlow || step.launchApp) {
|
|
3748
|
-
await this.runStep(step, i)
|
|
3853
|
+
await this.runStep(step, i, onStepStart)
|
|
3749
3854
|
} else {
|
|
3750
3855
|
await Promise.race([
|
|
3751
|
-
this.runStep(step, i),
|
|
3856
|
+
this.runStep(step, i, onStepStart),
|
|
3752
3857
|
new Promise<never>((_, reject) =>
|
|
3753
3858
|
setTimeout(
|
|
3754
3859
|
() =>
|