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
|
@@ -22,6 +22,10 @@ import { PERMISSION_KINDS } from './commands/permissions'
|
|
|
22
22
|
import { saveCurrentSimId } from './current-sim'
|
|
23
23
|
import { parseFlowFile } from './flow-file'
|
|
24
24
|
import { FlowLiveStatusReporter } from './flow-live-status'
|
|
25
|
+
import {
|
|
26
|
+
accessibilityBounds,
|
|
27
|
+
sameNativeAccessibilityShape,
|
|
28
|
+
} from './maestro-accessibility'
|
|
25
29
|
import { MaestroJsContext, scriptConditionIsTruthy } from './maestro-js'
|
|
26
30
|
import { ensureCliRecordingEntitlement } from './recording-access'
|
|
27
31
|
import type { PerformResult, PerformStep, ResetResult } from '../src/bridge-contract'
|
|
@@ -43,6 +47,23 @@ const DEFAULT_TIMEOUT = 10000
|
|
|
43
47
|
// how long maestro looks for a required assertion's element, counted from the
|
|
44
48
|
// latest interaction (Orchestra lookupTimeoutMs and adjustedToLatestInteraction)
|
|
45
49
|
const ASSERTION_LOOKUP_MS = 17_000
|
|
50
|
+
// commands Orchestra.executeCommand reports as not mutating the device, so
|
|
51
|
+
// they leave the lookup window running; every other command restarts it. the
|
|
52
|
+
// last four are Peach observations with no device effect.
|
|
53
|
+
const NON_MUTATING_STEPS = [
|
|
54
|
+
'assertTrue',
|
|
55
|
+
'assertVisible',
|
|
56
|
+
'assertNotVisible',
|
|
57
|
+
'extendedWaitUntil',
|
|
58
|
+
'takeScreenshot',
|
|
59
|
+
'setClipboard',
|
|
60
|
+
'copyTextFrom',
|
|
61
|
+
'clearKeychain',
|
|
62
|
+
'waitFor',
|
|
63
|
+
'wait',
|
|
64
|
+
'dumpTree',
|
|
65
|
+
'assertTreeContains',
|
|
66
|
+
] as const satisfies readonly (keyof MaestroStep)[]
|
|
46
67
|
const FLOW_TIMEOUT_SCALE = flowTimeoutScale()
|
|
47
68
|
const scaledTimeout = (ms: number) => Math.round(ms * FLOW_TIMEOUT_SCALE)
|
|
48
69
|
|
|
@@ -235,23 +256,6 @@ function accessibilityTextMatcher(
|
|
|
235
256
|
)
|
|
236
257
|
}
|
|
237
258
|
|
|
238
|
-
// maestro reads ios frames as integer edges (AXElement.boundsString).
|
|
239
|
-
function accessibilityBounds(node: PeachAccessibilityNode): {
|
|
240
|
-
x: number
|
|
241
|
-
y: number
|
|
242
|
-
width: number
|
|
243
|
-
height: number
|
|
244
|
-
} {
|
|
245
|
-
const left = Math.trunc(node.frame.x)
|
|
246
|
-
const top = Math.trunc(node.frame.y)
|
|
247
|
-
return {
|
|
248
|
-
x: left,
|
|
249
|
-
y: top,
|
|
250
|
-
width: Math.trunc(node.frame.x + node.frame.width) - left,
|
|
251
|
-
height: Math.trunc(node.frame.y + node.frame.height) - top,
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
259
|
const ACCESSIBILITY_TRAIT_FILTERS: Record<string, AccessibilityNodePredicate> = {
|
|
256
260
|
// IOSDriver sets attributes["text"] on every node, including empty text.
|
|
257
261
|
TEXT: () => true,
|
|
@@ -355,7 +359,12 @@ function distinctAccessibilityNodes(
|
|
|
355
359
|
node.children.length,
|
|
356
360
|
])
|
|
357
361
|
const bucket = buckets.get(key)
|
|
358
|
-
if (
|
|
362
|
+
if (
|
|
363
|
+
bucket?.some((other) =>
|
|
364
|
+
sameNativeAccessibilityShape(node, other, accessibilityBounds),
|
|
365
|
+
)
|
|
366
|
+
)
|
|
367
|
+
return false
|
|
359
368
|
if (bucket) bucket.push(node)
|
|
360
369
|
else buckets.set(key, [node])
|
|
361
370
|
return true
|
|
@@ -371,39 +380,6 @@ function accessibilityAnchorSelector(value: unknown): FlowTargetSelector {
|
|
|
371
380
|
return isSelectorRecord(value) ? value : {}
|
|
372
381
|
}
|
|
373
382
|
|
|
374
|
-
// upstream Filters.containsChild compares the resolved child TreeNode against
|
|
375
|
-
// a candidate's immediate children by structural equality: the native
|
|
376
|
-
// attribute map, the boolean flags, and the children, recursively. the native
|
|
377
|
-
// hintText attribute is the placeholder (accessibilityHint is not a TreeNode
|
|
378
|
-
// attribute), and engine identity (elementId/nodeId) plus the projection's own
|
|
379
|
-
// metadata (type, role, isAccessibilityElement, state) are not part of that
|
|
380
|
-
// native shape.
|
|
381
|
-
function sameNativeAccessibilityShape(
|
|
382
|
-
a: PeachAccessibilityNode,
|
|
383
|
-
b: PeachAccessibilityNode,
|
|
384
|
-
): boolean {
|
|
385
|
-
if (
|
|
386
|
-
(a.testID ?? '') !== (b.testID ?? '') ||
|
|
387
|
-
(a.label ?? '') !== (b.label ?? '') ||
|
|
388
|
-
(a.placeholder ?? '') !== (b.placeholder ?? '') ||
|
|
389
|
-
(a.value ?? '') !== (b.value ?? '') ||
|
|
390
|
-
a.enabled !== b.enabled ||
|
|
391
|
-
a.selected !== b.selected ||
|
|
392
|
-
a.checked !== b.checked ||
|
|
393
|
-
a.clickable !== b.clickable ||
|
|
394
|
-
(a.hasFocus ?? false) !== (b.hasFocus ?? false) ||
|
|
395
|
-
JSON.stringify(accessibilityBounds(a)) !== JSON.stringify(accessibilityBounds(b)) ||
|
|
396
|
-
a.children.length !== b.children.length
|
|
397
|
-
) {
|
|
398
|
-
return false
|
|
399
|
-
}
|
|
400
|
-
for (const [index, child] of a.children.entries()) {
|
|
401
|
-
const other = b.children[index]
|
|
402
|
-
if (!other || !sameNativeAccessibilityShape(child, other)) return false
|
|
403
|
-
}
|
|
404
|
-
return true
|
|
405
|
-
}
|
|
406
|
-
|
|
407
383
|
// Filters.containsDescendants: every selector must match inside a child
|
|
408
384
|
// subtree; the node itself is excluded. A selector matches a subtree node by
|
|
409
385
|
// the full filter applied to that single node (Filters' `matches`), which for
|
|
@@ -490,7 +466,7 @@ function filterAccessibilityNodesIn(
|
|
|
490
466
|
candidates = child
|
|
491
467
|
? candidates.filter((node) =>
|
|
492
468
|
node.children.some((candidate) =>
|
|
493
|
-
sameNativeAccessibilityShape(candidate, child),
|
|
469
|
+
sameNativeAccessibilityShape(candidate, child, accessibilityBounds),
|
|
494
470
|
),
|
|
495
471
|
)
|
|
496
472
|
: []
|
|
@@ -679,6 +655,14 @@ export interface MaestroStep {
|
|
|
679
655
|
optional?: boolean
|
|
680
656
|
}
|
|
681
657
|
waitForAnimationToEnd?: boolean | number | { timeout?: number }
|
|
658
|
+
advanceAnimationClock?:
|
|
659
|
+
| number
|
|
660
|
+
| number[]
|
|
661
|
+
| {
|
|
662
|
+
steps?: number | number[]
|
|
663
|
+
ms?: number | number[]
|
|
664
|
+
fps?: number
|
|
665
|
+
}
|
|
682
666
|
back?: boolean
|
|
683
667
|
hideKeyboard?: boolean
|
|
684
668
|
launchApp?: any
|
|
@@ -1009,6 +993,8 @@ export class SootSimBridgeFlowRunner {
|
|
|
1009
993
|
|
|
1010
994
|
private firstLaunchDone = false
|
|
1011
995
|
private appStopped = false
|
|
996
|
+
private animationClockLocked = false
|
|
997
|
+
private flowDepth = 0
|
|
1012
998
|
private profilingEnabled = false
|
|
1013
999
|
private recordingEnabled = false
|
|
1014
1000
|
private recordingAccessChecked = false
|
|
@@ -1901,7 +1887,7 @@ export class SootSimBridgeFlowRunner {
|
|
|
1901
1887
|
await this.tap(point.x, point.y, point.target)
|
|
1902
1888
|
await repeatTaps(tappedAtMs, () => this.tap(point.x, point.y, point.target))
|
|
1903
1889
|
}
|
|
1904
|
-
await this.
|
|
1890
|
+
await this.waitForAppToSettle()
|
|
1905
1891
|
return
|
|
1906
1892
|
}
|
|
1907
1893
|
// auto-wait like real maestro: if the target is not mounted yet, poll
|
|
@@ -1955,7 +1941,7 @@ export class SootSimBridgeFlowRunner {
|
|
|
1955
1941
|
throw new Error(`tapOn refused: ${JSON.stringify(hit)}`)
|
|
1956
1942
|
if (hit?.hit === true) {
|
|
1957
1943
|
await repeatTaps(tappedAtMs, () => this.tap(cx, cy, replayTarget))
|
|
1958
|
-
await this.
|
|
1944
|
+
await this.waitForAppToSettle()
|
|
1959
1945
|
if (!current.isTextInput) return
|
|
1960
1946
|
// confirm focus landed on THIS text input; retry the tap if a
|
|
1961
1947
|
// previous field still owns focus.
|
|
@@ -1977,7 +1963,7 @@ export class SootSimBridgeFlowRunner {
|
|
|
1977
1963
|
)
|
|
1978
1964
|
: null
|
|
1979
1965
|
if (direct?.ok) {
|
|
1980
|
-
await this.
|
|
1966
|
+
await this.waitForAppToSettle()
|
|
1981
1967
|
return
|
|
1982
1968
|
}
|
|
1983
1969
|
throw new Error(`tapOn: element not found: ${JSON.stringify(opts)}`)
|
|
@@ -1991,12 +1977,12 @@ export class SootSimBridgeFlowRunner {
|
|
|
1991
1977
|
if (result?.ok === false || result?.value === false) {
|
|
1992
1978
|
throw new Error(`longPressOn: long press missed: ${JSON.stringify(opts)}`)
|
|
1993
1979
|
}
|
|
1994
|
-
await this.
|
|
1980
|
+
await this.waitForAppToSettle()
|
|
1995
1981
|
}
|
|
1996
1982
|
|
|
1997
1983
|
async tapAtCoords(x: number, y: number) {
|
|
1998
1984
|
await this.tap(x, y)
|
|
1999
|
-
await this.
|
|
1985
|
+
await this.waitForAppToSettle()
|
|
2000
1986
|
}
|
|
2001
1987
|
|
|
2002
1988
|
async doubleTapAtCoords(x: number, y: number, gapMs = 80) {
|
|
@@ -2008,7 +1994,7 @@ export class SootSimBridgeFlowRunner {
|
|
|
2008
1994
|
`doubleTapAtCoords failed at (${x}, ${y}): ${result?.error ?? 'double tap missed'}`,
|
|
2009
1995
|
)
|
|
2010
1996
|
}
|
|
2011
|
-
await this.
|
|
1997
|
+
await this.waitForAppToSettle()
|
|
2012
1998
|
}
|
|
2013
1999
|
|
|
2014
2000
|
async assertVisible(target: string | { id?: string; text?: string }) {
|
|
@@ -2189,48 +2175,58 @@ export class SootSimBridgeFlowRunner {
|
|
|
2189
2175
|
await sleep(120)
|
|
2190
2176
|
}
|
|
2191
2177
|
|
|
2178
|
+
// maestro's android driver sends Back. its ios driver never presses a key:
|
|
2179
|
+
// while the keyboard shows it swipes from the screen center up 3%, then
|
|
2180
|
+
// left 3%, and the app's own handling of those touches dismisses it, or
|
|
2181
|
+
// presses whatever sits under the center
|
|
2192
2182
|
async hideKeyboard() {
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
await this.callTest<boolean>('blurFocusedTextInput')
|
|
2199
|
-
} catch {
|
|
2200
|
-
// fall through to the shell-level dismiss below; older bridges may not
|
|
2201
|
-
// expose the text-input helper yet.
|
|
2183
|
+
if (this.js.maestro.platform.toLowerCase() === 'android') {
|
|
2184
|
+
await this.back()
|
|
2185
|
+
await sleep(300)
|
|
2186
|
+
await this.waitForAppToSettle()
|
|
2187
|
+
return
|
|
2202
2188
|
}
|
|
2189
|
+
if (!(await this.isKeyboardVisible())) return
|
|
2190
|
+
const screen = await this.screenBounds()
|
|
2191
|
+
const center = parsePoint('50%,50%', screen)
|
|
2192
|
+
await this.xctestSwipe(center, parsePoint('50%,47%', screen), 50)
|
|
2193
|
+
if (!(await this.isKeyboardVisible())) return
|
|
2194
|
+
await this.xctestSwipe(center, parsePoint('47%,50%', screen), 50)
|
|
2195
|
+
await this.waitForAppToSettle()
|
|
2196
|
+
}
|
|
2203
2197
|
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
}
|
|
2198
|
+
// the xctest runner's swipe: press, move to the end over its 100ms tap
|
|
2199
|
+
// duration, rest there for the requested duration, then lift, so the
|
|
2200
|
+
// finger leaves at rest rather than flinging
|
|
2201
|
+
private async xctestSwipe(
|
|
2202
|
+
from: { x: number; y: number },
|
|
2203
|
+
to: { x: number; y: number },
|
|
2204
|
+
durationMs: number,
|
|
2205
|
+
) {
|
|
2206
|
+
const moves = 6
|
|
2207
|
+
const steps: PerformStep[] = [{ type: 'touchDown', x: from.x, y: from.y }]
|
|
2208
|
+
for (let i = 1; i <= moves; i++) {
|
|
2209
|
+
steps.push({ type: 'wait', ms: 100 / moves })
|
|
2210
|
+
steps.push({
|
|
2211
|
+
type: 'touchMove',
|
|
2212
|
+
x: from.x + ((to.x - from.x) * i) / moves,
|
|
2213
|
+
y: from.y + ((to.y - from.y) * i) / moves,
|
|
2214
|
+
})
|
|
2215
|
+
}
|
|
2216
|
+
steps.push({ type: 'wait', ms: durationMs })
|
|
2217
|
+
steps.push({ type: 'touchUp', x: to.x, y: to.y })
|
|
2218
|
+
const result = await this.perform(steps)
|
|
2219
|
+
if (!result?.ok) throw new Error(`swipe failed: ${result?.error ?? 'swipe missed'}`)
|
|
2220
|
+
await this.waitForAppToSettle()
|
|
2221
|
+
}
|
|
2209
2222
|
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
}>(`(async () => {
|
|
2218
|
-
const keyboard =
|
|
2219
|
-
window.__sootsimKeyboard ??
|
|
2220
|
-
window.SootSim?.bridges?.keyboard ??
|
|
2221
|
-
null
|
|
2222
|
-
const test = window.__sootsimTest
|
|
2223
|
-
return {
|
|
2224
|
-
visible: !!keyboard?.isVisible?.(),
|
|
2225
|
-
focused: !!(await test?.isTextInputFocused?.()),
|
|
2226
|
-
}
|
|
2227
|
-
})()`)
|
|
2228
|
-
if (!state?.visible && !state?.focused) {
|
|
2229
|
-
await sleep(80)
|
|
2230
|
-
return
|
|
2231
|
-
}
|
|
2232
|
-
await sleep(80)
|
|
2233
|
-
} while (Date.now() < deadline)
|
|
2223
|
+
private async isKeyboardVisible(): Promise<boolean> {
|
|
2224
|
+
const visible = await this.evaluate<boolean>(`(() => {
|
|
2225
|
+
const keyboard =
|
|
2226
|
+
window.__sootsimKeyboard ?? window.SootSim?.bridges?.keyboard ?? null
|
|
2227
|
+
return !!keyboard?.isVisible?.()
|
|
2228
|
+
})()`)
|
|
2229
|
+
return visible === true
|
|
2234
2230
|
}
|
|
2235
2231
|
|
|
2236
2232
|
async eraseText(count: number) {
|
|
@@ -2549,7 +2545,7 @@ export class SootSimBridgeFlowRunner {
|
|
|
2549
2545
|
if (!result?.ok) {
|
|
2550
2546
|
throw new Error(`drag failed: ${result?.error ?? 'drag missed'}`)
|
|
2551
2547
|
}
|
|
2552
|
-
await this.
|
|
2548
|
+
await this.waitForAppToSettle()
|
|
2553
2549
|
}
|
|
2554
2550
|
|
|
2555
2551
|
async swipeCoords(start: string, end: string, duration = 400) {
|
|
@@ -2660,6 +2656,76 @@ export class SootSimBridgeFlowRunner {
|
|
|
2660
2656
|
})
|
|
2661
2657
|
}
|
|
2662
2658
|
|
|
2659
|
+
// step peach's lockstep animation clock by one or more explicit millisecond
|
|
2660
|
+
// deltas and hold the clock parked at the final frame so a subsequent
|
|
2661
|
+
// takeScreenshot or assertion reads that exact presentation phase.
|
|
2662
|
+
async advanceAnimationClock(spec: NonNullable<MaestroStep['advanceAnimationClock']>) {
|
|
2663
|
+
const rawSteps =
|
|
2664
|
+
typeof spec === 'number' || Array.isArray(spec) ? spec : (spec.steps ?? spec.ms)
|
|
2665
|
+
const steps = (Array.isArray(rawSteps) ? rawSteps : [rawSteps]).map((value) =>
|
|
2666
|
+
Number(value),
|
|
2667
|
+
)
|
|
2668
|
+
if (steps.length === 0 || steps.some((ms) => !Number.isFinite(ms) || ms <= 0)) {
|
|
2669
|
+
throw new Error(
|
|
2670
|
+
`advanceAnimationClock requires positive millisecond step(s): ${JSON.stringify(spec)}`,
|
|
2671
|
+
)
|
|
2672
|
+
}
|
|
2673
|
+
const fps =
|
|
2674
|
+
typeof spec === 'object' &&
|
|
2675
|
+
!Array.isArray(spec) &&
|
|
2676
|
+
typeof spec.fps === 'number' &&
|
|
2677
|
+
Number.isFinite(spec.fps) &&
|
|
2678
|
+
spec.fps > 0
|
|
2679
|
+
? spec.fps
|
|
2680
|
+
: 60
|
|
2681
|
+
const result = await this.evaluate<{
|
|
2682
|
+
ok: boolean
|
|
2683
|
+
error?: string
|
|
2684
|
+
virtualTimestampMs?: number
|
|
2685
|
+
frameIndex?: number
|
|
2686
|
+
}>(
|
|
2687
|
+
`(async () => {
|
|
2688
|
+
const lockstep = window.SootSim?.bridges?.lockstepCapture
|
|
2689
|
+
if (!lockstep || typeof lockstep.begin !== 'function') {
|
|
2690
|
+
return { ok: false, error: 'lockstep capture bridge unavailable' }
|
|
2691
|
+
}
|
|
2692
|
+
let controller = window.__peachFlowAnimationClockController
|
|
2693
|
+
if (!controller) {
|
|
2694
|
+
controller = await lockstep.begin(${JSON.stringify(fps)})
|
|
2695
|
+
window.__peachFlowAnimationClockController = controller
|
|
2696
|
+
}
|
|
2697
|
+
let receipt = controller.initialFrame
|
|
2698
|
+
for (const ms of ${JSON.stringify(steps)}) {
|
|
2699
|
+
receipt = await controller.stepFrame(ms)
|
|
2700
|
+
}
|
|
2701
|
+
return {
|
|
2702
|
+
ok: true,
|
|
2703
|
+
virtualTimestampMs: receipt?.virtualTimestampMs,
|
|
2704
|
+
frameIndex: receipt?.frameIndex,
|
|
2705
|
+
}
|
|
2706
|
+
})()`,
|
|
2707
|
+
60_000,
|
|
2708
|
+
)
|
|
2709
|
+
if (!result?.ok) {
|
|
2710
|
+
throw new Error(`advanceAnimationClock failed: ${result?.error ?? 'unknown error'}`)
|
|
2711
|
+
}
|
|
2712
|
+
this.animationClockLocked = true
|
|
2713
|
+
}
|
|
2714
|
+
|
|
2715
|
+
private async releaseAnimationClock() {
|
|
2716
|
+
if (!this.animationClockLocked) return
|
|
2717
|
+
this.animationClockLocked = false
|
|
2718
|
+
await this.evaluate(`(() => {
|
|
2719
|
+
const controller = window.__peachFlowAnimationClockController
|
|
2720
|
+
window.__peachFlowAnimationClockController = null
|
|
2721
|
+
if (controller && typeof controller.end === 'function') {
|
|
2722
|
+
controller.end()
|
|
2723
|
+
return
|
|
2724
|
+
}
|
|
2725
|
+
window.SootSim?.bridges?.lockstepCapture?.end?.()
|
|
2726
|
+
})()`).catch(() => {})
|
|
2727
|
+
}
|
|
2728
|
+
|
|
2663
2729
|
async back() {
|
|
2664
2730
|
const dispatched = await this.bridge.send({
|
|
2665
2731
|
type: 'call',
|
|
@@ -2709,8 +2775,15 @@ export class SootSimBridgeFlowRunner {
|
|
|
2709
2775
|
: { x: centerX, y: screen.height * 0.1 }
|
|
2710
2776
|
const element = await this.findElement(target)
|
|
2711
2777
|
if (element) {
|
|
2712
|
-
|
|
2713
|
-
const { width, height } =
|
|
2778
|
+
// maestro computes visibility from integer edges, as it does selection.
|
|
2779
|
+
const { x, y, width, height } = accessibilityBounds({
|
|
2780
|
+
frame: {
|
|
2781
|
+
x: element.absolutePosition.x,
|
|
2782
|
+
y: element.absolutePosition.y,
|
|
2783
|
+
width: element.layout.width,
|
|
2784
|
+
height: element.layout.height,
|
|
2785
|
+
},
|
|
2786
|
+
})
|
|
2714
2787
|
const shownX = Math.max(0, Math.min(x + width, screen.width) - Math.max(x, 0))
|
|
2715
2788
|
const shownY = Math.max(0, Math.min(y + height, screen.height) - Math.max(y, 0))
|
|
2716
2789
|
const share = width * height > 0 ? (shownX * shownY) / (width * height) : 0
|
|
@@ -2836,6 +2909,12 @@ export class SootSimBridgeFlowRunner {
|
|
|
2836
2909
|
const wantsClear = !!(opts && typeof opts === 'object' && opts.clearState)
|
|
2837
2910
|
const wantsResetRuntime = readBooleanKey(opts, 'resetRuntime')
|
|
2838
2911
|
const wantsForeground = isObjectRecord(opts) && opts.stopApp === false
|
|
2912
|
+
// maestro's launchApp clears the keychain, then app state, then sets
|
|
2913
|
+
// permissions and launches
|
|
2914
|
+
if (readBooleanKey(opts, 'clearKeychain')) {
|
|
2915
|
+
if (!this.firstLaunchDone && !this.appStopped) await this.waitForTree(120000)
|
|
2916
|
+
await this.clearKeychain()
|
|
2917
|
+
}
|
|
2839
2918
|
await this.setLaunchPermissions(isObjectRecord(opts) ? opts.permissions : undefined)
|
|
2840
2919
|
|
|
2841
2920
|
if (!this.firstLaunchDone) {
|
|
@@ -2926,9 +3005,7 @@ export class SootSimBridgeFlowRunner {
|
|
|
2926
3005
|
|
|
2927
3006
|
private async setLaunchPermissions(permissions: unknown) {
|
|
2928
3007
|
const statuses = maestroLaunchPermissions(permissions)
|
|
2929
|
-
const
|
|
2930
|
-
const appId = readStringKey(state, 'activeApp') || readRecentShellAppId(state)
|
|
2931
|
-
if (!appId) throw new Error('launchApp: no simulator app to set permissions for')
|
|
3008
|
+
const appId = await this.callTest<string>('permissionGuestAppId')
|
|
2932
3009
|
for (const [kind, status] of statuses) {
|
|
2933
3010
|
await this.callTest('permissionSetAppPermission', appId, kind, {
|
|
2934
3011
|
status,
|
|
@@ -2936,14 +3013,8 @@ export class SootSimBridgeFlowRunner {
|
|
|
2936
3013
|
}
|
|
2937
3014
|
}
|
|
2938
3015
|
|
|
2939
|
-
//
|
|
2940
|
-
//
|
|
2941
|
-
// still in flight (a submit spinner, a sheet closing) finishes before the
|
|
2942
|
-
// next command resolves its target. compare consecutive screen pixels,
|
|
2943
|
-
// rather than requiring frame publications to stop for half a second:
|
|
2944
|
-
// a focused input keeps repainting its native blinking caret.
|
|
2945
|
-
// a screen that never stops proceeds when the budget runs out, as there,
|
|
2946
|
-
// and names what kept it moving: every such step pays the whole budget.
|
|
3016
|
+
// ios checks pixels for three seconds, then compares native hierarchies.
|
|
3017
|
+
// the observer overlaps those reads to keep this command's existing budget.
|
|
2947
3018
|
private async waitForAppToSettle() {
|
|
2948
3019
|
try {
|
|
2949
3020
|
const result = await waitForSootsimIdle({
|
|
@@ -2951,7 +3022,7 @@ export class SootSimBridgeFlowRunner {
|
|
|
2951
3022
|
simId: this.opts.simId,
|
|
2952
3023
|
maxMs: 3000,
|
|
2953
3024
|
pollMs: 32,
|
|
2954
|
-
|
|
3025
|
+
maestro: true,
|
|
2955
3026
|
paintStill: true,
|
|
2956
3027
|
})
|
|
2957
3028
|
if (!result.settled) {
|
|
@@ -2968,11 +3039,6 @@ export class SootSimBridgeFlowRunner {
|
|
|
2968
3039
|
}
|
|
2969
3040
|
}
|
|
2970
3041
|
|
|
2971
|
-
private async settleAfterInput() {
|
|
2972
|
-
await this.waitForAppToSettle()
|
|
2973
|
-
this.lastInteractionAt = Date.now()
|
|
2974
|
-
}
|
|
2975
|
-
|
|
2976
3042
|
private async waitForVisualSettle() {
|
|
2977
3043
|
const result = await waitForSootsimIdle({
|
|
2978
3044
|
bridge: this.bridge,
|
|
@@ -2980,19 +3046,10 @@ export class SootSimBridgeFlowRunner {
|
|
|
2980
3046
|
maxMs: 10_000,
|
|
2981
3047
|
pollMs: 32,
|
|
2982
3048
|
stablePolls: 3,
|
|
2983
|
-
|
|
3049
|
+
maestro: true,
|
|
2984
3050
|
paintStill: true,
|
|
2985
3051
|
})
|
|
2986
3052
|
if (!result.settled) {
|
|
2987
|
-
// an app loop that never ends (a pulsing status dot) leaves only the
|
|
2988
|
-
// pixels moving. maestro on ios captures that frame and continues, so
|
|
2989
|
-
// do the same; any other blocker is real unfinished work and still fails.
|
|
2990
|
-
if (result.blockedBy.startsWith('screen pixels still changing over ')) {
|
|
2991
|
-
console.warn(
|
|
2992
|
-
` [flow] screen never became still in ${result.elapsed}ms; capturing as maestro does`,
|
|
2993
|
-
)
|
|
2994
|
-
return
|
|
2995
|
-
}
|
|
2996
3053
|
throw new Error(
|
|
2997
3054
|
`visual state did not settle within ${result.elapsed}ms (${result.blockedBy})`,
|
|
2998
3055
|
)
|
|
@@ -3373,8 +3430,6 @@ export class SootSimBridgeFlowRunner {
|
|
|
3373
3430
|
step.extendedWaitUntil ||
|
|
3374
3431
|
step.waitFor ||
|
|
3375
3432
|
step.scrollUntilVisible ||
|
|
3376
|
-
// deep links own the same bounded guest launch and tree waits.
|
|
3377
|
-
step.openLink ||
|
|
3378
3433
|
// runScript http calls are synchronous with a 5-minute upstream
|
|
3379
3434
|
// timeout — the 10s watchdog would kill legitimate setup scripts.
|
|
3380
3435
|
step.runScript ||
|
|
@@ -3386,6 +3441,7 @@ export class SootSimBridgeFlowRunner {
|
|
|
3386
3441
|
// waitForAnimationToEnd ends at its own timeout, which a flow may set
|
|
3387
3442
|
// past 10s, and continues as maestro does on a screen still moving.
|
|
3388
3443
|
step.waitForAnimationToEnd ||
|
|
3444
|
+
step.advanceAnimationClock ||
|
|
3389
3445
|
// assertions wait out maestro's lookup window, longer than 10s, and
|
|
3390
3446
|
// every query they make times out on its own, so they report their
|
|
3391
3447
|
// own failure instead of the watchdog's guess.
|
|
@@ -3399,7 +3455,9 @@ export class SootSimBridgeFlowRunner {
|
|
|
3399
3455
|
// launchApp self-governs with an internal waitForTree (120s on a
|
|
3400
3456
|
// clearState reload): a cache-cleared reload of a heavy bundle re-fetches
|
|
3401
3457
|
// every chunk from metro and routinely needs more than the step ceiling.
|
|
3402
|
-
|
|
3458
|
+
// openLink relaunches the guest through the same bounded reload and tree
|
|
3459
|
+
// wait, so it governs itself the same way.
|
|
3460
|
+
if (isContainer || step.launchApp || step.openLink) {
|
|
3403
3461
|
const result = await body
|
|
3404
3462
|
if (typeof result === 'string') screenshotPath = result
|
|
3405
3463
|
} else {
|
|
@@ -3423,6 +3481,11 @@ export class SootSimBridgeFlowRunner {
|
|
|
3423
3481
|
if (timer) clearTimeout(timer)
|
|
3424
3482
|
}
|
|
3425
3483
|
}
|
|
3484
|
+
// Orchestra.executeCommand restarts the lookup window after every
|
|
3485
|
+
// command that returns mutating; containers leave it to their children.
|
|
3486
|
+
if (!isContainer && !NON_MUTATING_STEPS.some((key) => step[key] !== undefined)) {
|
|
3487
|
+
this.lastInteractionAt = Date.now()
|
|
3488
|
+
}
|
|
3426
3489
|
const isAssertion = !!(
|
|
3427
3490
|
step.assertVisible ||
|
|
3428
3491
|
step.assertNotVisible ||
|
|
@@ -3480,6 +3543,25 @@ export class SootSimBridgeFlowRunner {
|
|
|
3480
3543
|
}
|
|
3481
3544
|
|
|
3482
3545
|
private async runStepInner(step: MaestroStep) {
|
|
3546
|
+
if (
|
|
3547
|
+
this.animationClockLocked &&
|
|
3548
|
+
!step.advanceAnimationClock &&
|
|
3549
|
+
!step.takeScreenshot &&
|
|
3550
|
+
!step.assertVisible &&
|
|
3551
|
+
!step.assertNotVisible &&
|
|
3552
|
+
step.assertTrue === undefined &&
|
|
3553
|
+
!step.assertTreeContains &&
|
|
3554
|
+
!step.dumpTree &&
|
|
3555
|
+
step.copyTextFrom === undefined &&
|
|
3556
|
+
typeof step.evalScript !== 'string' &&
|
|
3557
|
+
!step.runScript &&
|
|
3558
|
+
step.setClipboard === undefined &&
|
|
3559
|
+
!step.runFlow &&
|
|
3560
|
+
!step.repeat &&
|
|
3561
|
+
!step.retry
|
|
3562
|
+
) {
|
|
3563
|
+
await this.releaseAnimationClock()
|
|
3564
|
+
}
|
|
3483
3565
|
if (step.tapOn) await this.tapOn(step.tapOn)
|
|
3484
3566
|
else if (step.doubleTapOn) {
|
|
3485
3567
|
// maestro YamlFluentCommand: doubleTapOn is tapOn with TapRepeat(2, delay)
|
|
@@ -3534,6 +3616,8 @@ export class SootSimBridgeFlowRunner {
|
|
|
3534
3616
|
? waitSpec.timeout
|
|
3535
3617
|
: 2000,
|
|
3536
3618
|
)
|
|
3619
|
+
} else if (step.advanceAnimationClock !== undefined) {
|
|
3620
|
+
await this.advanceAnimationClock(step.advanceAnimationClock)
|
|
3537
3621
|
} else if (step.back) {
|
|
3538
3622
|
await this.back()
|
|
3539
3623
|
} else if (step.hideKeyboard) {
|
|
@@ -3766,41 +3850,49 @@ export class SootSimBridgeFlowRunner {
|
|
|
3766
3850
|
async runFlow(steps: (MaestroStep | string)[], options: { skipPlan?: boolean } = {}) {
|
|
3767
3851
|
const { onStart, onComplete, body } = this.extractFlowBody(steps)
|
|
3768
3852
|
this.lastInteractionAt = Date.now()
|
|
3853
|
+
this.flowDepth++
|
|
3769
3854
|
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3855
|
+
try {
|
|
3856
|
+
if (!options.skipPlan && this.liveStatus) {
|
|
3857
|
+
await this.planFlow(steps)
|
|
3858
|
+
}
|
|
3773
3859
|
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3860
|
+
if (onStart.length > 0) {
|
|
3861
|
+
console.log(`[flow] onFlowStart (${onStart.length} steps)`)
|
|
3862
|
+
for (let i = 0; i < onStart.length; i++) {
|
|
3863
|
+
await this.runStep(onStart[i], i)
|
|
3864
|
+
}
|
|
3778
3865
|
}
|
|
3779
|
-
}
|
|
3780
3866
|
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3867
|
+
let caughtError: Error | null = null
|
|
3868
|
+
try {
|
|
3869
|
+
await this.runFlowBody(body)
|
|
3870
|
+
} catch (err) {
|
|
3871
|
+
caughtError = err instanceof Error ? err : new Error(String(err))
|
|
3872
|
+
}
|
|
3787
3873
|
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3874
|
+
if (onComplete.length > 0) {
|
|
3875
|
+
console.log(`[flow] onFlowComplete (${onComplete.length} steps)`)
|
|
3876
|
+
for (let i = 0; i < onComplete.length; i++) {
|
|
3877
|
+
try {
|
|
3878
|
+
await this.runStep(onComplete[i], i)
|
|
3879
|
+
} catch (hookErr) {
|
|
3880
|
+
console.warn(
|
|
3881
|
+
`[flow] onFlowComplete step ${i + 1} failed: ${
|
|
3882
|
+
hookErr instanceof Error ? hookErr.message : hookErr
|
|
3883
|
+
}`,
|
|
3884
|
+
)
|
|
3885
|
+
}
|
|
3799
3886
|
}
|
|
3800
3887
|
}
|
|
3801
|
-
}
|
|
3802
3888
|
|
|
3803
|
-
|
|
3889
|
+
if (caughtError) throw caughtError
|
|
3890
|
+
} finally {
|
|
3891
|
+
this.flowDepth = Math.max(0, this.flowDepth - 1)
|
|
3892
|
+
if (this.flowDepth === 0) {
|
|
3893
|
+
await this.releaseAnimationClock()
|
|
3894
|
+
}
|
|
3895
|
+
}
|
|
3804
3896
|
}
|
|
3805
3897
|
|
|
3806
3898
|
private async runFlowBody(steps: MaestroStep[]) {
|