peachsim 0.1.1625 → 0.1.1640
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 +15 -2
- package/cli/app-config.ts +18 -6
- package/cli/app-fonts.ts +37 -17
- package/cli/app-stubs.ts +284 -20
- package/cli/bridge-flow-runner.ts +66 -45
- package/cli/cloud-client.ts +60 -40
- package/cli/command-registry.ts +1 -0
- package/cli/commands/control.ts +283 -49
- package/cli/commands/doctor.ts +30 -189
- package/cli/commands/flow.ts +79 -11
- package/cli/commands/inspect/core.ts +10 -5
- package/cli/commands/inspect/settling.ts +14 -2
- package/cli/commands/inspect/shared.ts +9 -4
- package/cli/commands/inspect/wait-ready.ts +12 -0
- package/cli/commands/inspect.ts +91 -50
- package/cli/commands/install-desktop.ts +1 -0
- package/cli/commands/maestro-compare.ts +97 -38
- package/cli/commands/maestro.ts +9 -1
- package/cli/commands/preview.ts +1 -1
- package/cli/commands/record.ts +118 -48
- 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 +14 -3
- package/cli/commands/upload.ts +93 -48
- package/cli/desktop-companion.ts +102 -17
- package/cli/drivers/desktop.ts +14 -1
- package/cli/drivers/playwright-sim-host.ts +103 -45
- package/cli/drivers/playwright.ts +9 -8
- package/cli/drivers/types.ts +5 -0
- package/cli/maestro-accessibility.ts +1 -1
- package/cli/maestro-compare.ts +84 -12
- package/cli/main.ts +3 -2
- package/cli/outbound-endpoints.ts +20 -6
- package/cli/pause-aware-timeout.ts +106 -0
- package/cli/scene-video.ts +89 -0
- package/cli/ws-bridge.ts +37 -19
- package/detox/attempt-events.cjs +66 -5
- package/detox/attempt-events.d.cts +7 -0
- package/detox/expectations.ts +7 -3
- package/detox/index.ts +269 -170
- package/detox/motion-change.cjs +206 -21
- package/detox/motion-change.d.cts +39 -2
- 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 +8 -7
- 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 +11887 -7900
- 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 +6 -3
- package/dist-lib/config.mjs +6 -3
- package/dist-lib/detox/index.cjs +1379 -725
- package/dist-lib/dev-bundle-resolution.cjs +14 -2
- package/dist-lib/expo-swift-bundle.cjs +9341 -5462
- package/dist-lib/expo-swift-metadata.cjs +11 -7
- package/dist-lib/expo-swift-metadata.mjs +11 -7
- package/dist-lib/home-paths.cjs +32 -3
- package/dist-lib/host/bridge-host.cjs +50204 -7258
- 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 +3541 -448
- 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 +2931 -104
- package/dist-lib/native-app-identity-project.cjs +289 -49
- package/dist-lib/native-app-identity-project.mjs +289 -49
- package/dist-lib/native-app-identity.cjs +77 -26
- package/dist-lib/native-app-identity.mjs +76 -26
- 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 +6 -2
- package/dist-lib/react-native-host-modules.mjs +6 -2
- package/dist-lib/scripts/dev-server-scanner.cjs +22 -8
- package/dist-lib/sdk.cjs +27 -11
- package/dist-lib/sdk.mjs +27 -10
- package/dist-lib/skills.cjs +72201 -26208
- package/dist-lib/swift-compile.cjs +1007 -140
- package/dist-lib/swift-compile.mjs +1049 -174
- package/dist-lib/swift-package.cjs +12 -3
- package/dist-lib/swift-package.mjs +12 -3
- package/dist-lib/swift-submit.cjs +13 -5
- package/dist-lib/swift-submit.mjs +11 -5
- package/dist-lib/swift-toolchain.cjs +423 -33
- package/dist-lib/swift.cjs +1 -1
- package/dist-lib/vite-swift.cjs +8628 -5268
- package/dist-lib/vite.cjs +2859 -12
- package/dist-lib/worklet-executable.cjs +14 -2
- package/dist-lib/worklet-executable.mjs +14 -2
- package/kotlin/AppSources/dev/peach/app/PeachDrawable.kt +21 -14
- package/kotlin/Sources/androidx/annotation/DrawableRes.kt +14 -0
- package/package.json +1 -1
- package/scripts/dev-server-scanner.ts +2 -1
- package/skills/peach-perf/SKILL.md +11 -5
- package/skills/peach-setup/SKILL.md +16 -19
- package/src/backend-origin.ts +13 -6
- package/src/bridge-constants.ts +11 -0
- package/src/bridge-contract.ts +22 -34
- package/src/config.ts +15 -0
- package/src/desktop-host-delivery.ts +5 -1
- package/src/dev-bundle-resolution.ts +2 -1
- package/src/expo-manifest-body.ts +13 -0
- package/src/expo-swift-host.ts +196 -49
- package/src/expo-swift-ready.ts +78 -0
- package/src/expoKotlinContract.ts +4 -7
- package/src/expoSwiftContract.ts +17 -6
- package/src/home-paths.ts +48 -2
- package/src/host/bridge-host.ts +5 -2
- package/src/host/staged-host-files.ts +9 -0
- package/src/native-app-identity-project.ts +27 -6
- package/src/native-app-identity.ts +91 -4
- package/src/native-firebase-config.ts +209 -13
- package/src/nativeAppIconContract.ts +1 -4
- package/src/output-lock-stdio.ts +36 -0
- package/src/process-group.ts +25 -2
- package/src/react-native-bridge-modules.ts +431 -0
- package/src/react-native-host-modules.ts +8 -1
- package/src/settings-bundle.ts +1 -4
- package/src/swift-binary-modules.ts +2 -2
- package/src/swift-compile.ts +918 -109
- package/src/swift-compiler-digest.ts +14 -0
- package/src/swift-distribution.lock.json +24 -12
- package/src/swift-distribution.ts +103 -4
- package/src/swift-host.ts +180 -55
- package/src/swift-icu.ts +27 -13
- package/src/swift-objc-compile.ts +355 -0
- package/src/swift-objc-sdk.ts +821 -27
- package/src/swift-objc-standin.ts +148 -14
- package/src/swift-package-manifest.ts +60 -7
- package/src/swift-package.ts +15 -1
- package/src/swift-packages.ts +154 -81
- package/src/swift-standin-emit.ts +96 -15
- package/src/swift-standins.ts +530 -116
- package/src/swift-submit.ts +13 -5
- package/src/swift-toolchain.ts +500 -42
- package/src/vite-plugin.ts +12 -1
- package/src/worker-system-chunks.ts +11 -12
- package/src/worklet-executable.ts +19 -1
- package/src/xcode-project.ts +270 -30
package/detox/index.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
+
import { randomBytes } from 'crypto'
|
|
1
2
|
// detox-compatible test driver for sootsim
|
|
2
3
|
// drop-in replacement for `import { by, device, element, expect, waitFor } from 'detox'`
|
|
3
4
|
// uses playwright to control a headless browser running sootsim
|
|
4
|
-
|
|
5
|
-
import { randomBytes } from 'crypto'
|
|
6
5
|
import * as fs from 'fs'
|
|
7
6
|
import { execFile } from 'node:child_process'
|
|
8
7
|
import { createRequire } from 'node:module'
|
|
@@ -14,6 +13,8 @@ import {
|
|
|
14
13
|
reserveChromiumAdmission,
|
|
15
14
|
type ChromiumLaunchAdmission,
|
|
16
15
|
} from '../../../scripts/lib/reap-browser'
|
|
16
|
+
import { automateWebAuthenticationSessions } from '../../peach-engine/src/web-authentication-automation-host'
|
|
17
|
+
import type { LockstepInputReceipt, SootSimLockstepFrameReceipt } from '@peach/globals'
|
|
17
18
|
export { reserveChromiumAdmission } from '../../../scripts/lib/reap-browser'
|
|
18
19
|
import {
|
|
19
20
|
readBrowserCompositeCaptureRequest,
|
|
@@ -87,10 +88,17 @@ type DiscreteObserverState = {
|
|
|
87
88
|
finish: () => DiscreteFrameTrace
|
|
88
89
|
close: () => void
|
|
89
90
|
}
|
|
91
|
+
type VisibleChangeCaptureState = {
|
|
92
|
+
cancelled: boolean
|
|
93
|
+
ready: boolean
|
|
94
|
+
triggerStarted: boolean
|
|
95
|
+
triggerCompleted: boolean
|
|
96
|
+
}
|
|
90
97
|
declare global {
|
|
91
98
|
var __peachChromiumAdmission: ChromiumLaunchAdmission | undefined
|
|
92
99
|
var __peachDetoxCleanup: (() => Promise<void>) | undefined
|
|
93
100
|
interface Window {
|
|
101
|
+
__sootsimVisibleChangeCaptures?: Record<string, VisibleChangeCaptureState>
|
|
94
102
|
__peachClassifyDiscreteFrame?: typeof classifyDiscreteFrame
|
|
95
103
|
__peachDiscreteObserver?: DiscreteObserverState
|
|
96
104
|
}
|
|
@@ -119,6 +127,7 @@ let _browserLaunch: Promise<void> | null = null
|
|
|
119
127
|
let _browserLaunchAbort: AbortController | null = null
|
|
120
128
|
let _reapBrowserNow: (() => void) | null = null
|
|
121
129
|
let _context: BrowserContext | null = null
|
|
130
|
+
let _appStorageState: Awaited<ReturnType<BrowserContext['storageState']>> | undefined
|
|
122
131
|
let _page: Page | null = null
|
|
123
132
|
let _synchronizationEnabled = false
|
|
124
133
|
let _proofFrameObservationActive = false
|
|
@@ -378,7 +387,8 @@ export async function waitForSootsimShellReady(
|
|
|
378
387
|
async function waitForSootsimWorkletsIdle(page: Page, timeout = 3000): Promise<void> {
|
|
379
388
|
const result = await page.evaluate(
|
|
380
389
|
async ({ timeout }) => {
|
|
381
|
-
const
|
|
390
|
+
const testBridge = window.__sootsimTest
|
|
391
|
+
const getPeerStats = testBridge?.getPeerStats
|
|
382
392
|
if (typeof getPeerStats !== 'function') {
|
|
383
393
|
return { settled: true, skipped: true }
|
|
384
394
|
}
|
|
@@ -390,14 +400,30 @@ async function waitForSootsimWorkletsIdle(page: Page, timeout = 3000): Promise<v
|
|
|
390
400
|
let lastPending: number | null | undefined = null
|
|
391
401
|
let completedReads = 0
|
|
392
402
|
let lastReply: string | null = null
|
|
403
|
+
let readStartedAt: number | null = null
|
|
404
|
+
let tenantAtRead = window.__sootsimRenderHost
|
|
405
|
+
let shellAtRead = window.SootSim?.bridges?.shellHost
|
|
406
|
+
let peerProgress: import('@peach/globals').PeachPeerStatsProgress | null = null
|
|
407
|
+
let deadlineTimerLateMs: number | null = null
|
|
393
408
|
|
|
394
409
|
while (Date.now() < deadline) {
|
|
410
|
+
readStartedAt = Date.now()
|
|
411
|
+
tenantAtRead = window.__sootsimRenderHost
|
|
412
|
+
shellAtRead = window.SootSim?.bridges?.shellHost
|
|
395
413
|
let timer: ReturnType<typeof setTimeout> | undefined
|
|
414
|
+
const peerRead = getPeerStats(false)
|
|
415
|
+
peerProgress =
|
|
416
|
+
peerRead && 'peachPeerProgress' in peerRead
|
|
417
|
+
? (peerRead.peachPeerProgress ?? null)
|
|
418
|
+
: null
|
|
396
419
|
const stats = await Promise.race([
|
|
397
|
-
|
|
420
|
+
peerRead,
|
|
398
421
|
new Promise<undefined>((resolve) => {
|
|
399
422
|
timer = setTimeout(
|
|
400
|
-
() =>
|
|
423
|
+
() => {
|
|
424
|
+
deadlineTimerLateMs = Date.now() - deadline
|
|
425
|
+
resolve(undefined)
|
|
426
|
+
},
|
|
401
427
|
Math.max(0, deadline - Date.now()),
|
|
402
428
|
)
|
|
403
429
|
}),
|
|
@@ -405,6 +431,7 @@ async function waitForSootsimWorkletsIdle(page: Page, timeout = 3000): Promise<v
|
|
|
405
431
|
if (timer !== undefined) clearTimeout(timer)
|
|
406
432
|
})
|
|
407
433
|
if (stats === undefined) break
|
|
434
|
+
readStartedAt = null
|
|
408
435
|
if (stats === null) return { settled: true, elapsed: Date.now() - start }
|
|
409
436
|
completedReads++
|
|
410
437
|
lastReply = JSON.stringify({ elapsedMs: Date.now() - start, stats })
|
|
@@ -426,77 +453,45 @@ async function waitForSootsimWorkletsIdle(page: Page, timeout = 3000): Promise<v
|
|
|
426
453
|
}
|
|
427
454
|
|
|
428
455
|
const waited = Date.now() - start
|
|
429
|
-
//
|
|
430
|
-
//
|
|
431
|
-
const
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
stats,
|
|
457
|
-
}),
|
|
458
|
-
(error: unknown) => ({
|
|
459
|
-
status: 'rejected',
|
|
460
|
-
elapsedMs: Date.now() - asked,
|
|
461
|
-
error: error instanceof Error ? error.message : String(error),
|
|
462
|
-
}),
|
|
463
|
-
),
|
|
464
|
-
new Promise<{ status: 'no-reply'; elapsedMs: number }>((resolve) => {
|
|
465
|
-
timer = setTimeout(
|
|
466
|
-
() => resolve({ status: 'no-reply', elapsedMs: Date.now() - asked }),
|
|
467
|
-
1000,
|
|
468
|
-
)
|
|
469
|
-
}),
|
|
470
|
-
])
|
|
471
|
-
} finally {
|
|
472
|
-
if (timer !== undefined) clearTimeout(timer)
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
const timerAsked = Date.now()
|
|
476
|
-
const [tenant, shell, pageTimerLateMs] = await Promise.all([
|
|
477
|
-
probe(window.__sootsimRenderHost),
|
|
478
|
-
probe(window.SootSim?.bridges?.shellHost),
|
|
479
|
-
sleep(0).then(() => Date.now() - timerAsked),
|
|
480
|
-
])
|
|
481
|
-
realms = JSON.stringify({
|
|
482
|
-
phase: 'after-deadline',
|
|
483
|
-
tenant,
|
|
484
|
-
shell,
|
|
485
|
-
pageTimerLateMs,
|
|
486
|
-
})
|
|
456
|
+
// snapshot only the original read. a later request cannot explain which
|
|
457
|
+
// realm delayed this join, and an unchanged host does not prove readiness.
|
|
458
|
+
const tenantAtDeadline = window.__sootsimRenderHost
|
|
459
|
+
const shellAtDeadline = window.SootSim?.bridges?.shellHost
|
|
460
|
+
const observation = {
|
|
461
|
+
completedReads,
|
|
462
|
+
lastReply,
|
|
463
|
+
outstandingRead:
|
|
464
|
+
readStartedAt === null
|
|
465
|
+
? null
|
|
466
|
+
: {
|
|
467
|
+
startedAtMs: readStartedAt - start,
|
|
468
|
+
elapsedMs: Date.now() - readStartedAt,
|
|
469
|
+
peerProgress: peerProgress ? { ...peerProgress } : null,
|
|
470
|
+
testBridgeChanged: window.__sootsimTest !== testBridge,
|
|
471
|
+
tenant: {
|
|
472
|
+
presentAtRequest: !!tenantAtRead,
|
|
473
|
+
presentAtDeadline: !!tenantAtDeadline,
|
|
474
|
+
changed: tenantAtRead !== tenantAtDeadline,
|
|
475
|
+
},
|
|
476
|
+
shell: {
|
|
477
|
+
presentAtRequest: !!shellAtRead,
|
|
478
|
+
presentAtDeadline: !!shellAtDeadline,
|
|
479
|
+
changed: shellAtRead !== shellAtDeadline,
|
|
480
|
+
},
|
|
481
|
+
},
|
|
482
|
+
deadlineTimerLateMs,
|
|
487
483
|
}
|
|
488
484
|
return {
|
|
489
485
|
settled: false,
|
|
490
486
|
elapsed: waited,
|
|
491
487
|
pending: lastPending,
|
|
492
|
-
realms,
|
|
493
488
|
observation,
|
|
494
489
|
}
|
|
495
490
|
},
|
|
496
491
|
{ timeout },
|
|
497
492
|
)
|
|
498
493
|
if (!result?.settled) {
|
|
499
|
-
const pending = result?.pending ??
|
|
494
|
+
const pending = result?.pending ?? 'no peer stats reply'
|
|
500
495
|
throw new Error(
|
|
501
496
|
`sootsim worklet runtime did not settle after ${result?.elapsed ?? timeout}ms (pending=${pending}; observation=${JSON.stringify(result?.observation)})`,
|
|
502
497
|
)
|
|
@@ -566,7 +561,8 @@ async function quiesceContextMedia(context: BrowserContext): Promise<void> {
|
|
|
566
561
|
}
|
|
567
562
|
}
|
|
568
563
|
|
|
569
|
-
async function closeContext() {
|
|
564
|
+
async function closeContext(preserveStorage: boolean) {
|
|
565
|
+
if (!preserveStorage) _appStorageState = undefined
|
|
570
566
|
if (_browserCompromised) {
|
|
571
567
|
throw new Error(`sootsim browser is compromised: ${_browserCompromised}`)
|
|
572
568
|
}
|
|
@@ -574,6 +570,15 @@ async function closeContext() {
|
|
|
574
570
|
const context = _context
|
|
575
571
|
const closingContextId = contextIdentity
|
|
576
572
|
try {
|
|
573
|
+
// process termination clears the app runtime, while its persistent stores
|
|
574
|
+
// remain installed until launchApp({ delete: true }) requests a fresh app.
|
|
575
|
+
if (preserveStorage) {
|
|
576
|
+
_appStorageState = await withLifecycleTimeout(
|
|
577
|
+
'context.storageState()',
|
|
578
|
+
closingContextId,
|
|
579
|
+
() => context.storageState({ indexedDB: true }),
|
|
580
|
+
)
|
|
581
|
+
}
|
|
577
582
|
await quiesceContextMedia(context)
|
|
578
583
|
await withLifecycleTimeout('context.close()', closingContextId, () => context.close())
|
|
579
584
|
_context = null
|
|
@@ -588,7 +593,8 @@ async function closeContext() {
|
|
|
588
593
|
}
|
|
589
594
|
}
|
|
590
595
|
|
|
591
|
-
async function closeBrowser() {
|
|
596
|
+
async function closeBrowser(preserveStorage: boolean) {
|
|
597
|
+
if (!preserveStorage) _appStorageState = undefined
|
|
592
598
|
if (!_browser) return
|
|
593
599
|
const browser = _browser
|
|
594
600
|
const reapBrowserNow = _reapBrowserNow
|
|
@@ -598,7 +604,7 @@ async function closeBrowser() {
|
|
|
598
604
|
|
|
599
605
|
let closeError: Error | null = null
|
|
600
606
|
try {
|
|
601
|
-
await closeContext()
|
|
607
|
+
await closeContext(preserveStorage)
|
|
602
608
|
} catch (error) {
|
|
603
609
|
closeError = error instanceof Error ? error : new Error(String(error))
|
|
604
610
|
}
|
|
@@ -851,6 +857,31 @@ async function dispatchTapAtSootsimPoint(page: Page, point: TapPoint, token?: st
|
|
|
851
857
|
}
|
|
852
858
|
}
|
|
853
859
|
}
|
|
860
|
+
// detox creates the touch in the matched view's window and sends it
|
|
861
|
+
// through uiapplication, so system status-bar routing cannot take it.
|
|
862
|
+
// that window's hit test picks the receiver, so a covering view such as
|
|
863
|
+
// a box-only parent or a pointerEvents none sibling takes the press.
|
|
864
|
+
if (token) window.__sootsimAnchorTimedInput?.(token)
|
|
865
|
+
let result = await window.__sootsimTest!.activatePressAt(x, y, point.nodeId)
|
|
866
|
+
const refusal = result?.tap
|
|
867
|
+
const covering =
|
|
868
|
+
refusal &&
|
|
869
|
+
typeof refusal === 'object' &&
|
|
870
|
+
'reason' in refusal &&
|
|
871
|
+
refusal.reason === 'target-covered' &&
|
|
872
|
+
'coordinateTarget' in refusal
|
|
873
|
+
? refusal.coordinateTarget
|
|
874
|
+
: null
|
|
875
|
+
if (covering && typeof covering === 'object' && 'nodeId' in covering) {
|
|
876
|
+
const { nodeId } = covering
|
|
877
|
+
if (typeof nodeId === 'number') {
|
|
878
|
+
result = await window.__sootsimTest!.activatePressAt(x, y, nodeId)
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
if (result?.ok !== true) {
|
|
882
|
+
throw new Error(`element tap was refused: ${JSON.stringify(result)}`)
|
|
883
|
+
}
|
|
884
|
+
return
|
|
854
885
|
} else {
|
|
855
886
|
;({ x, y } = point)
|
|
856
887
|
}
|
|
@@ -885,7 +916,7 @@ async function focusElementForTextEntry(el: SootSimElement): Promise<void> {
|
|
|
885
916
|
// would edit around a caret the tap then moves, so wait for its focus.
|
|
886
917
|
await pollUntil(
|
|
887
918
|
() => page.evaluate(async () => window.__sootsimTest!.getFocusedNode()),
|
|
888
|
-
(focused) => focused !== null && focusIsWithin(focused, node
|
|
919
|
+
(focused) => focused !== null && focusIsWithin(focused, node),
|
|
889
920
|
{
|
|
890
921
|
timeoutMs: 5000,
|
|
891
922
|
label: `SootSim text input ${node.nodeId} to focus`,
|
|
@@ -1429,11 +1460,12 @@ function createSootSimElement(matcher: Matcher): SootSimElement {
|
|
|
1429
1460
|
normalizedStartingPointX = 0.5,
|
|
1430
1461
|
normalizedStartingPointY = 0.5,
|
|
1431
1462
|
) {
|
|
1463
|
+
const page = getPage()
|
|
1464
|
+
if (_synchronizationEnabled) await waitForSootsimIdle(page, 3000, false, 0)
|
|
1432
1465
|
const node = await findStableNodeByMatcher(matcher, 'swipe')
|
|
1433
1466
|
if (!node)
|
|
1434
1467
|
throw new Error(`element not found for swipe: ${JSON.stringify(matcher)}`)
|
|
1435
1468
|
|
|
1436
|
-
const page = getPage()
|
|
1437
1469
|
const { absolutePosition, layout } = node
|
|
1438
1470
|
|
|
1439
1471
|
const startSootSimX = absolutePosition.x + layout.width * normalizedStartingPointX
|
|
@@ -1673,18 +1705,20 @@ function createSootSimElement(matcher: Matcher): SootSimElement {
|
|
|
1673
1705
|
visible: node.layout.width > 0 && node.layout.height > 0,
|
|
1674
1706
|
enabled: !node.accessibilityState?.disabled,
|
|
1675
1707
|
...node,
|
|
1676
|
-
//
|
|
1677
|
-
//
|
|
1708
|
+
// native detox reads a gauge's accessibility path, which excludes
|
|
1709
|
+
// the empty space below an accessory arc. keep other frame contracts.
|
|
1678
1710
|
frame:
|
|
1679
|
-
node.
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1711
|
+
node.nativeType === 'ExpoUI_GaugeView' && node.accessibilityFrame
|
|
1712
|
+
? node.accessibilityFrame
|
|
1713
|
+
: node.absolutePosition?.width !== undefined &&
|
|
1714
|
+
node.absolutePosition.height !== undefined
|
|
1715
|
+
? {
|
|
1716
|
+
x: node.absolutePosition.x,
|
|
1717
|
+
y: node.absolutePosition.y,
|
|
1718
|
+
width: node.absolutePosition.width,
|
|
1719
|
+
height: node.absolutePosition.height,
|
|
1720
|
+
}
|
|
1721
|
+
: node.frame,
|
|
1688
1722
|
})
|
|
1689
1723
|
// detox reads an unindexed pattern that selects several elements as a list
|
|
1690
1724
|
if (matcher.regexFlags !== undefined && matcher.index === undefined) {
|
|
@@ -1958,10 +1992,11 @@ export const device = {
|
|
|
1958
1992
|
}
|
|
1959
1993
|
|
|
1960
1994
|
if (!_page || newInstance || opts?.delete || opts?.recordVideoDir) {
|
|
1961
|
-
await closeContext()
|
|
1995
|
+
await closeContext(!opts?.delete)
|
|
1962
1996
|
const contextOpts: Parameters<NonNullable<typeof _browser>['newContext']>[0] = {
|
|
1963
1997
|
hasTouch: true,
|
|
1964
1998
|
viewport: { width: 500, height: 900 },
|
|
1999
|
+
storageState: _appStorageState,
|
|
1965
2000
|
}
|
|
1966
2001
|
if (opts?.recordVideoDir) {
|
|
1967
2002
|
fs.mkdirSync(opts.recordVideoDir, { recursive: true })
|
|
@@ -2019,6 +2054,12 @@ export const device = {
|
|
|
2019
2054
|
reapOwnedBrowser()
|
|
2020
2055
|
throw error
|
|
2021
2056
|
}
|
|
2057
|
+
await automateWebAuthenticationSessions(
|
|
2058
|
+
_context!,
|
|
2059
|
+
'__peachWebAuthenticationHost',
|
|
2060
|
+
_page,
|
|
2061
|
+
false,
|
|
2062
|
+
)
|
|
2022
2063
|
contextIdentity = newContextIdentity
|
|
2023
2064
|
}
|
|
2024
2065
|
|
|
@@ -2070,7 +2111,7 @@ export const device = {
|
|
|
2070
2111
|
},
|
|
2071
2112
|
|
|
2072
2113
|
async terminateApp() {
|
|
2073
|
-
await closeBrowser()
|
|
2114
|
+
await closeBrowser(true)
|
|
2074
2115
|
},
|
|
2075
2116
|
|
|
2076
2117
|
async installApp() {
|
|
@@ -2597,12 +2638,11 @@ export const device = {
|
|
|
2597
2638
|
}>,
|
|
2598
2639
|
options: {
|
|
2599
2640
|
detectionTimeoutMs?: number
|
|
2641
|
+
workletFrameTimestampsSeconds?: number[]
|
|
2600
2642
|
frame?: { height: number; width: number; x: number; y: number } | null
|
|
2601
2643
|
// a pointer the capture itself presses at the trigger point and releases
|
|
2602
2644
|
// once durationMs of lockstep time has elapsed, so held states (a lifted
|
|
2603
|
-
// control thumb, a pressed row) are painted mid-hold.
|
|
2604
|
-
// that holds and releases before returning never paints a frame in
|
|
2605
|
-
// between, because no lockstep frame steps while it runs. moves drag the
|
|
2645
|
+
// control thumb, a pressed row) are painted mid-hold. moves drag the
|
|
2606
2646
|
// held pointer to each point once afterMs of lockstep time has elapsed
|
|
2607
2647
|
// since the press, and the release lands at the last point reached.
|
|
2608
2648
|
hold?: {
|
|
@@ -2611,11 +2651,16 @@ export const device = {
|
|
|
2611
2651
|
x: number
|
|
2612
2652
|
y: number
|
|
2613
2653
|
} | null
|
|
2654
|
+
// stages measure from the hold's press ('down') or the frame that
|
|
2655
|
+
// delivers its release ('up') rather than from the first visible
|
|
2656
|
+
// change, the delivered touch anchor the native lane uses.
|
|
2657
|
+
anchorAtTouch?: 'down' | 'up'
|
|
2614
2658
|
rootFrame?: { height: number; width: number; x: number; y: number } | null
|
|
2615
2659
|
settleMs?: number
|
|
2616
2660
|
trigger?: () => unknown
|
|
2617
2661
|
},
|
|
2618
2662
|
): Promise<{
|
|
2663
|
+
inputReceipts: LockstepInputReceipt[]
|
|
2619
2664
|
anchors: Array<{
|
|
2620
2665
|
changedPixelPercent: number
|
|
2621
2666
|
frameIndex: number
|
|
@@ -2659,6 +2704,7 @@ export const device = {
|
|
|
2659
2704
|
installGlobal: true,
|
|
2660
2705
|
})
|
|
2661
2706
|
const capturePromise: Promise<{
|
|
2707
|
+
inputReceipts: LockstepInputReceipt[]
|
|
2662
2708
|
anchors: Array<{
|
|
2663
2709
|
changedPixelPercent: number
|
|
2664
2710
|
frameIndex: number
|
|
@@ -2686,12 +2732,6 @@ export const device = {
|
|
|
2686
2732
|
}
|
|
2687
2733
|
}> = page.evaluate(
|
|
2688
2734
|
async (input) => {
|
|
2689
|
-
type FrameReceipt = { frameIndex: number; virtualTimestampMs: number }
|
|
2690
|
-
type CaptureState = {
|
|
2691
|
-
cancelled: boolean
|
|
2692
|
-
ready: boolean
|
|
2693
|
-
triggerCompleted: boolean
|
|
2694
|
-
}
|
|
2695
2735
|
type CaptureWindow = Window & {
|
|
2696
2736
|
__sootsimMeasureMotionChange?: (input: {
|
|
2697
2737
|
baseline: Uint8ClampedArray
|
|
@@ -2701,7 +2741,6 @@ export const device = {
|
|
|
2701
2741
|
label: string
|
|
2702
2742
|
width: number
|
|
2703
2743
|
}) => MotionChangeMeasurement
|
|
2704
|
-
__sootsimVisibleChangeCaptures?: Record<string, CaptureState>
|
|
2705
2744
|
SootSim?: {
|
|
2706
2745
|
bridges?: {
|
|
2707
2746
|
interact?: Required<
|
|
@@ -2713,19 +2752,6 @@ export const device = {
|
|
|
2713
2752
|
frameToken: number
|
|
2714
2753
|
}>
|
|
2715
2754
|
}
|
|
2716
|
-
lockstepCapture?: {
|
|
2717
|
-
begin: (fps: number) => Promise<{
|
|
2718
|
-
readonly initialFrame: FrameReceipt
|
|
2719
|
-
flushFrame: () => Promise<FrameReceipt>
|
|
2720
|
-
stepFrame: () => Promise<FrameReceipt>
|
|
2721
|
-
framesForOffset: (offsetMs: number) => number
|
|
2722
|
-
stepToOffset: (
|
|
2723
|
-
anchor: FrameReceipt,
|
|
2724
|
-
offsetMs: number,
|
|
2725
|
-
) => Promise<FrameReceipt>
|
|
2726
|
-
end: () => void
|
|
2727
|
-
}>
|
|
2728
|
-
}
|
|
2729
2755
|
}
|
|
2730
2756
|
}
|
|
2731
2757
|
}
|
|
@@ -2751,9 +2777,10 @@ export const device = {
|
|
|
2751
2777
|
const registry =
|
|
2752
2778
|
captureWindow.__sootsimVisibleChangeCaptures ??
|
|
2753
2779
|
(captureWindow.__sootsimVisibleChangeCaptures = {})
|
|
2754
|
-
const state:
|
|
2780
|
+
const state: VisibleChangeCaptureState = {
|
|
2755
2781
|
cancelled: false,
|
|
2756
2782
|
ready: false,
|
|
2783
|
+
triggerStarted: false,
|
|
2757
2784
|
triggerCompleted: false,
|
|
2758
2785
|
}
|
|
2759
2786
|
registry[input.token] = state
|
|
@@ -2838,7 +2865,18 @@ export const device = {
|
|
|
2838
2865
|
if (input.hold && (!interact || typeof interact.touchDown !== 'function')) {
|
|
2839
2866
|
throw new Error('peach interact bridge is not installed')
|
|
2840
2867
|
}
|
|
2841
|
-
const
|
|
2868
|
+
const pairedPeerStats = async (reset = false) => {
|
|
2869
|
+
const stats = await captureWindow.SootSim?.bridges?.test?.getPeerStats?.(reset)
|
|
2870
|
+
if (!stats || !Number.isSafeInteger(stats.replayedFrameCallbacks))
|
|
2871
|
+
throw new Error(
|
|
2872
|
+
'paired worklets clock needs native frame delivery diagnostics',
|
|
2873
|
+
)
|
|
2874
|
+
return stats
|
|
2875
|
+
}
|
|
2876
|
+
if (input.workletFrameTimestampsSeconds) await pairedPeerStats(true)
|
|
2877
|
+
const controller = await lockstep.begin(60, {
|
|
2878
|
+
workletFrameTimestampsSeconds: input.workletFrameTimestampsSeconds,
|
|
2879
|
+
})
|
|
2842
2880
|
// the hold's pointer goes down before the first stepped frame, so its
|
|
2843
2881
|
// start is one frame before the first receipt; each move and the release
|
|
2844
2882
|
// land on the first stepped frame at or past their time from there. the
|
|
@@ -2848,6 +2886,7 @@ export const device = {
|
|
|
2848
2886
|
const holdState = {
|
|
2849
2887
|
nextMove: 0,
|
|
2850
2888
|
pressFrameIndex: null as number | null,
|
|
2889
|
+
releaseReceipt: null as SootSimLockstepFrameReceipt | null,
|
|
2851
2890
|
released: false,
|
|
2852
2891
|
x: input.hold?.x ?? 0,
|
|
2853
2892
|
y: input.hold?.y ?? 0,
|
|
@@ -2861,7 +2900,7 @@ export const device = {
|
|
|
2861
2900
|
const move = moves[holdState.nextMove]
|
|
2862
2901
|
return holdEventFrameIndex(move ? move.afterMs : input.hold.durationMs)
|
|
2863
2902
|
}
|
|
2864
|
-
const advanceHold = async (receipt:
|
|
2903
|
+
const advanceHold = async (receipt: SootSimLockstepFrameReceipt) => {
|
|
2865
2904
|
if (!input.hold || !interact || holdState.released) return
|
|
2866
2905
|
if (holdState.pressFrameIndex === null) {
|
|
2867
2906
|
holdState.pressFrameIndex = receipt.frameIndex - 1
|
|
@@ -2885,6 +2924,7 @@ export const device = {
|
|
|
2885
2924
|
}
|
|
2886
2925
|
if (receipt.frameIndex < holdEventFrameIndex(input.hold.durationMs)) return
|
|
2887
2926
|
holdState.released = true
|
|
2927
|
+
holdState.releaseReceipt = receipt
|
|
2888
2928
|
await interact.touchUp(
|
|
2889
2929
|
holdState.x,
|
|
2890
2930
|
holdState.y,
|
|
@@ -2894,11 +2934,30 @@ export const device = {
|
|
|
2894
2934
|
}
|
|
2895
2935
|
// an anchor is its visible-change frame; its bitmap is kept so a 0ms
|
|
2896
2936
|
// stage can still capture the anchor frame itself.
|
|
2897
|
-
const keptFrames = new Map<
|
|
2937
|
+
const keptFrames = new Map<
|
|
2938
|
+
number,
|
|
2939
|
+
{ bitmap: ImageBitmap; receipt: SootSimLockstepFrameReceipt }
|
|
2940
|
+
>()
|
|
2941
|
+
const keepFrame = (receipt: SootSimLockstepFrameReceipt, bitmap: ImageBitmap) => {
|
|
2942
|
+
keptFrames.get(receipt.frameIndex)?.bitmap.close()
|
|
2943
|
+
keptFrames.set(receipt.frameIndex, { bitmap, receipt })
|
|
2944
|
+
}
|
|
2898
2945
|
try {
|
|
2946
|
+
if (input.anchorAtTouch && !input.hold) {
|
|
2947
|
+
throw new Error('peach touch anchoring needs a hold')
|
|
2948
|
+
}
|
|
2899
2949
|
const baselineBitmap = await captureBitmap()
|
|
2900
2950
|
const baseline = regionPixels(baselineBitmap)
|
|
2901
|
-
|
|
2951
|
+
// the press lands before the first stepped frame, so the screen it
|
|
2952
|
+
// lands on is the baseline itself
|
|
2953
|
+
if (input.anchorAtTouch === 'down')
|
|
2954
|
+
keepFrame(controller.initialFrame, baselineBitmap)
|
|
2955
|
+
else baselineBitmap.close()
|
|
2956
|
+
if (
|
|
2957
|
+
input.workletFrameTimestampsSeconds &&
|
|
2958
|
+
(await pairedPeerStats()).replayedFrameCallbacks > 0
|
|
2959
|
+
)
|
|
2960
|
+
throw new Error('paired worklets callback preceded the Peach trigger')
|
|
2902
2961
|
state.ready = true
|
|
2903
2962
|
if (input.hold && interact) {
|
|
2904
2963
|
const hit = await interact.touchDown(
|
|
@@ -2913,13 +2972,13 @@ export const device = {
|
|
|
2913
2972
|
)
|
|
2914
2973
|
}
|
|
2915
2974
|
} else {
|
|
2916
|
-
while (!state.
|
|
2975
|
+
while (!state.triggerStarted) {
|
|
2917
2976
|
if (state.cancelled) throw new Error('peach motion capture was cancelled')
|
|
2918
2977
|
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
2919
2978
|
}
|
|
2920
2979
|
}
|
|
2921
2980
|
|
|
2922
|
-
let visibleStart:
|
|
2981
|
+
let visibleStart: SootSimLockstepFrameReceipt | null = null
|
|
2923
2982
|
let detectedMeasurement: MotionChangeMeasurement | null = null
|
|
2924
2983
|
let previousBitmap: ImageBitmap | null = null
|
|
2925
2984
|
// android can publish mount layout at p=0 before a clock step.
|
|
@@ -2940,7 +2999,7 @@ export const device = {
|
|
|
2940
2999
|
if (measurement.passedMinimum) {
|
|
2941
3000
|
visibleStart = receipt
|
|
2942
3001
|
detectedMeasurement = measurement
|
|
2943
|
-
|
|
3002
|
+
keepFrame(visibleStart, candidateBitmap)
|
|
2944
3003
|
} else {
|
|
2945
3004
|
candidateBitmap.close()
|
|
2946
3005
|
}
|
|
@@ -2951,6 +3010,13 @@ export const device = {
|
|
|
2951
3010
|
const receipt = await controller.stepFrame()
|
|
2952
3011
|
await advanceHold(receipt)
|
|
2953
3012
|
const candidateBitmap = await captureBitmap()
|
|
3013
|
+
// a touch anchor's stages may precede the first visible change
|
|
3014
|
+
if (
|
|
3015
|
+
input.anchorAtTouch === 'down' ||
|
|
3016
|
+
(input.anchorAtTouch === 'up' && holdState.releaseReceipt === receipt)
|
|
3017
|
+
) {
|
|
3018
|
+
keepFrame(receipt, await createImageBitmap(candidateBitmap))
|
|
3019
|
+
}
|
|
2954
3020
|
const bounds = boundsFor(candidateBitmap)
|
|
2955
3021
|
const measurement = measureChange({
|
|
2956
3022
|
baseline,
|
|
@@ -2965,7 +3031,7 @@ export const device = {
|
|
|
2965
3031
|
previousBitmap = null
|
|
2966
3032
|
visibleStart = receipt
|
|
2967
3033
|
detectedMeasurement = measurement
|
|
2968
|
-
|
|
3034
|
+
keepFrame(receipt, candidateBitmap)
|
|
2969
3035
|
break
|
|
2970
3036
|
}
|
|
2971
3037
|
previousBitmap?.close()
|
|
@@ -2980,10 +3046,26 @@ export const device = {
|
|
|
2980
3046
|
// stages on every platform measure from the first frame that
|
|
2981
3047
|
// differs from baseline: native extraction anchors at its
|
|
2982
3048
|
// detection frame's pts, so the drive starts here too. the
|
|
2983
|
-
// preceding tick is still baseline on both legs.
|
|
2984
|
-
|
|
3049
|
+
// preceding tick is still baseline on both legs. a touch anchor
|
|
3050
|
+
// instead starts the drive at the press or at the frame that
|
|
3051
|
+
// delivers the touch-up.
|
|
3052
|
+
let driveStart = visibleStart
|
|
2985
3053
|
let currentReceipt = visibleStart
|
|
2986
|
-
|
|
3054
|
+
if (input.anchorAtTouch === 'down') driveStart = controller.initialFrame
|
|
3055
|
+
if (input.anchorAtTouch === 'up') {
|
|
3056
|
+
while (!holdState.released) {
|
|
3057
|
+
if (state.cancelled) throw new Error('peach motion capture was cancelled')
|
|
3058
|
+
const receipt = await controller.stepFrame()
|
|
3059
|
+
await advanceHold(receipt)
|
|
3060
|
+
currentReceipt = receipt
|
|
3061
|
+
if (holdState.released) keepFrame(receipt, await captureBitmap())
|
|
3062
|
+
}
|
|
3063
|
+
if (!holdState.releaseReceipt) {
|
|
3064
|
+
throw new Error('peach release anchoring found no release frame')
|
|
3065
|
+
}
|
|
3066
|
+
driveStart = holdState.releaseReceipt
|
|
3067
|
+
}
|
|
3068
|
+
const resolvedAnchors = new Map<string, SootSimLockstepFrameReceipt>()
|
|
2987
3069
|
const anchorRecords: Array<{
|
|
2988
3070
|
changedPixelPercent: number
|
|
2989
3071
|
frameIndex: number
|
|
@@ -2997,7 +3079,7 @@ export const device = {
|
|
|
2997
3079
|
const resolveAnchor = async (stage: {
|
|
2998
3080
|
label: string
|
|
2999
3081
|
anchor?: { id: string; notBeforeMs: number; onsetThresholdPercent?: number }
|
|
3000
|
-
}): Promise<{ anchorId: string; receipt:
|
|
3082
|
+
}): Promise<{ anchorId: string; receipt: SootSimLockstepFrameReceipt }> => {
|
|
3001
3083
|
const anchor = stage.anchor
|
|
3002
3084
|
if (!anchor) return { anchorId: 'transition', receipt: driveStart }
|
|
3003
3085
|
const known = resolvedAnchors.get(anchor.id)
|
|
@@ -3078,8 +3160,7 @@ export const device = {
|
|
|
3078
3160
|
`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`,
|
|
3079
3161
|
)
|
|
3080
3162
|
}
|
|
3081
|
-
|
|
3082
|
-
keptFrames.set(anchorReceipt.frameIndex, anchorBitmap)
|
|
3163
|
+
keepFrame(anchorReceipt, anchorBitmap)
|
|
3083
3164
|
resolvedAnchors.set(anchor.id, anchorReceipt)
|
|
3084
3165
|
anchorRecords.push({
|
|
3085
3166
|
changedPixelPercent: onsetMeasurement.changePercent,
|
|
@@ -3103,13 +3184,14 @@ export const device = {
|
|
|
3103
3184
|
const kept = keptFrames.get(stageFrameIndex)
|
|
3104
3185
|
if (kept && stageFrameIndex <= currentReceipt.frameIndex) {
|
|
3105
3186
|
snapshots.push({
|
|
3106
|
-
actualMs:
|
|
3187
|
+
actualMs:
|
|
3188
|
+
kept.receipt.virtualTimestampMs - anchorReceipt.virtualTimestampMs,
|
|
3107
3189
|
anchorId,
|
|
3108
|
-
bitmap: await createImageBitmap(kept),
|
|
3109
|
-
frameIndex:
|
|
3190
|
+
bitmap: await createImageBitmap(kept.bitmap),
|
|
3191
|
+
frameIndex: kept.receipt.frameIndex,
|
|
3110
3192
|
label: stage.label,
|
|
3111
3193
|
offsetMs: stage.offsetMs,
|
|
3112
|
-
virtualTimestampMs:
|
|
3194
|
+
virtualTimestampMs: kept.receipt.virtualTimestampMs,
|
|
3113
3195
|
})
|
|
3114
3196
|
continue
|
|
3115
3197
|
}
|
|
@@ -3133,6 +3215,9 @@ export const device = {
|
|
|
3133
3215
|
virtualTimestampMs: receipt.virtualTimestampMs,
|
|
3134
3216
|
})
|
|
3135
3217
|
}
|
|
3218
|
+
// the native receipt was recorded through the last stage, not the
|
|
3219
|
+
// release and settle frames that follow it.
|
|
3220
|
+
if (input.workletFrameTimestampsSeconds) controller.endWorkletFrameReceipt()
|
|
3136
3221
|
// a hold that outlasts the last stage still moves and releases on its
|
|
3137
3222
|
// own frames; the settle jump below would carry the pointer past them
|
|
3138
3223
|
// and leave it down. the settle then runs from that release.
|
|
@@ -3153,7 +3238,27 @@ export const device = {
|
|
|
3153
3238
|
)
|
|
3154
3239
|
await controller.stepToOffset(driveStart, maximumPrimaryMs + input.settleMs)
|
|
3155
3240
|
}
|
|
3241
|
+
while (!state.triggerCompleted) {
|
|
3242
|
+
if (state.cancelled) throw new Error('peach motion capture was cancelled')
|
|
3243
|
+
await controller.stepFrame()
|
|
3244
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
3245
|
+
}
|
|
3246
|
+
if (input.workletFrameTimestampsSeconds) {
|
|
3247
|
+
const stats = await pairedPeerStats()
|
|
3248
|
+
const first = stats.nativeFrameFirstTimestampMs
|
|
3249
|
+
const firstFrameIndex =
|
|
3250
|
+
first === null
|
|
3251
|
+
? null
|
|
3252
|
+
: Math.round(
|
|
3253
|
+
(first - controller.initialFrame.virtualTimestampMs) / (1000 / 60),
|
|
3254
|
+
)
|
|
3255
|
+
if (firstFrameIndex !== 1)
|
|
3256
|
+
throw new Error(
|
|
3257
|
+
`paired worklets first delivery did not match the next UI frame: ${firstFrameIndex}`,
|
|
3258
|
+
)
|
|
3259
|
+
}
|
|
3156
3260
|
return {
|
|
3261
|
+
inputReceipts: controller.inputReceipts,
|
|
3157
3262
|
anchors: anchorRecords,
|
|
3158
3263
|
driveStart: {
|
|
3159
3264
|
frameIndex: driveStart.frameIndex,
|
|
@@ -3172,14 +3277,16 @@ export const device = {
|
|
|
3172
3277
|
},
|
|
3173
3278
|
}
|
|
3174
3279
|
} finally {
|
|
3175
|
-
for (const
|
|
3280
|
+
for (const kept of keptFrames.values()) kept.bitmap.close()
|
|
3176
3281
|
controller.end()
|
|
3177
3282
|
delete registry[input.token]
|
|
3178
3283
|
}
|
|
3179
3284
|
},
|
|
3180
3285
|
{
|
|
3286
|
+
workletFrameTimestampsSeconds: options.workletFrameTimestampsSeconds,
|
|
3181
3287
|
detectionTimeoutMs: options.detectionTimeoutMs ?? 3000,
|
|
3182
3288
|
frame: options.frame ?? null,
|
|
3289
|
+
anchorAtTouch: options.anchorAtTouch ?? null,
|
|
3183
3290
|
hold: options.hold ?? null,
|
|
3184
3291
|
motionChangeDefinition: MOTION_CHANGE_DEFINITION,
|
|
3185
3292
|
platform: device.getPlatform(),
|
|
@@ -3190,47 +3297,46 @@ export const device = {
|
|
|
3190
3297
|
},
|
|
3191
3298
|
)
|
|
3192
3299
|
|
|
3193
|
-
await
|
|
3194
|
-
(
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
},
|
|
3205
|
-
|
|
3206
|
-
{ timeout: 5000 },
|
|
3207
|
-
)
|
|
3300
|
+
await Promise.race([
|
|
3301
|
+
page.waitForFunction(
|
|
3302
|
+
(captureToken) => {
|
|
3303
|
+
const registry = window.__sootsimVisibleChangeCaptures
|
|
3304
|
+
return registry?.[captureToken]?.ready === true
|
|
3305
|
+
},
|
|
3306
|
+
token,
|
|
3307
|
+
{ timeout: 5000 },
|
|
3308
|
+
),
|
|
3309
|
+
capturePromise.then(() => {
|
|
3310
|
+
throw new Error('peach motion capture completed before its trigger')
|
|
3311
|
+
}),
|
|
3312
|
+
])
|
|
3208
3313
|
|
|
3209
3314
|
const triggerStartMs = Date.now()
|
|
3210
3315
|
let triggerCompletedMs = 0
|
|
3211
3316
|
try {
|
|
3212
|
-
|
|
3213
|
-
if (
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
await Promise.resolve(options.trigger())
|
|
3217
|
-
}
|
|
3218
|
-
triggerCompletedMs = Date.now()
|
|
3219
|
-
await page.evaluate((captureToken) => {
|
|
3220
|
-
const registry = (
|
|
3221
|
-
window as Window & {
|
|
3222
|
-
__sootsimVisibleChangeCaptures?: Record<
|
|
3223
|
-
string,
|
|
3224
|
-
{ cancelled: boolean; ready: boolean; triggerCompleted: boolean }
|
|
3225
|
-
>
|
|
3317
|
+
const triggerPromise = (async () => {
|
|
3318
|
+
if (!options.hold) {
|
|
3319
|
+
if (typeof options.trigger !== 'function') {
|
|
3320
|
+
throw new Error('peach motion capture needs a trigger or a hold')
|
|
3226
3321
|
}
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3322
|
+
const triggered = Promise.resolve().then(options.trigger)
|
|
3323
|
+
await page.evaluate((captureToken) => {
|
|
3324
|
+
const capture = window.__sootsimVisibleChangeCaptures?.[captureToken]
|
|
3325
|
+
if (!capture) throw new Error('peach motion capture disarmed before trigger')
|
|
3326
|
+
capture.triggerStarted = true
|
|
3327
|
+
}, token)
|
|
3328
|
+
await triggered
|
|
3329
|
+
}
|
|
3330
|
+
triggerCompletedMs = Date.now()
|
|
3331
|
+
await page.evaluate((captureToken) => {
|
|
3332
|
+
const capture = window.__sootsimVisibleChangeCaptures?.[captureToken]
|
|
3333
|
+
if (!capture) throw new Error('peach motion capture disarmed before trigger')
|
|
3334
|
+
capture.triggerCompleted = true
|
|
3335
|
+
}, token)
|
|
3336
|
+
})()
|
|
3337
|
+
const [capture] = await Promise.all([capturePromise, triggerPromise])
|
|
3233
3338
|
return {
|
|
3339
|
+
inputReceipts: capture.inputReceipts,
|
|
3234
3340
|
anchors: capture.anchors,
|
|
3235
3341
|
driveStart: capture.driveStart,
|
|
3236
3342
|
frames: writeFastScreenshotFrames(namePrefix, capture.frames),
|
|
@@ -3245,14 +3351,7 @@ export const device = {
|
|
|
3245
3351
|
} catch (error) {
|
|
3246
3352
|
await page
|
|
3247
3353
|
.evaluate((captureToken) => {
|
|
3248
|
-
const registry =
|
|
3249
|
-
window as Window & {
|
|
3250
|
-
__sootsimVisibleChangeCaptures?: Record<
|
|
3251
|
-
string,
|
|
3252
|
-
{ cancelled: boolean; ready: boolean; triggerCompleted: boolean }
|
|
3253
|
-
>
|
|
3254
|
-
}
|
|
3255
|
-
).__sootsimVisibleChangeCaptures
|
|
3354
|
+
const registry = window.__sootsimVisibleChangeCaptures
|
|
3256
3355
|
const capture = registry?.[captureToken]
|
|
3257
3356
|
if (capture) capture.cancelled = true
|
|
3258
3357
|
}, token)
|
|
@@ -3822,5 +3921,5 @@ export { sootsimExpect as expect, sootsimWaitFor as waitFor }
|
|
|
3822
3921
|
export async function cleanup() {
|
|
3823
3922
|
_browserLaunchAbort?.abort(new Error('Peach driver cleaned up'))
|
|
3824
3923
|
await _browserLaunch?.catch(() => {})
|
|
3825
|
-
await closeBrowser()
|
|
3924
|
+
await closeBrowser(false)
|
|
3826
3925
|
}
|