peachsim 0.1.1640 → 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 +5 -0
- package/cli/app-stubs.ts +38 -13
- 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 +24 -15
- package/cli/commands/inspect/actions.ts +3 -2
- package/cli/commands/inspect/core.ts +0 -8
- package/cli/commands/inspect.ts +0 -14
- package/cli/commands/platform.ts +2 -1
- package/cli/commands/record.ts +64 -28
- package/cli/commands/swift.ts +10 -2
- package/cli/commands/three-mode.ts +4 -38
- package/cli/commands/upload.ts +8 -8
- package/cli/flow-live-status.ts +0 -2
- package/cli/main.ts +1 -1
- package/cli/ws-bridge.ts +0 -6
- package/detox/attempt-events.cjs +41 -5
- package/detox/index.ts +155 -109
- package/detox/jest-environment.cjs +5 -1
- 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 +10 -1
- package/dist-lib/beta.cjs +1 -1
- package/dist-lib/beta.mjs +1 -1
- package/dist-lib/bridge-constants.cjs +1 -1
- package/dist-lib/bridge-contract-input.cjs +1 -1
- package/dist-lib/bridge-contract-input.mjs +1 -1
- package/dist-lib/bridge-contract.cjs +1 -1
- package/dist-lib/bridge-contract.mjs +1 -1
- package/dist-lib/bundle-channel.cjs +2059 -1368
- 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 +572 -125
- 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 +1837 -1164
- package/dist-lib/expo-swift-metadata.cjs +1 -1
- package/dist-lib/expo-swift-metadata.mjs +1 -1
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +2290 -1616
- 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 +44 -17
- 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 +33 -13
- package/dist-lib/native-app-identity-project.cjs +7 -4
- package/dist-lib/native-app-identity-project.mjs +7 -4
- package/dist-lib/native-app-identity.cjs +1 -1
- package/dist-lib/native-app-identity.mjs +1 -1
- package/dist-lib/native-firebase-metadata.cjs +1 -1
- package/dist-lib/native-firebase-metadata.mjs +1 -1
- package/dist-lib/plist.cjs +1 -1
- package/dist-lib/plist.mjs +1 -1
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/public-brand.cjs +1 -1
- package/dist-lib/react-native-host-modules.cjs +1 -1
- package/dist-lib/react-native-host-modules.mjs +1 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
- package/dist-lib/sdk.cjs +4 -3
- package/dist-lib/sdk.mjs +4 -3
- package/dist-lib/skills.cjs +2823 -2109
- package/dist-lib/swift-compile.cjs +370 -239
- package/dist-lib/swift-compile.mjs +368 -230
- package/dist-lib/swift-package.cjs +1 -1
- package/dist-lib/swift-package.mjs +1 -1
- package/dist-lib/swift-submit.cjs +8 -3
- package/dist-lib/swift-submit.mjs +8 -3
- package/dist-lib/swift-toolchain.cjs +1 -1
- package/dist-lib/swift.cjs +1 -1
- package/dist-lib/vite-swift.cjs +1757 -1108
- package/dist-lib/vite.cjs +1 -1
- package/dist-lib/worklet-executable.cjs +1 -1
- package/dist-lib/worklet-executable.mjs +1 -1
- package/package.json +1 -1
- package/src/backend-origin.ts +8 -0
- package/src/react-native-bridge-modules.ts +89 -76
- package/src/swift-compile.ts +134 -107
- package/src/swift-distribution.lock.json +9 -8
- package/src/swift-host.ts +6 -4
- package/src/swift-objc-compile.ts +385 -193
- package/src/swift-objc-metadata.ts +204 -0
- package/src/swift-objc-sdk.ts +195 -2
- package/src/swift-package-manifest.ts +6 -3
- package/src/swift-root-lock.ts +153 -0
- package/src/swift-submit.ts +15 -2
- package/src/swiftContract.ts +12 -0
- package/src/xcode-project.ts +10 -3
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
import { createHash } from 'node:crypto'
|
|
7
7
|
import { mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from 'node:fs'
|
|
8
8
|
import { dirname, extname, resolve } from 'node:path'
|
|
9
|
+
import {
|
|
10
|
+
collectTimelineAssetRefs,
|
|
11
|
+
timelineAssetRefOf,
|
|
12
|
+
} from 'peach-engine/screenshots/timeline-assets'
|
|
9
13
|
import {
|
|
10
14
|
isThreeModeSetting,
|
|
11
15
|
THREE_MODE_SETTING_IDS,
|
|
@@ -353,44 +357,6 @@ function mimeTypeForAsset(path: string, contentType: unknown) {
|
|
|
353
357
|
// timeline as blob: URLs.
|
|
354
358
|
const TIMELINE_ASSET_CHUNK_BYTES = 1_048_576
|
|
355
359
|
|
|
356
|
-
type TimelineAssetRef = {
|
|
357
|
-
src: string
|
|
358
|
-
recording: boolean
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
function timelineAssetRefOf(value: Record<string, unknown>): TimelineAssetRef | null {
|
|
362
|
-
if (
|
|
363
|
-
(value.type === 'recording' ||
|
|
364
|
-
value.type === 'screenshot' ||
|
|
365
|
-
value.kind === 'screen') &&
|
|
366
|
-
typeof value.src === 'string' &&
|
|
367
|
-
!/^(?:blob:|data:|https?:)/i.test(value.src)
|
|
368
|
-
) {
|
|
369
|
-
return { src: value.src, recording: value.type === 'recording' }
|
|
370
|
-
}
|
|
371
|
-
return null
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
function collectTimelineAssetRefs(value: unknown): TimelineAssetRef[] {
|
|
375
|
-
const refs: TimelineAssetRef[] = []
|
|
376
|
-
const seen = new Set<string>()
|
|
377
|
-
const visit = (node: unknown): void => {
|
|
378
|
-
if (Array.isArray(node)) {
|
|
379
|
-
for (const entry of node) visit(entry)
|
|
380
|
-
return
|
|
381
|
-
}
|
|
382
|
-
if (!isRecord(node)) return
|
|
383
|
-
for (const entry of Object.values(node)) visit(entry)
|
|
384
|
-
const ref = timelineAssetRefOf(node)
|
|
385
|
-
if (ref && !seen.has(ref.src)) {
|
|
386
|
-
seen.add(ref.src)
|
|
387
|
-
refs.push(ref)
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
visit(value)
|
|
391
|
-
return refs
|
|
392
|
-
}
|
|
393
|
-
|
|
394
360
|
type DistinctTimelineAsset = {
|
|
395
361
|
hash: string
|
|
396
362
|
mime: string
|
package/cli/commands/upload.ts
CHANGED
|
@@ -522,7 +522,7 @@ export async function runUpload(
|
|
|
522
522
|
bridge,
|
|
523
523
|
'(typeof window.__sootsimCaptureBundle === "function") ? window.__sootsimCaptureBundle() : null',
|
|
524
524
|
)
|
|
525
|
-
if (snapshot?.bundleUrl) {
|
|
525
|
+
if (hasBundle && snapshot?.bundleUrl) {
|
|
526
526
|
const bundleOrigin = new URL(snapshot.bundleUrl).origin
|
|
527
527
|
// manifest stays the body-less re-fetch path for bundle-origin static
|
|
528
528
|
// assets (chunks, fonts, wasm). bundle-origin api endpoints would also
|
|
@@ -784,7 +784,7 @@ export async function runUpload(
|
|
|
784
784
|
.join('\n')
|
|
785
785
|
const timelineGz = gzipSync(traceTimelineJsonl ? `${traceTimelineJsonl}\n` : '')
|
|
786
786
|
const storageGz =
|
|
787
|
-
opts.storageSnapshot && hasShareableStorage(opts.storageSnapshot)
|
|
787
|
+
hasBundle && opts.storageSnapshot && hasShareableStorage(opts.storageSnapshot)
|
|
788
788
|
? gzipSync(JSON.stringify(opts.storageSnapshot))
|
|
789
789
|
: undefined
|
|
790
790
|
console.log(
|
|
@@ -971,10 +971,9 @@ export async function runUpload(
|
|
|
971
971
|
// fetcher (same code path as the build's `peach app-fonts stage`) so both
|
|
972
972
|
// pipelines store identical bytes keyed by sha256(url).
|
|
973
973
|
const appFontFiles: PreparedFile[] = []
|
|
974
|
-
const appFontsWire =
|
|
975
|
-
appFontsWireFromPageHref(snapshot.pageHref),
|
|
976
|
-
|
|
977
|
-
)
|
|
974
|
+
const appFontsWire = hasBundle
|
|
975
|
+
? await resolveAppFontsWire(appFontsWireFromPageHref(snapshot.pageHref), fetch)
|
|
976
|
+
: ''
|
|
978
977
|
if (appFontsWire) {
|
|
979
978
|
const fonts = await fetchAppFontFiles(appFontsWire, {
|
|
980
979
|
onStaged: ({ url, byteLength }) =>
|
|
@@ -1003,7 +1002,7 @@ export async function runUpload(
|
|
|
1003
1002
|
// still-live metro and persist them keyed by sha256(url). the engine's
|
|
1004
1003
|
// <img src> resolves them through the share fetch proxy in preview mode.
|
|
1005
1004
|
const appSplashFiles: PreparedFile[] = []
|
|
1006
|
-
const appSplashWire = appSplashWireFromPageHref(snapshot.pageHref)
|
|
1005
|
+
const appSplashWire = hasBundle ? appSplashWireFromPageHref(snapshot.pageHref) : ''
|
|
1007
1006
|
if (appSplashWire) {
|
|
1008
1007
|
const images = await fetchAppSplashImages(appSplashWire, {
|
|
1009
1008
|
onStaged: ({ url, byteLength }) =>
|
|
@@ -1033,7 +1032,8 @@ export async function runUpload(
|
|
|
1033
1032
|
// explicitly so the installed app keeps its identity in share replay.
|
|
1034
1033
|
const appIdentityFiles: PreparedFile[] = []
|
|
1035
1034
|
let appManifest: string | undefined
|
|
1036
|
-
const metroOrigin =
|
|
1035
|
+
const metroOrigin =
|
|
1036
|
+
hasBundle && snapshot.bundleUrl ? loopbackOriginOf(snapshot.bundleUrl) : null
|
|
1037
1037
|
if (metroOrigin && snapshot.bundleUrl) {
|
|
1038
1038
|
const platform = new URL(snapshot.bundleUrl).searchParams.get('platform') || 'ios'
|
|
1039
1039
|
const manifestUrl = `${metroOrigin}/`
|
package/cli/flow-live-status.ts
CHANGED
|
@@ -13,8 +13,6 @@ export type LiveStepStatus = 'running' | 'success' | 'failure' | 'skipped'
|
|
|
13
13
|
|
|
14
14
|
type PlanStep = { index: number; name: string; target?: string }
|
|
15
15
|
|
|
16
|
-
const PAUSE_POLL_MS = 400
|
|
17
|
-
|
|
18
16
|
function replyPaused(reply: unknown) {
|
|
19
17
|
if (typeof reply !== 'object' || reply == null || !('control' in reply)) return false
|
|
20
18
|
const control = reply.control
|
package/cli/main.ts
CHANGED
|
@@ -466,7 +466,7 @@ try {
|
|
|
466
466
|
// inner verb's --json payload, so it never needs the shared bridge
|
|
467
467
|
// machinery that lives inside runInspect.
|
|
468
468
|
const { runAssert } = await import('./commands/assert')
|
|
469
|
-
await runAssert(parsed.commandArgs)
|
|
469
|
+
await runAssert(parsed.commandArgs, { port, sim: globalSimTarget })
|
|
470
470
|
break
|
|
471
471
|
}
|
|
472
472
|
|
package/cli/ws-bridge.ts
CHANGED
|
@@ -374,12 +374,6 @@ export class BridgeSimLockedError extends Error {
|
|
|
374
374
|
}
|
|
375
375
|
}
|
|
376
376
|
|
|
377
|
-
interface WsResponse {
|
|
378
|
-
id: number
|
|
379
|
-
result?: any
|
|
380
|
-
error?: string
|
|
381
|
-
}
|
|
382
|
-
|
|
383
377
|
interface WsBridgeOptions {
|
|
384
378
|
connectTimeoutMs?: number
|
|
385
379
|
commandTimeoutMs?: number
|
package/detox/attempt-events.cjs
CHANGED
|
@@ -318,6 +318,10 @@ function writeDiscreteFrameReceipt(
|
|
|
318
318
|
}
|
|
319
319
|
|
|
320
320
|
function authenticateNativeCadenceCensus(root, sourceRoot, life, decoded) {
|
|
321
|
+
const {
|
|
322
|
+
authenticateNativeRefreshSamples,
|
|
323
|
+
} = require('../../peach-conformance/e2e/utils/native-frame-receipt.cjs')
|
|
324
|
+
authenticateNativeRefreshSamples(root, sourceRoot, life)
|
|
321
325
|
const native = life.nativeFramebuffer
|
|
322
326
|
if (
|
|
323
327
|
!native ||
|
|
@@ -344,13 +348,13 @@ function authenticateNativeCadenceCensus(root, sourceRoot, life, decoded) {
|
|
|
344
348
|
root,
|
|
345
349
|
native.mediaFrames,
|
|
346
350
|
native.mediaFramesSha256,
|
|
347
|
-
'native cadence
|
|
351
|
+
'native cadence sampled surface census',
|
|
348
352
|
)
|
|
349
353
|
.trim()
|
|
350
354
|
.split('\n')
|
|
351
355
|
.map(JSON.parse)
|
|
352
356
|
if (frames.length !== decoded.observations.length || media.length !== frames.length)
|
|
353
|
-
throw new Error('native cadence acquisition and
|
|
357
|
+
throw new Error('native cadence acquisition and sampled surface census differ')
|
|
354
358
|
const {
|
|
355
359
|
decodeNativeFramebuffer,
|
|
356
360
|
requireNativeRawFrame,
|
|
@@ -377,13 +381,25 @@ function authenticateNativeCadenceCensus(root, sourceRoot, life, decoded) {
|
|
|
377
381
|
return row.callbackIndex
|
|
378
382
|
}),
|
|
379
383
|
)
|
|
380
|
-
const
|
|
384
|
+
const reads = boundText(
|
|
385
|
+
root,
|
|
386
|
+
native.reads,
|
|
387
|
+
native.readsSha256,
|
|
388
|
+
'native cadence observations',
|
|
389
|
+
)
|
|
390
|
+
.split('\n')
|
|
391
|
+
.filter(Boolean)
|
|
392
|
+
.map(JSON.parse)
|
|
381
393
|
let expectedCallback = 0
|
|
382
|
-
for (const
|
|
394
|
+
for (const read of reads) {
|
|
383
395
|
while (superseded.delete(expectedCallback)) expectedCallback++
|
|
396
|
+
if (read.callbackIndex !== expectedCallback++)
|
|
397
|
+
throw new Error('native cadence observations have invalid callback order')
|
|
398
|
+
}
|
|
399
|
+
const origin = frames[0].consumerArrival
|
|
400
|
+
for (const [index, frame] of frames.entries()) {
|
|
384
401
|
const clock = frame.consumerArrival
|
|
385
402
|
if (
|
|
386
|
-
frame.callbackIndex !== expectedCallback++ ||
|
|
387
403
|
media[index].sourceFrameIndex !== index ||
|
|
388
404
|
!clock ||
|
|
389
405
|
clock.domain !== 'host-mach-absolute-time' ||
|
|
@@ -771,6 +787,26 @@ function discreteFrameEvidenceFiles(root, evidence) {
|
|
|
771
787
|
paths.add(native.receipt)
|
|
772
788
|
paths.add(native.superseded)
|
|
773
789
|
paths.add(native.mediaFrames)
|
|
790
|
+
paths.add(native.gradingMarkers)
|
|
791
|
+
paths.add(native.gradingPlan)
|
|
792
|
+
paths.add(native.windowResolution)
|
|
793
|
+
paths.add(native.reads)
|
|
794
|
+
for (const receipt of native.finalReads) {
|
|
795
|
+
const text = boundText(
|
|
796
|
+
root,
|
|
797
|
+
receipt.path,
|
|
798
|
+
receipt.sha256,
|
|
799
|
+
'native sampled end state',
|
|
800
|
+
)
|
|
801
|
+
paths.add(receipt.path)
|
|
802
|
+
for (const row of text.split('\n').filter(Boolean).map(JSON.parse)) {
|
|
803
|
+
const file = proofBoundPath(
|
|
804
|
+
path.dirname(proofBoundPath(root, receipt.path)),
|
|
805
|
+
row.rawPath,
|
|
806
|
+
)
|
|
807
|
+
paths.add(path.relative(root, file).split(path.sep).join('/'))
|
|
808
|
+
}
|
|
809
|
+
}
|
|
774
810
|
const rows = fs
|
|
775
811
|
.readFileSync(proofBoundPath(root, native.receipt), 'utf8')
|
|
776
812
|
.trim()
|
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
|
|
@@ -492,8 +498,12 @@ async function waitForSootsimWorkletsIdle(page: Page, timeout = 3000): Promise<v
|
|
|
492
498
|
)
|
|
493
499
|
if (!result?.settled) {
|
|
494
500
|
const pending = result?.pending ?? 'no peer stats reply'
|
|
495
|
-
throw
|
|
496
|
-
|
|
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',
|
|
497
507
|
)
|
|
498
508
|
}
|
|
499
509
|
}
|
|
@@ -2119,7 +2129,7 @@ export const device = {
|
|
|
2119
2129
|
},
|
|
2120
2130
|
|
|
2121
2131
|
async uninstallApp() {
|
|
2122
|
-
|
|
2132
|
+
await closeBrowser(false)
|
|
2123
2133
|
},
|
|
2124
2134
|
|
|
2125
2135
|
async openURL(url: { url: string; sourceApp?: string }) {
|
|
@@ -2639,15 +2649,18 @@ export const device = {
|
|
|
2639
2649
|
options: {
|
|
2640
2650
|
detectionTimeoutMs?: number
|
|
2641
2651
|
workletFrameTimestampsSeconds?: number[]
|
|
2652
|
+
workletFrameTracePath?: string
|
|
2642
2653
|
frame?: { height: number; width: number; x: number; y: number } | null
|
|
2643
2654
|
// a pointer the capture itself presses at the trigger point and releases
|
|
2644
2655
|
// once durationMs of lockstep time has elapsed, so held states (a lifted
|
|
2645
2656
|
// control thumb, a pressed row) are painted mid-hold. moves drag the
|
|
2646
2657
|
// held pointer to each point once afterMs of lockstep time has elapsed
|
|
2647
|
-
// since the press
|
|
2658
|
+
// since the press. an explicit release position preserves witnessed up coordinates.
|
|
2648
2659
|
hold?: {
|
|
2649
2660
|
durationMs: number
|
|
2650
|
-
|
|
2661
|
+
eventDurationMs?: number
|
|
2662
|
+
release?: { x: number; y: number }
|
|
2663
|
+
moves?: Array<{ afterMs: number; eventAfterMs?: number; x: number; y: number }>
|
|
2651
2664
|
x: number
|
|
2652
2665
|
y: number
|
|
2653
2666
|
} | null
|
|
@@ -2703,6 +2716,14 @@ export const device = {
|
|
|
2703
2716
|
definition: MOTION_CHANGE_DEFINITION,
|
|
2704
2717
|
installGlobal: true,
|
|
2705
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
|
+
}
|
|
2706
2727
|
const capturePromise: Promise<{
|
|
2707
2728
|
inputReceipts: LockstepInputReceipt[]
|
|
2708
2729
|
anchors: Array<{
|
|
@@ -2877,61 +2898,9 @@ export const device = {
|
|
|
2877
2898
|
const controller = await lockstep.begin(60, {
|
|
2878
2899
|
workletFrameTimestampsSeconds: input.workletFrameTimestampsSeconds,
|
|
2879
2900
|
})
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
// deadlines are frame indexes: comparing summed virtual timestamps
|
|
2884
|
-
// against a duration lets rounding push an event past its frame.
|
|
2885
|
-
const moves = input.hold?.moves ?? []
|
|
2886
|
-
const holdState = {
|
|
2887
|
-
nextMove: 0,
|
|
2888
|
-
pressFrameIndex: null as number | null,
|
|
2889
|
-
releaseReceipt: null as SootSimLockstepFrameReceipt | null,
|
|
2890
|
-
released: false,
|
|
2891
|
-
x: input.hold?.x ?? 0,
|
|
2892
|
-
y: input.hold?.y ?? 0,
|
|
2893
|
-
}
|
|
2894
|
-
const holdEventFrameIndex = (afterMs: number) =>
|
|
2895
|
-
(holdState.pressFrameIndex ?? 0) + controller.framesForOffset(afterMs)
|
|
2896
|
-
const nextHoldEventFrameIndex = () => {
|
|
2897
|
-
if (!input.hold || holdState.released || holdState.pressFrameIndex === null) {
|
|
2898
|
-
return null
|
|
2899
|
-
}
|
|
2900
|
-
const move = moves[holdState.nextMove]
|
|
2901
|
-
return holdEventFrameIndex(move ? move.afterMs : input.hold.durationMs)
|
|
2902
|
-
}
|
|
2903
|
-
const advanceHold = async (receipt: SootSimLockstepFrameReceipt) => {
|
|
2904
|
-
if (!input.hold || !interact || holdState.released) return
|
|
2905
|
-
if (holdState.pressFrameIndex === null) {
|
|
2906
|
-
holdState.pressFrameIndex = receipt.frameIndex - 1
|
|
2907
|
-
}
|
|
2908
|
-
for (
|
|
2909
|
-
let move = moves[holdState.nextMove];
|
|
2910
|
-
move && receipt.frameIndex >= holdEventFrameIndex(move.afterMs);
|
|
2911
|
-
move = moves[holdState.nextMove]
|
|
2912
|
-
) {
|
|
2913
|
-
holdState.nextMove++
|
|
2914
|
-
holdState.x = move.x
|
|
2915
|
-
holdState.y = move.y
|
|
2916
|
-
// event time remains on the replay clock when delivery ceilings
|
|
2917
|
-
// to a display tick or several native samples share one frame.
|
|
2918
|
-
await interact.touchMove(
|
|
2919
|
-
move.x,
|
|
2920
|
-
move.y,
|
|
2921
|
-
999,
|
|
2922
|
-
controller.initialFrame.virtualTimestampMs + move.afterMs,
|
|
2923
|
-
)
|
|
2924
|
-
}
|
|
2925
|
-
if (receipt.frameIndex < holdEventFrameIndex(input.hold.durationMs)) return
|
|
2926
|
-
holdState.released = true
|
|
2927
|
-
holdState.releaseReceipt = receipt
|
|
2928
|
-
await interact.touchUp(
|
|
2929
|
-
holdState.x,
|
|
2930
|
-
holdState.y,
|
|
2931
|
-
999,
|
|
2932
|
-
controller.initialFrame.virtualTimestampMs + input.hold.durationMs,
|
|
2933
|
-
)
|
|
2934
|
-
}
|
|
2901
|
+
let heldInput: Promise<void> | null = null
|
|
2902
|
+
const releaseReceipt = () =>
|
|
2903
|
+
controller.inputReceipts.find((receipt) => receipt.phase === 'up')
|
|
2935
2904
|
// an anchor is its visible-change frame; its bitmap is kept so a 0ms
|
|
2936
2905
|
// stage can still capture the anchor frame itself.
|
|
2937
2906
|
const keptFrames = new Map<
|
|
@@ -2960,17 +2929,51 @@ export const device = {
|
|
|
2960
2929
|
throw new Error('paired worklets callback preceded the Peach trigger')
|
|
2961
2930
|
state.ready = true
|
|
2962
2931
|
if (input.hold && interact) {
|
|
2963
|
-
const
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
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()
|
|
2974
2977
|
} else {
|
|
2975
2978
|
while (!state.triggerStarted) {
|
|
2976
2979
|
if (state.cancelled) throw new Error('peach motion capture was cancelled')
|
|
@@ -3008,12 +3011,12 @@ export const device = {
|
|
|
3008
3011
|
for (let frame = 0; !visibleStart && frame < detectionFrameLimit; frame++) {
|
|
3009
3012
|
if (state.cancelled) throw new Error('peach motion capture was cancelled')
|
|
3010
3013
|
const receipt = await controller.stepFrame()
|
|
3011
|
-
await advanceHold(receipt)
|
|
3012
3014
|
const candidateBitmap = await captureBitmap()
|
|
3013
3015
|
// a touch anchor's stages may precede the first visible change
|
|
3014
3016
|
if (
|
|
3015
3017
|
input.anchorAtTouch === 'down' ||
|
|
3016
|
-
(input.anchorAtTouch === 'up' &&
|
|
3018
|
+
(input.anchorAtTouch === 'up' &&
|
|
3019
|
+
releaseReceipt()?.frameIndex === receipt.frameIndex)
|
|
3017
3020
|
) {
|
|
3018
3021
|
keepFrame(receipt, await createImageBitmap(candidateBitmap))
|
|
3019
3022
|
}
|
|
@@ -3053,17 +3056,17 @@ export const device = {
|
|
|
3053
3056
|
let currentReceipt = visibleStart
|
|
3054
3057
|
if (input.anchorAtTouch === 'down') driveStart = controller.initialFrame
|
|
3055
3058
|
if (input.anchorAtTouch === 'up') {
|
|
3056
|
-
while (!
|
|
3059
|
+
while (!releaseReceipt()) {
|
|
3057
3060
|
if (state.cancelled) throw new Error('peach motion capture was cancelled')
|
|
3058
3061
|
const receipt = await controller.stepFrame()
|
|
3059
|
-
await advanceHold(receipt)
|
|
3060
3062
|
currentReceipt = receipt
|
|
3061
|
-
if (
|
|
3063
|
+
if (releaseReceipt()) keepFrame(receipt, await captureBitmap())
|
|
3062
3064
|
}
|
|
3063
|
-
|
|
3065
|
+
const released = releaseReceipt()
|
|
3066
|
+
if (!released) {
|
|
3064
3067
|
throw new Error('peach release anchoring found no release frame')
|
|
3065
3068
|
}
|
|
3066
|
-
driveStart =
|
|
3069
|
+
driveStart = { ...released, tenantFrame: null }
|
|
3067
3070
|
}
|
|
3068
3071
|
const resolvedAnchors = new Map<string, SootSimLockstepFrameReceipt>()
|
|
3069
3072
|
const anchorRecords: Array<{
|
|
@@ -3107,7 +3110,6 @@ export const device = {
|
|
|
3107
3110
|
while (searchReceipt.virtualTimestampMs < searchStartVts) {
|
|
3108
3111
|
if (state.cancelled) throw new Error('peach motion capture was cancelled')
|
|
3109
3112
|
searchReceipt = await controller.stepFrame()
|
|
3110
|
-
await advanceHold(searchReceipt)
|
|
3111
3113
|
}
|
|
3112
3114
|
currentReceipt = searchReceipt
|
|
3113
3115
|
let anchorBitmap = await captureBitmap()
|
|
@@ -3123,7 +3125,6 @@ export const device = {
|
|
|
3123
3125
|
for (let frame = 0; frame < detectionFrameLimit; frame++) {
|
|
3124
3126
|
if (state.cancelled) throw new Error('peach motion capture was cancelled')
|
|
3125
3127
|
const receipt = await controller.stepFrame()
|
|
3126
|
-
await advanceHold(receipt)
|
|
3127
3128
|
const candidateBitmap = await captureBitmap()
|
|
3128
3129
|
const bounds = boundsFor(candidateBitmap)
|
|
3129
3130
|
const candidate = regionPixels(candidateBitmap)
|
|
@@ -3176,9 +3177,6 @@ export const device = {
|
|
|
3176
3177
|
for (const stage of input.stages) {
|
|
3177
3178
|
if (state.cancelled) throw new Error('peach motion capture was cancelled')
|
|
3178
3179
|
const { anchorId, receipt: anchorReceipt } = await resolveAnchor(stage)
|
|
3179
|
-
// step through every hold move and the release that fall before
|
|
3180
|
-
// this stage one frame at a time, so the stage paints the pointer
|
|
3181
|
-
// where it belongs rather than a hold the jump skipped past.
|
|
3182
3180
|
const stageFrameIndex =
|
|
3183
3181
|
anchorReceipt.frameIndex + controller.framesForOffset(stage.offsetMs)
|
|
3184
3182
|
const kept = keptFrames.get(stageFrameIndex)
|
|
@@ -3195,15 +3193,7 @@ export const device = {
|
|
|
3195
3193
|
})
|
|
3196
3194
|
continue
|
|
3197
3195
|
}
|
|
3198
|
-
for (
|
|
3199
|
-
let eventFrameIndex = nextHoldEventFrameIndex();
|
|
3200
|
-
eventFrameIndex !== null && stageFrameIndex > eventFrameIndex;
|
|
3201
|
-
eventFrameIndex = nextHoldEventFrameIndex()
|
|
3202
|
-
) {
|
|
3203
|
-
await advanceHold(await controller.stepFrame())
|
|
3204
|
-
}
|
|
3205
3196
|
const receipt = await controller.stepToOffset(anchorReceipt, stage.offsetMs)
|
|
3206
|
-
await advanceHold(receipt)
|
|
3207
3197
|
currentReceipt = receipt
|
|
3208
3198
|
snapshots.push({
|
|
3209
3199
|
actualMs: receipt.virtualTimestampMs - anchorReceipt.virtualTimestampMs,
|
|
@@ -3222,11 +3212,11 @@ export const device = {
|
|
|
3222
3212
|
// own frames; the settle jump below would carry the pointer past them
|
|
3223
3213
|
// and leave it down. the settle then runs from that release.
|
|
3224
3214
|
let releasedOffsetMs = 0
|
|
3225
|
-
while (
|
|
3215
|
+
while (input.hold && !releaseReceipt()) {
|
|
3226
3216
|
const receipt = await controller.stepFrame()
|
|
3227
|
-
await advanceHold(receipt)
|
|
3228
3217
|
releasedOffsetMs = receipt.virtualTimestampMs - driveStart.virtualTimestampMs
|
|
3229
3218
|
}
|
|
3219
|
+
await heldInput
|
|
3230
3220
|
if (input.settleMs > 0) {
|
|
3231
3221
|
const maximumPrimaryMs = snapshots.reduce(
|
|
3232
3222
|
(maximum, snapshot) =>
|
|
@@ -3297,23 +3287,25 @@ export const device = {
|
|
|
3297
3287
|
},
|
|
3298
3288
|
)
|
|
3299
3289
|
|
|
3300
|
-
|
|
3301
|
-
|
|
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
|
-
])
|
|
3313
|
-
|
|
3314
|
-
const triggerStartMs = Date.now()
|
|
3290
|
+
let captureFailure: { error: unknown } | undefined
|
|
3291
|
+
let triggerStartMs = 0
|
|
3315
3292
|
let triggerCompletedMs = 0
|
|
3316
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()
|
|
3317
3309
|
const triggerPromise = (async () => {
|
|
3318
3310
|
if (!options.hold) {
|
|
3319
3311
|
if (typeof options.trigger !== 'function') {
|
|
@@ -3349,6 +3341,7 @@ export const device = {
|
|
|
3349
3341
|
},
|
|
3350
3342
|
}
|
|
3351
3343
|
} catch (error) {
|
|
3344
|
+
captureFailure = { error }
|
|
3352
3345
|
await page
|
|
3353
3346
|
.evaluate((captureToken) => {
|
|
3354
3347
|
const registry = window.__sootsimVisibleChangeCaptures
|
|
@@ -3358,6 +3351,59 @@ export const device = {
|
|
|
3358
3351
|
.catch(() => {})
|
|
3359
3352
|
await capturePromise.catch(() => {})
|
|
3360
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
|
+
}
|
|
3361
3407
|
}
|
|
3362
3408
|
},
|
|
3363
3409
|
|
|
@@ -50,11 +50,15 @@ module.exports = class PeachEnvironment extends TestEnvironment {
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
async handleTestEvent(event) {
|
|
53
|
+
async handleTestEvent(event, state) {
|
|
54
54
|
if (event.name === 'hook_failure') {
|
|
55
55
|
this.controller.abort(new Error('Peach test hook failed'))
|
|
56
56
|
try {
|
|
57
57
|
await this.global.__peachDetoxCleanup?.()
|
|
58
|
+
} catch (error) {
|
|
59
|
+
// circus already recorded the hook failure. a cleanup failure belongs
|
|
60
|
+
// beside it, rather than escaping this handler and replacing the result.
|
|
61
|
+
state.unhandledErrors.push(error)
|
|
58
62
|
} finally {
|
|
59
63
|
this.releaseAdmission?.()
|
|
60
64
|
this.releaseAdmission = null
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! peach v0.1.
|
|
1
|
+
/*! peach v0.1.1641 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = '', __sootsim_import_meta_dirname = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; __sootsim_import_meta_dirname = __dirname; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __create = Object.create;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! peach v0.1.
|
|
1
|
+
/*! peach v0.1.1641 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = '', __sootsim_import_meta_dirname = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; __sootsim_import_meta_dirname = __dirname; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! peach v0.1.
|
|
1
|
+
/*! peach v0.1.1641 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = '', __sootsim_import_meta_dirname = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; __sootsim_import_meta_dirname = __dirname; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __create = Object.create;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! peach v0.1.
|
|
1
|
+
/*! peach v0.1.1641 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = '', __sootsim_import_meta_dirname = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; __sootsim_import_meta_dirname = __dirname; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __create = Object.create;
|