peachsim 0.1.1635 → 0.1.1641
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 +10 -1
- package/cli/app-config.ts +14 -5
- package/cli/app-stubs.ts +96 -22
- package/cli/bridge-flow-runner.ts +2 -2
- package/cli/cloud-client.ts +60 -40
- package/cli/cloud-session.ts +16 -0
- package/cli/commands/assert.ts +17 -4
- package/cli/commands/box.ts +33 -19
- package/cli/commands/doctor.ts +37 -20
- package/cli/commands/flow.ts +1 -1
- package/cli/commands/inspect/actions.ts +3 -2
- package/cli/commands/inspect/core.ts +0 -8
- package/cli/commands/inspect/shared.ts +9 -4
- package/cli/commands/inspect.ts +65 -61
- package/cli/commands/platform.ts +2 -1
- package/cli/commands/preview.ts +1 -1
- package/cli/commands/record.ts +154 -68
- package/cli/commands/swift.ts +18 -4
- package/cli/commands/three-mode.ts +4 -38
- package/cli/commands/upload.ts +55 -49
- package/cli/drivers/playwright-sim-host.ts +65 -42
- package/cli/flow-live-status.ts +0 -2
- package/cli/main.ts +2 -2
- package/cli/outbound-endpoints.ts +7 -5
- package/cli/pause-aware-timeout.ts +4 -0
- package/cli/ws-bridge.ts +19 -16
- package/detox/attempt-events.cjs +99 -6
- package/detox/attempt-events.d.cts +7 -0
- package/detox/expectations.ts +7 -3
- package/detox/index.ts +259 -139
- package/detox/jest-environment.cjs +5 -1
- package/detox/motion-change.cjs +170 -21
- package/detox/motion-change.d.cts +32 -0
- package/detox/navigation-lifecycle.ts +35 -3
- package/dist-lib/agent-daemon-client.cjs +1 -1
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/attached-projects.cjs +1 -1
- package/dist-lib/auth/shared-session.cjs +1 -1
- package/dist-lib/backend-origin.cjs +17 -7
- 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 +5014 -2268
- package/dist-lib/capture-contract.cjs +1 -1
- package/dist-lib/capture-contract.mjs +1 -1
- package/dist-lib/cloud-contract.cjs +1 -1
- package/dist-lib/cloud-contract.mjs +1 -1
- package/dist-lib/cloud-test-args.cjs +1 -1
- package/dist-lib/cloud-test-args.mjs +1 -1
- package/dist-lib/cloud.cjs +1 -1
- package/dist-lib/cloud.mjs +1 -1
- package/dist-lib/config.cjs +1 -1
- package/dist-lib/config.mjs +1 -1
- package/dist-lib/detox/index.cjs +1036 -166
- package/dist-lib/detox/jest-environment.cjs +5 -1
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/expo-swift-bundle.cjs +4613 -1963
- package/dist-lib/expo-swift-metadata.cjs +9 -7
- package/dist-lib/expo-swift-metadata.mjs +9 -7
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +58072 -16400
- 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 +2790 -143
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/jump-to-source-native.cjs +1 -1
- package/dist-lib/kotlin-host.cjs +1 -1
- package/dist-lib/menu.cjs +1 -1
- package/dist-lib/menu.mjs +1 -1
- package/dist-lib/metro-fingerprint-registry.cjs +1 -1
- package/dist-lib/metro-fingerprint-registry.mjs +1 -1
- package/dist-lib/metro-production-bundle.cjs +1 -1
- package/dist-lib/metro-production-bundle.mjs +1 -1
- package/dist-lib/metro.cjs +2579 -95
- package/dist-lib/native-app-identity-project.cjs +243 -53
- package/dist-lib/native-app-identity-project.mjs +243 -53
- package/dist-lib/native-app-identity.cjs +29 -30
- package/dist-lib/native-app-identity.mjs +29 -30
- 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 +2 -2
- package/dist-lib/react-native-host-modules.mjs +2 -2
- package/dist-lib/scripts/dev-server-scanner.cjs +8 -6
- package/dist-lib/sdk.cjs +4 -3
- package/dist-lib/sdk.mjs +4 -3
- package/dist-lib/skills.cjs +66475 -23097
- package/dist-lib/swift-compile.cjs +452 -260
- package/dist-lib/swift-compile.mjs +451 -252
- package/dist-lib/swift-package.cjs +1 -1
- package/dist-lib/swift-package.mjs +1 -1
- package/dist-lib/swift-submit.cjs +14 -7
- package/dist-lib/swift-submit.mjs +14 -7
- package/dist-lib/swift-toolchain.cjs +316 -41
- package/dist-lib/swift.cjs +1 -1
- package/dist-lib/vite-swift.cjs +8188 -5636
- package/dist-lib/vite.cjs +2857 -12
- package/dist-lib/worklet-executable.cjs +14 -2
- package/dist-lib/worklet-executable.mjs +14 -2
- package/package.json +1 -1
- package/src/backend-origin.ts +21 -6
- package/src/expo-swift-host.ts +5 -2
- package/src/expoKotlinContract.ts +4 -7
- package/src/expoSwiftContract.ts +4 -6
- package/src/host/staged-host-files.ts +9 -0
- package/src/native-app-identity-project.ts +1 -4
- package/src/native-app-identity.ts +1 -4
- package/src/nativeAppIconContract.ts +1 -4
- package/src/output-lock-stdio.ts +36 -0
- package/src/react-native-bridge-modules.ts +106 -78
- package/src/react-native-host-modules.ts +4 -1
- package/src/settings-bundle.ts +1 -4
- package/src/swift-compile.ts +224 -139
- package/src/swift-distribution.lock.json +23 -12
- package/src/swift-distribution.ts +103 -4
- package/src/swift-host.ts +95 -30
- package/src/swift-icu.ts +16 -10
- package/src/swift-objc-compile.ts +547 -0
- package/src/swift-objc-metadata.ts +204 -0
- package/src/swift-objc-sdk.ts +883 -19
- package/src/swift-objc-standin.ts +126 -14
- package/src/swift-package-manifest.ts +42 -1
- package/src/swift-packages.ts +35 -21
- package/src/swift-root-lock.ts +153 -0
- package/src/swift-standin-emit.ts +40 -7
- package/src/swift-standins.ts +404 -138
- package/src/swift-submit.ts +18 -7
- package/src/swift-toolchain.ts +378 -46
- package/src/swiftContract.ts +12 -0
- package/src/worker-system-chunks.ts +11 -12
- package/src/worklet-executable.ts +19 -1
- package/src/xcode-project.ts +280 -33
package/detox/index.ts
CHANGED
|
@@ -14,7 +14,11 @@ import {
|
|
|
14
14
|
type ChromiumLaunchAdmission,
|
|
15
15
|
} from '../../../scripts/lib/reap-browser'
|
|
16
16
|
import { automateWebAuthenticationSessions } from '../../peach-engine/src/web-authentication-automation-host'
|
|
17
|
-
import type {
|
|
17
|
+
import type {
|
|
18
|
+
LockstepInputReceipt,
|
|
19
|
+
LockstepScheduledInput,
|
|
20
|
+
SootSimLockstepFrameReceipt,
|
|
21
|
+
} from '@peach/globals'
|
|
18
22
|
export { reserveChromiumAdmission } from '../../../scripts/lib/reap-browser'
|
|
19
23
|
import {
|
|
20
24
|
readBrowserCompositeCaptureRequest,
|
|
@@ -72,6 +76,8 @@ import type { DiscreteProofContext } from './attempt-events.cjs'
|
|
|
72
76
|
import type { SootSimElement } from './element-types'
|
|
73
77
|
import type { Browser, BrowserContext, Page } from 'playwright-core'
|
|
74
78
|
|
|
79
|
+
const WORKLET_FRAME_TRACE_LIMIT = 16384
|
|
80
|
+
|
|
75
81
|
type DiscreteStageFrame = {
|
|
76
82
|
label: string
|
|
77
83
|
offsetMs: number
|
|
@@ -127,6 +133,7 @@ let _browserLaunch: Promise<void> | null = null
|
|
|
127
133
|
let _browserLaunchAbort: AbortController | null = null
|
|
128
134
|
let _reapBrowserNow: (() => void) | null = null
|
|
129
135
|
let _context: BrowserContext | null = null
|
|
136
|
+
let _appStorageState: Awaited<ReturnType<BrowserContext['storageState']>> | undefined
|
|
130
137
|
let _page: Page | null = null
|
|
131
138
|
let _synchronizationEnabled = false
|
|
132
139
|
let _proofFrameObservationActive = false
|
|
@@ -402,6 +409,7 @@ async function waitForSootsimWorkletsIdle(page: Page, timeout = 3000): Promise<v
|
|
|
402
409
|
let readStartedAt: number | null = null
|
|
403
410
|
let tenantAtRead = window.__sootsimRenderHost
|
|
404
411
|
let shellAtRead = window.SootSim?.bridges?.shellHost
|
|
412
|
+
let peerProgress: import('@peach/globals').PeachPeerStatsProgress | null = null
|
|
405
413
|
let deadlineTimerLateMs: number | null = null
|
|
406
414
|
|
|
407
415
|
while (Date.now() < deadline) {
|
|
@@ -409,8 +417,13 @@ async function waitForSootsimWorkletsIdle(page: Page, timeout = 3000): Promise<v
|
|
|
409
417
|
tenantAtRead = window.__sootsimRenderHost
|
|
410
418
|
shellAtRead = window.SootSim?.bridges?.shellHost
|
|
411
419
|
let timer: ReturnType<typeof setTimeout> | undefined
|
|
420
|
+
const peerRead = getPeerStats(false)
|
|
421
|
+
peerProgress =
|
|
422
|
+
peerRead && 'peachPeerProgress' in peerRead
|
|
423
|
+
? (peerRead.peachPeerProgress ?? null)
|
|
424
|
+
: null
|
|
412
425
|
const stats = await Promise.race([
|
|
413
|
-
|
|
426
|
+
peerRead,
|
|
414
427
|
new Promise<undefined>((resolve) => {
|
|
415
428
|
timer = setTimeout(
|
|
416
429
|
() => {
|
|
@@ -459,6 +472,7 @@ async function waitForSootsimWorkletsIdle(page: Page, timeout = 3000): Promise<v
|
|
|
459
472
|
: {
|
|
460
473
|
startedAtMs: readStartedAt - start,
|
|
461
474
|
elapsedMs: Date.now() - readStartedAt,
|
|
475
|
+
peerProgress: peerProgress ? { ...peerProgress } : null,
|
|
462
476
|
testBridgeChanged: window.__sootsimTest !== testBridge,
|
|
463
477
|
tenant: {
|
|
464
478
|
presentAtRequest: !!tenantAtRead,
|
|
@@ -484,8 +498,12 @@ async function waitForSootsimWorkletsIdle(page: Page, timeout = 3000): Promise<v
|
|
|
484
498
|
)
|
|
485
499
|
if (!result?.settled) {
|
|
486
500
|
const pending = result?.pending ?? 'no peer stats reply'
|
|
487
|
-
throw
|
|
488
|
-
|
|
501
|
+
throw await describeSootsimBridgeFailure(
|
|
502
|
+
page,
|
|
503
|
+
new Error(
|
|
504
|
+
`sootsim worklet runtime did not settle after ${result?.elapsed ?? timeout}ms (pending=${pending}; observation=${JSON.stringify(result?.observation)})`,
|
|
505
|
+
),
|
|
506
|
+
'sootsim worklet runtime did not settle',
|
|
489
507
|
)
|
|
490
508
|
}
|
|
491
509
|
}
|
|
@@ -553,7 +571,8 @@ async function quiesceContextMedia(context: BrowserContext): Promise<void> {
|
|
|
553
571
|
}
|
|
554
572
|
}
|
|
555
573
|
|
|
556
|
-
async function closeContext() {
|
|
574
|
+
async function closeContext(preserveStorage: boolean) {
|
|
575
|
+
if (!preserveStorage) _appStorageState = undefined
|
|
557
576
|
if (_browserCompromised) {
|
|
558
577
|
throw new Error(`sootsim browser is compromised: ${_browserCompromised}`)
|
|
559
578
|
}
|
|
@@ -561,6 +580,15 @@ async function closeContext() {
|
|
|
561
580
|
const context = _context
|
|
562
581
|
const closingContextId = contextIdentity
|
|
563
582
|
try {
|
|
583
|
+
// process termination clears the app runtime, while its persistent stores
|
|
584
|
+
// remain installed until launchApp({ delete: true }) requests a fresh app.
|
|
585
|
+
if (preserveStorage) {
|
|
586
|
+
_appStorageState = await withLifecycleTimeout(
|
|
587
|
+
'context.storageState()',
|
|
588
|
+
closingContextId,
|
|
589
|
+
() => context.storageState({ indexedDB: true }),
|
|
590
|
+
)
|
|
591
|
+
}
|
|
564
592
|
await quiesceContextMedia(context)
|
|
565
593
|
await withLifecycleTimeout('context.close()', closingContextId, () => context.close())
|
|
566
594
|
_context = null
|
|
@@ -575,7 +603,8 @@ async function closeContext() {
|
|
|
575
603
|
}
|
|
576
604
|
}
|
|
577
605
|
|
|
578
|
-
async function closeBrowser() {
|
|
606
|
+
async function closeBrowser(preserveStorage: boolean) {
|
|
607
|
+
if (!preserveStorage) _appStorageState = undefined
|
|
579
608
|
if (!_browser) return
|
|
580
609
|
const browser = _browser
|
|
581
610
|
const reapBrowserNow = _reapBrowserNow
|
|
@@ -585,7 +614,7 @@ async function closeBrowser() {
|
|
|
585
614
|
|
|
586
615
|
let closeError: Error | null = null
|
|
587
616
|
try {
|
|
588
|
-
await closeContext()
|
|
617
|
+
await closeContext(preserveStorage)
|
|
589
618
|
} catch (error) {
|
|
590
619
|
closeError = error instanceof Error ? error : new Error(String(error))
|
|
591
620
|
}
|
|
@@ -897,7 +926,7 @@ async function focusElementForTextEntry(el: SootSimElement): Promise<void> {
|
|
|
897
926
|
// would edit around a caret the tap then moves, so wait for its focus.
|
|
898
927
|
await pollUntil(
|
|
899
928
|
() => page.evaluate(async () => window.__sootsimTest!.getFocusedNode()),
|
|
900
|
-
(focused) => focused !== null && focusIsWithin(focused, node
|
|
929
|
+
(focused) => focused !== null && focusIsWithin(focused, node),
|
|
901
930
|
{
|
|
902
931
|
timeoutMs: 5000,
|
|
903
932
|
label: `SootSim text input ${node.nodeId} to focus`,
|
|
@@ -1441,11 +1470,12 @@ function createSootSimElement(matcher: Matcher): SootSimElement {
|
|
|
1441
1470
|
normalizedStartingPointX = 0.5,
|
|
1442
1471
|
normalizedStartingPointY = 0.5,
|
|
1443
1472
|
) {
|
|
1473
|
+
const page = getPage()
|
|
1474
|
+
if (_synchronizationEnabled) await waitForSootsimIdle(page, 3000, false, 0)
|
|
1444
1475
|
const node = await findStableNodeByMatcher(matcher, 'swipe')
|
|
1445
1476
|
if (!node)
|
|
1446
1477
|
throw new Error(`element not found for swipe: ${JSON.stringify(matcher)}`)
|
|
1447
1478
|
|
|
1448
|
-
const page = getPage()
|
|
1449
1479
|
const { absolutePosition, layout } = node
|
|
1450
1480
|
|
|
1451
1481
|
const startSootSimX = absolutePosition.x + layout.width * normalizedStartingPointX
|
|
@@ -1972,10 +2002,11 @@ export const device = {
|
|
|
1972
2002
|
}
|
|
1973
2003
|
|
|
1974
2004
|
if (!_page || newInstance || opts?.delete || opts?.recordVideoDir) {
|
|
1975
|
-
await closeContext()
|
|
2005
|
+
await closeContext(!opts?.delete)
|
|
1976
2006
|
const contextOpts: Parameters<NonNullable<typeof _browser>['newContext']>[0] = {
|
|
1977
2007
|
hasTouch: true,
|
|
1978
2008
|
viewport: { width: 500, height: 900 },
|
|
2009
|
+
storageState: _appStorageState,
|
|
1979
2010
|
}
|
|
1980
2011
|
if (opts?.recordVideoDir) {
|
|
1981
2012
|
fs.mkdirSync(opts.recordVideoDir, { recursive: true })
|
|
@@ -2090,7 +2121,7 @@ export const device = {
|
|
|
2090
2121
|
},
|
|
2091
2122
|
|
|
2092
2123
|
async terminateApp() {
|
|
2093
|
-
await closeBrowser()
|
|
2124
|
+
await closeBrowser(true)
|
|
2094
2125
|
},
|
|
2095
2126
|
|
|
2096
2127
|
async installApp() {
|
|
@@ -2098,7 +2129,7 @@ export const device = {
|
|
|
2098
2129
|
},
|
|
2099
2130
|
|
|
2100
2131
|
async uninstallApp() {
|
|
2101
|
-
|
|
2132
|
+
await closeBrowser(false)
|
|
2102
2133
|
},
|
|
2103
2134
|
|
|
2104
2135
|
async openURL(url: { url: string; sourceApp?: string }) {
|
|
@@ -2617,22 +2648,26 @@ export const device = {
|
|
|
2617
2648
|
}>,
|
|
2618
2649
|
options: {
|
|
2619
2650
|
detectionTimeoutMs?: number
|
|
2651
|
+
workletFrameTimestampsSeconds?: number[]
|
|
2652
|
+
workletFrameTracePath?: string
|
|
2620
2653
|
frame?: { height: number; width: number; x: number; y: number } | null
|
|
2621
2654
|
// a pointer the capture itself presses at the trigger point and releases
|
|
2622
2655
|
// once durationMs of lockstep time has elapsed, so held states (a lifted
|
|
2623
2656
|
// control thumb, a pressed row) are painted mid-hold. moves drag the
|
|
2624
2657
|
// held pointer to each point once afterMs of lockstep time has elapsed
|
|
2625
|
-
// since the press
|
|
2658
|
+
// since the press. an explicit release position preserves witnessed up coordinates.
|
|
2626
2659
|
hold?: {
|
|
2627
2660
|
durationMs: number
|
|
2628
|
-
|
|
2661
|
+
eventDurationMs?: number
|
|
2662
|
+
release?: { x: number; y: number }
|
|
2663
|
+
moves?: Array<{ afterMs: number; eventAfterMs?: number; x: number; y: number }>
|
|
2629
2664
|
x: number
|
|
2630
2665
|
y: number
|
|
2631
2666
|
} | null
|
|
2632
|
-
// stages measure from the
|
|
2633
|
-
// than from the first visible
|
|
2634
|
-
// native lane uses.
|
|
2635
|
-
|
|
2667
|
+
// stages measure from the hold's press ('down') or the frame that
|
|
2668
|
+
// delivers its release ('up') rather than from the first visible
|
|
2669
|
+
// change, the delivered touch anchor the native lane uses.
|
|
2670
|
+
anchorAtTouch?: 'down' | 'up'
|
|
2636
2671
|
rootFrame?: { height: number; width: number; x: number; y: number } | null
|
|
2637
2672
|
settleMs?: number
|
|
2638
2673
|
trigger?: () => unknown
|
|
@@ -2681,6 +2716,14 @@ export const device = {
|
|
|
2681
2716
|
definition: MOTION_CHANGE_DEFINITION,
|
|
2682
2717
|
installGlobal: true,
|
|
2683
2718
|
})
|
|
2719
|
+
if (options.workletFrameTracePath) {
|
|
2720
|
+
await page.evaluate(async (limit) => {
|
|
2721
|
+
const enable = window.SootSim?.bridges?.test?.setShellReanimatedTraceEnabled
|
|
2722
|
+
if (typeof enable !== 'function')
|
|
2723
|
+
throw new Error('worklet trace bridge is absent')
|
|
2724
|
+
await enable(true, true, limit)
|
|
2725
|
+
}, WORKLET_FRAME_TRACE_LIMIT)
|
|
2726
|
+
}
|
|
2684
2727
|
const capturePromise: Promise<{
|
|
2685
2728
|
inputReceipts: LockstepInputReceipt[]
|
|
2686
2729
|
anchors: Array<{
|
|
@@ -2843,82 +2886,94 @@ export const device = {
|
|
|
2843
2886
|
if (input.hold && (!interact || typeof interact.touchDown !== 'function')) {
|
|
2844
2887
|
throw new Error('peach interact bridge is not installed')
|
|
2845
2888
|
}
|
|
2846
|
-
const
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
// against a duration lets rounding push an event past its frame.
|
|
2852
|
-
const moves = input.hold?.moves ?? []
|
|
2853
|
-
const holdState = {
|
|
2854
|
-
nextMove: 0,
|
|
2855
|
-
pressFrameIndex: null as number | null,
|
|
2856
|
-
releaseReceipt: null as SootSimLockstepFrameReceipt | null,
|
|
2857
|
-
released: false,
|
|
2858
|
-
x: input.hold?.x ?? 0,
|
|
2859
|
-
y: input.hold?.y ?? 0,
|
|
2860
|
-
}
|
|
2861
|
-
const holdEventFrameIndex = (afterMs: number) =>
|
|
2862
|
-
(holdState.pressFrameIndex ?? 0) + controller.framesForOffset(afterMs)
|
|
2863
|
-
const nextHoldEventFrameIndex = () => {
|
|
2864
|
-
if (!input.hold || holdState.released || holdState.pressFrameIndex === null) {
|
|
2865
|
-
return null
|
|
2866
|
-
}
|
|
2867
|
-
const move = moves[holdState.nextMove]
|
|
2868
|
-
return holdEventFrameIndex(move ? move.afterMs : input.hold.durationMs)
|
|
2869
|
-
}
|
|
2870
|
-
const advanceHold = async (receipt: SootSimLockstepFrameReceipt) => {
|
|
2871
|
-
if (!input.hold || !interact || holdState.released) return
|
|
2872
|
-
if (holdState.pressFrameIndex === null) {
|
|
2873
|
-
holdState.pressFrameIndex = receipt.frameIndex - 1
|
|
2874
|
-
}
|
|
2875
|
-
for (
|
|
2876
|
-
let move = moves[holdState.nextMove];
|
|
2877
|
-
move && receipt.frameIndex >= holdEventFrameIndex(move.afterMs);
|
|
2878
|
-
move = moves[holdState.nextMove]
|
|
2879
|
-
) {
|
|
2880
|
-
holdState.nextMove++
|
|
2881
|
-
holdState.x = move.x
|
|
2882
|
-
holdState.y = move.y
|
|
2883
|
-
// event time remains on the replay clock when delivery ceilings
|
|
2884
|
-
// to a display tick or several native samples share one frame.
|
|
2885
|
-
await interact.touchMove(
|
|
2886
|
-
move.x,
|
|
2887
|
-
move.y,
|
|
2888
|
-
999,
|
|
2889
|
-
controller.initialFrame.virtualTimestampMs + move.afterMs,
|
|
2889
|
+
const pairedPeerStats = async (reset = false) => {
|
|
2890
|
+
const stats = await captureWindow.SootSim?.bridges?.test?.getPeerStats?.(reset)
|
|
2891
|
+
if (!stats || !Number.isSafeInteger(stats.replayedFrameCallbacks))
|
|
2892
|
+
throw new Error(
|
|
2893
|
+
'paired worklets clock needs native frame delivery diagnostics',
|
|
2890
2894
|
)
|
|
2891
|
-
|
|
2892
|
-
if (receipt.frameIndex < holdEventFrameIndex(input.hold.durationMs)) return
|
|
2893
|
-
holdState.released = true
|
|
2894
|
-
holdState.releaseReceipt = receipt
|
|
2895
|
-
await interact.touchUp(
|
|
2896
|
-
holdState.x,
|
|
2897
|
-
holdState.y,
|
|
2898
|
-
999,
|
|
2899
|
-
controller.initialFrame.virtualTimestampMs + input.hold.durationMs,
|
|
2900
|
-
)
|
|
2895
|
+
return stats
|
|
2901
2896
|
}
|
|
2897
|
+
if (input.workletFrameTimestampsSeconds) await pairedPeerStats(true)
|
|
2898
|
+
const controller = await lockstep.begin(60, {
|
|
2899
|
+
workletFrameTimestampsSeconds: input.workletFrameTimestampsSeconds,
|
|
2900
|
+
})
|
|
2901
|
+
let heldInput: Promise<void> | null = null
|
|
2902
|
+
const releaseReceipt = () =>
|
|
2903
|
+
controller.inputReceipts.find((receipt) => receipt.phase === 'up')
|
|
2902
2904
|
// an anchor is its visible-change frame; its bitmap is kept so a 0ms
|
|
2903
2905
|
// stage can still capture the anchor frame itself.
|
|
2904
|
-
const keptFrames = new Map<
|
|
2906
|
+
const keptFrames = new Map<
|
|
2907
|
+
number,
|
|
2908
|
+
{ bitmap: ImageBitmap; receipt: SootSimLockstepFrameReceipt }
|
|
2909
|
+
>()
|
|
2910
|
+
const keepFrame = (receipt: SootSimLockstepFrameReceipt, bitmap: ImageBitmap) => {
|
|
2911
|
+
keptFrames.get(receipt.frameIndex)?.bitmap.close()
|
|
2912
|
+
keptFrames.set(receipt.frameIndex, { bitmap, receipt })
|
|
2913
|
+
}
|
|
2905
2914
|
try {
|
|
2915
|
+
if (input.anchorAtTouch && !input.hold) {
|
|
2916
|
+
throw new Error('peach touch anchoring needs a hold')
|
|
2917
|
+
}
|
|
2906
2918
|
const baselineBitmap = await captureBitmap()
|
|
2907
2919
|
const baseline = regionPixels(baselineBitmap)
|
|
2908
|
-
|
|
2920
|
+
// the press lands before the first stepped frame, so the screen it
|
|
2921
|
+
// lands on is the baseline itself
|
|
2922
|
+
if (input.anchorAtTouch === 'down')
|
|
2923
|
+
keepFrame(controller.initialFrame, baselineBitmap)
|
|
2924
|
+
else baselineBitmap.close()
|
|
2925
|
+
if (
|
|
2926
|
+
input.workletFrameTimestampsSeconds &&
|
|
2927
|
+
(await pairedPeerStats()).replayedFrameCallbacks > 0
|
|
2928
|
+
)
|
|
2929
|
+
throw new Error('paired worklets callback preceded the Peach trigger')
|
|
2909
2930
|
state.ready = true
|
|
2910
2931
|
if (input.hold && interact) {
|
|
2911
|
-
const
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2932
|
+
const hold = input.hold
|
|
2933
|
+
const moves = hold.moves ?? []
|
|
2934
|
+
const release = hold.release ?? moves.at(-1) ?? hold
|
|
2935
|
+
const events: LockstepScheduledInput[] = [
|
|
2936
|
+
{
|
|
2937
|
+
phase: 'down',
|
|
2938
|
+
x: hold.x,
|
|
2939
|
+
y: hold.y,
|
|
2940
|
+
pointerId: 999,
|
|
2941
|
+
afterMs: 0,
|
|
2942
|
+
async deliver(timestamp) {
|
|
2943
|
+
const hit = await interact.touchDown(hold.x, hold.y, 999, timestamp)
|
|
2944
|
+
if (!hit)
|
|
2945
|
+
throw new Error(`peach motion hold missed at ${hold.x},${hold.y}`)
|
|
2946
|
+
},
|
|
2947
|
+
},
|
|
2948
|
+
...moves.map(
|
|
2949
|
+
(move): LockstepScheduledInput => ({
|
|
2950
|
+
phase: 'move',
|
|
2951
|
+
x: move.x,
|
|
2952
|
+
y: move.y,
|
|
2953
|
+
pointerId: 999,
|
|
2954
|
+
afterMs: move.afterMs,
|
|
2955
|
+
eventAfterMs: move.eventAfterMs,
|
|
2956
|
+
async deliver(timestamp: number) {
|
|
2957
|
+
await interact.touchMove(move.x, move.y, 999, timestamp)
|
|
2958
|
+
},
|
|
2959
|
+
}),
|
|
2960
|
+
),
|
|
2961
|
+
{
|
|
2962
|
+
phase: 'up',
|
|
2963
|
+
x: release.x,
|
|
2964
|
+
y: release.y,
|
|
2965
|
+
pointerId: 999,
|
|
2966
|
+
afterMs: hold.durationMs,
|
|
2967
|
+
eventAfterMs: hold.eventDurationMs,
|
|
2968
|
+
async deliver(timestamp) {
|
|
2969
|
+
await interact.touchUp(release.x, release.y, 999, timestamp)
|
|
2970
|
+
},
|
|
2971
|
+
},
|
|
2972
|
+
]
|
|
2973
|
+
heldInput = controller.scheduleInput(events)
|
|
2974
|
+
// rendering propagates delivery errors; also observe cancellation rejection.
|
|
2975
|
+
heldInput.catch(() => {})
|
|
2976
|
+
await controller.flushFrame()
|
|
2922
2977
|
} else {
|
|
2923
2978
|
while (!state.triggerStarted) {
|
|
2924
2979
|
if (state.cancelled) throw new Error('peach motion capture was cancelled')
|
|
@@ -2947,7 +3002,7 @@ export const device = {
|
|
|
2947
3002
|
if (measurement.passedMinimum) {
|
|
2948
3003
|
visibleStart = receipt
|
|
2949
3004
|
detectedMeasurement = measurement
|
|
2950
|
-
|
|
3005
|
+
keepFrame(visibleStart, candidateBitmap)
|
|
2951
3006
|
} else {
|
|
2952
3007
|
candidateBitmap.close()
|
|
2953
3008
|
}
|
|
@@ -2956,11 +3011,14 @@ export const device = {
|
|
|
2956
3011
|
for (let frame = 0; !visibleStart && frame < detectionFrameLimit; frame++) {
|
|
2957
3012
|
if (state.cancelled) throw new Error('peach motion capture was cancelled')
|
|
2958
3013
|
const receipt = await controller.stepFrame()
|
|
2959
|
-
await advanceHold(receipt)
|
|
2960
3014
|
const candidateBitmap = await captureBitmap()
|
|
2961
|
-
// a
|
|
2962
|
-
if (
|
|
2963
|
-
|
|
3015
|
+
// a touch anchor's stages may precede the first visible change
|
|
3016
|
+
if (
|
|
3017
|
+
input.anchorAtTouch === 'down' ||
|
|
3018
|
+
(input.anchorAtTouch === 'up' &&
|
|
3019
|
+
releaseReceipt()?.frameIndex === receipt.frameIndex)
|
|
3020
|
+
) {
|
|
3021
|
+
keepFrame(receipt, await createImageBitmap(candidateBitmap))
|
|
2964
3022
|
}
|
|
2965
3023
|
const bounds = boundsFor(candidateBitmap)
|
|
2966
3024
|
const measurement = measureChange({
|
|
@@ -2976,8 +3034,7 @@ export const device = {
|
|
|
2976
3034
|
previousBitmap = null
|
|
2977
3035
|
visibleStart = receipt
|
|
2978
3036
|
detectedMeasurement = measurement
|
|
2979
|
-
|
|
2980
|
-
keptFrames.set(receipt.frameIndex, candidateBitmap)
|
|
3037
|
+
keepFrame(receipt, candidateBitmap)
|
|
2981
3038
|
break
|
|
2982
3039
|
}
|
|
2983
3040
|
previousBitmap?.close()
|
|
@@ -2992,27 +3049,24 @@ export const device = {
|
|
|
2992
3049
|
// stages on every platform measure from the first frame that
|
|
2993
3050
|
// differs from baseline: native extraction anchors at its
|
|
2994
3051
|
// detection frame's pts, so the drive starts here too. the
|
|
2995
|
-
// preceding tick is still baseline on both legs. a
|
|
2996
|
-
// instead starts the drive at the
|
|
3052
|
+
// preceding tick is still baseline on both legs. a touch anchor
|
|
3053
|
+
// instead starts the drive at the press or at the frame that
|
|
3054
|
+
// delivers the touch-up.
|
|
2997
3055
|
let driveStart = visibleStart
|
|
2998
3056
|
let currentReceipt = visibleStart
|
|
2999
|
-
if (input.
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
}
|
|
3003
|
-
while (!holdState.released) {
|
|
3057
|
+
if (input.anchorAtTouch === 'down') driveStart = controller.initialFrame
|
|
3058
|
+
if (input.anchorAtTouch === 'up') {
|
|
3059
|
+
while (!releaseReceipt()) {
|
|
3004
3060
|
if (state.cancelled) throw new Error('peach motion capture was cancelled')
|
|
3005
3061
|
const receipt = await controller.stepFrame()
|
|
3006
|
-
await advanceHold(receipt)
|
|
3007
3062
|
currentReceipt = receipt
|
|
3008
|
-
if (
|
|
3009
|
-
keptFrames.set(receipt.frameIndex, await captureBitmap())
|
|
3010
|
-
}
|
|
3063
|
+
if (releaseReceipt()) keepFrame(receipt, await captureBitmap())
|
|
3011
3064
|
}
|
|
3012
|
-
|
|
3065
|
+
const released = releaseReceipt()
|
|
3066
|
+
if (!released) {
|
|
3013
3067
|
throw new Error('peach release anchoring found no release frame')
|
|
3014
3068
|
}
|
|
3015
|
-
driveStart =
|
|
3069
|
+
driveStart = { ...released, tenantFrame: null }
|
|
3016
3070
|
}
|
|
3017
3071
|
const resolvedAnchors = new Map<string, SootSimLockstepFrameReceipt>()
|
|
3018
3072
|
const anchorRecords: Array<{
|
|
@@ -3056,7 +3110,6 @@ export const device = {
|
|
|
3056
3110
|
while (searchReceipt.virtualTimestampMs < searchStartVts) {
|
|
3057
3111
|
if (state.cancelled) throw new Error('peach motion capture was cancelled')
|
|
3058
3112
|
searchReceipt = await controller.stepFrame()
|
|
3059
|
-
await advanceHold(searchReceipt)
|
|
3060
3113
|
}
|
|
3061
3114
|
currentReceipt = searchReceipt
|
|
3062
3115
|
let anchorBitmap = await captureBitmap()
|
|
@@ -3072,7 +3125,6 @@ export const device = {
|
|
|
3072
3125
|
for (let frame = 0; frame < detectionFrameLimit; frame++) {
|
|
3073
3126
|
if (state.cancelled) throw new Error('peach motion capture was cancelled')
|
|
3074
3127
|
const receipt = await controller.stepFrame()
|
|
3075
|
-
await advanceHold(receipt)
|
|
3076
3128
|
const candidateBitmap = await captureBitmap()
|
|
3077
3129
|
const bounds = boundsFor(candidateBitmap)
|
|
3078
3130
|
const candidate = regionPixels(candidateBitmap)
|
|
@@ -3109,8 +3161,7 @@ export const device = {
|
|
|
3109
3161
|
`peach motion anchor ${anchor.id} found no onset past ${onsetThresholdPercent}% within ${input.detectionTimeoutMs}ms of ${anchor.notBeforeMs}ms (pan peak ${panPeakPercent.toFixed(2)}%, later peak ${latePeakPercent.toFixed(2)}%): the gate or the search start is wrong for this surface`,
|
|
3110
3162
|
)
|
|
3111
3163
|
}
|
|
3112
|
-
|
|
3113
|
-
keptFrames.set(anchorReceipt.frameIndex, anchorBitmap)
|
|
3164
|
+
keepFrame(anchorReceipt, anchorBitmap)
|
|
3114
3165
|
resolvedAnchors.set(anchor.id, anchorReceipt)
|
|
3115
3166
|
anchorRecords.push({
|
|
3116
3167
|
changedPixelPercent: onsetMeasurement.changePercent,
|
|
@@ -3126,33 +3177,23 @@ export const device = {
|
|
|
3126
3177
|
for (const stage of input.stages) {
|
|
3127
3178
|
if (state.cancelled) throw new Error('peach motion capture was cancelled')
|
|
3128
3179
|
const { anchorId, receipt: anchorReceipt } = await resolveAnchor(stage)
|
|
3129
|
-
// step through every hold move and the release that fall before
|
|
3130
|
-
// this stage one frame at a time, so the stage paints the pointer
|
|
3131
|
-
// where it belongs rather than a hold the jump skipped past.
|
|
3132
3180
|
const stageFrameIndex =
|
|
3133
3181
|
anchorReceipt.frameIndex + controller.framesForOffset(stage.offsetMs)
|
|
3134
3182
|
const kept = keptFrames.get(stageFrameIndex)
|
|
3135
3183
|
if (kept && stageFrameIndex <= currentReceipt.frameIndex) {
|
|
3136
3184
|
snapshots.push({
|
|
3137
|
-
actualMs:
|
|
3185
|
+
actualMs:
|
|
3186
|
+
kept.receipt.virtualTimestampMs - anchorReceipt.virtualTimestampMs,
|
|
3138
3187
|
anchorId,
|
|
3139
|
-
bitmap: await createImageBitmap(kept),
|
|
3140
|
-
frameIndex:
|
|
3188
|
+
bitmap: await createImageBitmap(kept.bitmap),
|
|
3189
|
+
frameIndex: kept.receipt.frameIndex,
|
|
3141
3190
|
label: stage.label,
|
|
3142
3191
|
offsetMs: stage.offsetMs,
|
|
3143
|
-
virtualTimestampMs:
|
|
3192
|
+
virtualTimestampMs: kept.receipt.virtualTimestampMs,
|
|
3144
3193
|
})
|
|
3145
3194
|
continue
|
|
3146
3195
|
}
|
|
3147
|
-
for (
|
|
3148
|
-
let eventFrameIndex = nextHoldEventFrameIndex();
|
|
3149
|
-
eventFrameIndex !== null && stageFrameIndex > eventFrameIndex;
|
|
3150
|
-
eventFrameIndex = nextHoldEventFrameIndex()
|
|
3151
|
-
) {
|
|
3152
|
-
await advanceHold(await controller.stepFrame())
|
|
3153
|
-
}
|
|
3154
3196
|
const receipt = await controller.stepToOffset(anchorReceipt, stage.offsetMs)
|
|
3155
|
-
await advanceHold(receipt)
|
|
3156
3197
|
currentReceipt = receipt
|
|
3157
3198
|
snapshots.push({
|
|
3158
3199
|
actualMs: receipt.virtualTimestampMs - anchorReceipt.virtualTimestampMs,
|
|
@@ -3164,15 +3205,18 @@ export const device = {
|
|
|
3164
3205
|
virtualTimestampMs: receipt.virtualTimestampMs,
|
|
3165
3206
|
})
|
|
3166
3207
|
}
|
|
3208
|
+
// the native receipt was recorded through the last stage, not the
|
|
3209
|
+
// release and settle frames that follow it.
|
|
3210
|
+
if (input.workletFrameTimestampsSeconds) controller.endWorkletFrameReceipt()
|
|
3167
3211
|
// a hold that outlasts the last stage still moves and releases on its
|
|
3168
3212
|
// own frames; the settle jump below would carry the pointer past them
|
|
3169
3213
|
// and leave it down. the settle then runs from that release.
|
|
3170
3214
|
let releasedOffsetMs = 0
|
|
3171
|
-
while (
|
|
3215
|
+
while (input.hold && !releaseReceipt()) {
|
|
3172
3216
|
const receipt = await controller.stepFrame()
|
|
3173
|
-
await advanceHold(receipt)
|
|
3174
3217
|
releasedOffsetMs = receipt.virtualTimestampMs - driveStart.virtualTimestampMs
|
|
3175
3218
|
}
|
|
3219
|
+
await heldInput
|
|
3176
3220
|
if (input.settleMs > 0) {
|
|
3177
3221
|
const maximumPrimaryMs = snapshots.reduce(
|
|
3178
3222
|
(maximum, snapshot) =>
|
|
@@ -3189,6 +3233,20 @@ export const device = {
|
|
|
3189
3233
|
await controller.stepFrame()
|
|
3190
3234
|
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
3191
3235
|
}
|
|
3236
|
+
if (input.workletFrameTimestampsSeconds) {
|
|
3237
|
+
const stats = await pairedPeerStats()
|
|
3238
|
+
const first = stats.nativeFrameFirstTimestampMs
|
|
3239
|
+
const firstFrameIndex =
|
|
3240
|
+
first === null
|
|
3241
|
+
? null
|
|
3242
|
+
: Math.round(
|
|
3243
|
+
(first - controller.initialFrame.virtualTimestampMs) / (1000 / 60),
|
|
3244
|
+
)
|
|
3245
|
+
if (firstFrameIndex !== 1)
|
|
3246
|
+
throw new Error(
|
|
3247
|
+
`paired worklets first delivery did not match the next UI frame: ${firstFrameIndex}`,
|
|
3248
|
+
)
|
|
3249
|
+
}
|
|
3192
3250
|
return {
|
|
3193
3251
|
inputReceipts: controller.inputReceipts,
|
|
3194
3252
|
anchors: anchorRecords,
|
|
@@ -3209,15 +3267,16 @@ export const device = {
|
|
|
3209
3267
|
},
|
|
3210
3268
|
}
|
|
3211
3269
|
} finally {
|
|
3212
|
-
for (const
|
|
3270
|
+
for (const kept of keptFrames.values()) kept.bitmap.close()
|
|
3213
3271
|
controller.end()
|
|
3214
3272
|
delete registry[input.token]
|
|
3215
3273
|
}
|
|
3216
3274
|
},
|
|
3217
3275
|
{
|
|
3276
|
+
workletFrameTimestampsSeconds: options.workletFrameTimestampsSeconds,
|
|
3218
3277
|
detectionTimeoutMs: options.detectionTimeoutMs ?? 3000,
|
|
3219
3278
|
frame: options.frame ?? null,
|
|
3220
|
-
|
|
3279
|
+
anchorAtTouch: options.anchorAtTouch ?? null,
|
|
3221
3280
|
hold: options.hold ?? null,
|
|
3222
3281
|
motionChangeDefinition: MOTION_CHANGE_DEFINITION,
|
|
3223
3282
|
platform: device.getPlatform(),
|
|
@@ -3228,18 +3287,25 @@ export const device = {
|
|
|
3228
3287
|
},
|
|
3229
3288
|
)
|
|
3230
3289
|
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
const registry = window.__sootsimVisibleChangeCaptures
|
|
3234
|
-
return registry?.[captureToken]?.ready === true
|
|
3235
|
-
},
|
|
3236
|
-
token,
|
|
3237
|
-
{ timeout: 5000 },
|
|
3238
|
-
)
|
|
3239
|
-
|
|
3240
|
-
const triggerStartMs = Date.now()
|
|
3290
|
+
let captureFailure: { error: unknown } | undefined
|
|
3291
|
+
let triggerStartMs = 0
|
|
3241
3292
|
let triggerCompletedMs = 0
|
|
3242
3293
|
try {
|
|
3294
|
+
await Promise.race([
|
|
3295
|
+
page.waitForFunction(
|
|
3296
|
+
(captureToken) => {
|
|
3297
|
+
const registry = window.__sootsimVisibleChangeCaptures
|
|
3298
|
+
return registry?.[captureToken]?.ready === true
|
|
3299
|
+
},
|
|
3300
|
+
token,
|
|
3301
|
+
{ timeout: 5000 },
|
|
3302
|
+
),
|
|
3303
|
+
capturePromise.then(() => {
|
|
3304
|
+
throw new Error('peach motion capture completed before its trigger')
|
|
3305
|
+
}),
|
|
3306
|
+
])
|
|
3307
|
+
|
|
3308
|
+
triggerStartMs = Date.now()
|
|
3243
3309
|
const triggerPromise = (async () => {
|
|
3244
3310
|
if (!options.hold) {
|
|
3245
3311
|
if (typeof options.trigger !== 'function') {
|
|
@@ -3275,6 +3341,7 @@ export const device = {
|
|
|
3275
3341
|
},
|
|
3276
3342
|
}
|
|
3277
3343
|
} catch (error) {
|
|
3344
|
+
captureFailure = { error }
|
|
3278
3345
|
await page
|
|
3279
3346
|
.evaluate((captureToken) => {
|
|
3280
3347
|
const registry = window.__sootsimVisibleChangeCaptures
|
|
@@ -3284,6 +3351,59 @@ export const device = {
|
|
|
3284
3351
|
.catch(() => {})
|
|
3285
3352
|
await capturePromise.catch(() => {})
|
|
3286
3353
|
throw error
|
|
3354
|
+
} finally {
|
|
3355
|
+
if (options.workletFrameTracePath) {
|
|
3356
|
+
try {
|
|
3357
|
+
const events = await page.evaluate(async () => {
|
|
3358
|
+
const test = window.SootSim?.bridges?.test
|
|
3359
|
+
const read = test?.getShellReanimatedTrace
|
|
3360
|
+
const enable = test?.setShellReanimatedTraceEnabled
|
|
3361
|
+
if (typeof read !== 'function' || typeof enable !== 'function')
|
|
3362
|
+
throw new Error('worklet trace bridge is absent')
|
|
3363
|
+
try {
|
|
3364
|
+
return await read()
|
|
3365
|
+
} finally {
|
|
3366
|
+
await enable(false, false)
|
|
3367
|
+
}
|
|
3368
|
+
})
|
|
3369
|
+
fs.writeFileSync(
|
|
3370
|
+
options.workletFrameTracePath,
|
|
3371
|
+
JSON.stringify(
|
|
3372
|
+
{
|
|
3373
|
+
source: 'peach-native-frame-queue',
|
|
3374
|
+
limit: WORKLET_FRAME_TRACE_LIMIT,
|
|
3375
|
+
events,
|
|
3376
|
+
},
|
|
3377
|
+
null,
|
|
3378
|
+
2,
|
|
3379
|
+
),
|
|
3380
|
+
)
|
|
3381
|
+
} catch (traceError) {
|
|
3382
|
+
if (!captureFailure) throw traceError
|
|
3383
|
+
const primary = captureFailure.error
|
|
3384
|
+
if (
|
|
3385
|
+
primary &&
|
|
3386
|
+
(typeof primary === 'object' || typeof primary === 'function') &&
|
|
3387
|
+
Object.isExtensible(primary)
|
|
3388
|
+
) {
|
|
3389
|
+
try {
|
|
3390
|
+
Object.defineProperty(primary, 'workletFrameTraceError', {
|
|
3391
|
+
value: traceError,
|
|
3392
|
+
configurable: true,
|
|
3393
|
+
})
|
|
3394
|
+
} catch (attachmentError) {
|
|
3395
|
+
console.error(
|
|
3396
|
+
'[peach motion capture] could not attach trace failure:',
|
|
3397
|
+
attachmentError,
|
|
3398
|
+
)
|
|
3399
|
+
}
|
|
3400
|
+
}
|
|
3401
|
+
console.error(
|
|
3402
|
+
'[peach motion capture] secondary worklet trace failure:',
|
|
3403
|
+
traceError,
|
|
3404
|
+
)
|
|
3405
|
+
}
|
|
3406
|
+
}
|
|
3287
3407
|
}
|
|
3288
3408
|
},
|
|
3289
3409
|
|
|
@@ -3847,5 +3967,5 @@ export { sootsimExpect as expect, sootsimWaitFor as waitFor }
|
|
|
3847
3967
|
export async function cleanup() {
|
|
3848
3968
|
_browserLaunchAbort?.abort(new Error('Peach driver cleaned up'))
|
|
3849
3969
|
await _browserLaunch?.catch(() => {})
|
|
3850
|
-
await closeBrowser()
|
|
3970
|
+
await closeBrowser(false)
|
|
3851
3971
|
}
|