@unrulysystems/native-motion 0.1.0-alpha.0
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/CHANGELOG.md +16 -0
- package/LICENSE +21 -0
- package/README.md +55 -0
- package/android/build.gradle +24 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/com/unrulysystems/nativemotion/GestureExclusionModule.kt +55 -0
- package/android/src/main/java/com/unrulysystems/nativemotion/NativeMotionPackage.kt +27 -0
- package/package.json +57 -0
- package/react-native.config.js +14 -0
- package/src/native/driver/instantWindow.ts +31 -0
- package/src/native/driver/strictModeReplay.ts +20 -0
- package/src/native/driver/uiLayoutEngine.ts +845 -0
- package/src/native/driver/uiLayoutGraph.ts +338 -0
- package/src/native/driver/uiValueChannel.ts +4640 -0
- package/src/native/driver/workletDriver.ts +3860 -0
- package/src/native/motion/AnimatePresence.tsx +1605 -0
- package/src/native/motion/LayoutGroup.tsx +163 -0
- package/src/native/motion/MotionConfig.tsx +400 -0
- package/src/native/motion/MotionRoot.tsx +250 -0
- package/src/native/motion/MotionView.tsx +6761 -0
- package/src/native/motion/addScaleCorrector.ts +97 -0
- package/src/native/motion/colorEndpointUiFeed.ts +32 -0
- package/src/native/motion/colorProjection.ts +39 -0
- package/src/native/motion/composeTransform.ts +271 -0
- package/src/native/motion/constraintMeasure.ts +157 -0
- package/src/native/motion/deferredPendingSnapshots.ts +25 -0
- package/src/native/motion/discreteProjection.ts +36 -0
- package/src/native/motion/dragAncestorPanContext.ts +28 -0
- package/src/native/motion/dragControls.ts +152 -0
- package/src/native/motion/dragGestureWiring.ts +1168 -0
- package/src/native/motion/dragHandoffBinding.ts +179 -0
- package/src/native/motion/dragHubStream.ts +756 -0
- package/src/native/motion/dragPropagationLock.ts +57 -0
- package/src/native/motion/driverValueChannel.ts +3288 -0
- package/src/native/motion/externalDragDriver.ts +214 -0
- package/src/native/motion/frameData.ts +31 -0
- package/src/native/motion/gestureBinding.ts +107 -0
- package/src/native/motion/gestureStateGate.ts +563 -0
- package/src/native/motion/gestureStateResolver.ts +286 -0
- package/src/native/motion/identityValueChannelControllerAdapter.ts +2043 -0
- package/src/native/motion/identityValueChannelControllerReconciliation.ts +176 -0
- package/src/native/motion/identityValueChannelLaneMarker.ts +14 -0
- package/src/native/motion/imperativeAnimate.ts +816 -0
- package/src/native/motion/keyframeTiming.ts +7 -0
- package/src/native/motion/layoutIdBinding.ts +261 -0
- package/src/native/motion/layoutIdFlightConfig.ts +51 -0
- package/src/native/motion/layoutProjection.ts +89 -0
- package/src/native/motion/layoutScroll.ts +204 -0
- package/src/native/motion/layoutTransition.ts +1302 -0
- package/src/native/motion/lengthLayoutContext.tsx +100 -0
- package/src/native/motion/lengthLayoutHost.ts +44 -0
- package/src/native/motion/mappedKeys.ts +184 -0
- package/src/native/motion/motionViewController.ts +3180 -0
- package/src/native/motion/nativeHostMarker.ts +22 -0
- package/src/native/motion/panSessionWiring.ts +127 -0
- package/src/native/motion/pathTransition.ts +101 -0
- package/src/native/motion/popLayout.ts +176 -0
- package/src/native/motion/presenceBinding.ts +382 -0
- package/src/native/motion/scaleCorrectorRegistry.ts +123 -0
- package/src/native/motion/scrollValues.ts +260 -0
- package/src/native/motion/serializablePayload.ts +151 -0
- package/src/native/motion/severity.ts +16 -0
- package/src/native/motion/shippedSurface.ts +843 -0
- package/src/native/motion/staticLengthGate.ts +86 -0
- package/src/native/motion/styleBaseGate.ts +72 -0
- package/src/native/motion/styleValueBinding.ts +657 -0
- package/src/native/motion/systemGestureExclusion.ts +61 -0
- package/src/native/motion/tapGestureWiring.ts +215 -0
- package/src/native/motion/transformOrder.ts +38 -0
- package/src/native/motion/transformStringBinding.ts +200 -0
- package/src/native/motion/transformTemplateGate.ts +45 -0
- package/src/native/motion/transitionGate.ts +435 -0
- package/src/native/motion/useAnimate.ts +52 -0
- package/src/native/motion/useCycle.ts +44 -0
- package/src/native/motion/useInstantTransition.ts +68 -0
- package/src/native/motion/useReducedMotion.ts +58 -0
- package/src/native/motion/useScroll.ts +162 -0
- package/src/native/motion/useViewportScroll.ts +29 -0
- package/src/native/motion/valueChannel.ts +7121 -0
- package/src/native/motion/valueHooks.ts +901 -0
- package/src/native/motion/variantChildRegistry.ts +67 -0
- package/src/native/motion/variantContext.tsx +172 -0
- package/src/native/motion/variantProps.ts +598 -0
- package/src/native.ts +159 -0
- package/src/verification/harnessMetrics.ts +78 -0
- package/src/verification/probe/LayoutIdentityWorkletProbe.tsx +251 -0
- package/src/verification/probe/WorkletParityProbe.tsx +162 -0
- package/src/verification/probe/layoutIdentityProbeEngine.ts +429 -0
- package/src/verification/screens/ArcPathChecksScreen.tsx +273 -0
- package/src/verification/screens/BooleanAnimateChecksScreen.tsx +358 -0
- package/src/verification/screens/ChoreographyGalleryScreen.tsx +1010 -0
- package/src/verification/screens/ColorBindingChecksScreen.tsx +568 -0
- package/src/verification/screens/CompletionChecksScreen.tsx +294 -0
- package/src/verification/screens/ConformanceScreen.tsx +573 -0
- package/src/verification/screens/ContentionProbeScreen.tsx +224 -0
- package/src/verification/screens/DriverSmokeScreen.tsx +99 -0
- package/src/verification/screens/DurationOnlyTweenChecksScreen.tsx +294 -0
- package/src/verification/screens/DynamicDragConfigChecksScreen.tsx +237 -0
- package/src/verification/screens/FrameDataChecksScreen.tsx +252 -0
- package/src/verification/screens/GestureChecksScreen.tsx +685 -0
- package/src/verification/screens/InstantTransitionChecksScreen.tsx +839 -0
- package/src/verification/screens/LayoutAnimationStartChecksScreen.tsx +648 -0
- package/src/verification/screens/LayoutChecksScreen.tsx +822 -0
- package/src/verification/screens/LayoutCommitSpikeScreen.tsx +142 -0
- package/src/verification/screens/MotionViewChecksScreen.tsx +930 -0
- package/src/verification/screens/PresenceChecksScreen.tsx +614 -0
- package/src/verification/screens/ReducedMotionChecksScreen.tsx +614 -0
- package/src/verification/screens/RestThresholdChecksScreen.tsx +634 -0
- package/src/verification/screens/ScaleCorrectorChecksScreen.tsx +425 -0
- package/src/verification/screens/SharedLayoutContinuityChecksScreen.tsx +338 -0
- package/src/verification/screens/SharedLayoutCrossfadeChecksScreen.tsx +425 -0
- package/src/verification/screens/TransitionDefaultSelectionChecksScreen.tsx +514 -0
- package/src/verification/screens/ViewportScrollAliasChecksScreen.tsx +303 -0
- package/src/verification/screens/conformanceBanner.ts +21 -0
- package/src/verification/screens/proofConsoleTap.ts +18 -0
- package/src/verification.ts +47 -0
- package/src/web.ts +98 -0
|
@@ -0,0 +1,648 @@
|
|
|
1
|
+
// U7e layout-animation-start device checks (the U7e packet's device floor): a NATIVE nested
|
|
2
|
+
// layout card with the start/complete readouts on screen, witnessing live on device the
|
|
3
|
+
// per-flight callback law the W2 web leg pinned against the raw pin (u7e-layout-animation-
|
|
4
|
+
// start.e2e.ts — startAnimation's onPlay/onComplete, create-projection-node.ts:567-574):
|
|
5
|
+
//
|
|
6
|
+
// - start fires ONCE per LOGICAL flight at flight creation (the host-layout worklet's UI
|
|
7
|
+
// commit edge; the public crossing is qualified per React layout-signature generation —
|
|
8
|
+
// the pin's didUpdate analogue — while the internal beginCombinedLayoutFlight reset still
|
|
9
|
+
// runs at every commit, F1), STRICTLY BEFORE that flight's
|
|
10
|
+
// complete (the JS-thread FIFO read: both callbacks land via the same scheduleOnRN
|
|
11
|
+
// crossing, so JS receipt order is the law);
|
|
12
|
+
// - a mid-flight retarget fires start AGAIN for the new flight and the interrupted flight
|
|
13
|
+
// NEVER completes (the pin stops the in-flight animation first — stop never fires
|
|
14
|
+
// complete), so start count > complete count across the interruption and complete catches
|
|
15
|
+
// up only for the final settled flight;
|
|
16
|
+
// - a mount with no layout change fires neither callback.
|
|
17
|
+
//
|
|
18
|
+
// The subject mirrors the pinned catalog Animation-layout-nested-position.tsx (read-only
|
|
19
|
+
// reference, ../motion/dev/react/src/examples/): the three-deep `layout` tree (parent → child
|
|
20
|
+
// → grandchild animating on parent size-delta propagation) on the verbatim 3s circIn tween,
|
|
21
|
+
// useCycle(true, false) toggling open/closed, callbacks on the PARENT. Recorded adaptations
|
|
22
|
+
// (the U7d/U7e-W2 pattern): geometry halved (400/200/150 → 200/100/75, left 400 → 200) to fit
|
|
23
|
+
// the screen, the catalog's onClick-on-parent becomes an explicit toggle button (the on-screen
|
|
24
|
+
// control; a mid-flight press IS the retarget trigger), and the console.log callbacks become
|
|
25
|
+
// count + mount-relative-stamp latches — the same signal a screenshot reads.
|
|
26
|
+
//
|
|
27
|
+
// The screen SELF-DRIVES (the iOS simulator has no tap injection): after a mount-quiet dwell
|
|
28
|
+
// the timeline presses once (flight A), awaits its settle with a fail-closed poll, then fires
|
|
29
|
+
// the retarget pair (flight B, then flight C 150ms after B's start is observed — waiting for
|
|
30
|
+
// the start first avoids the pendingAnimation frame race, the W2 discipline). Four rows:
|
|
31
|
+
//
|
|
32
|
+
// 1. MOUNT QUIET — zero starts and zero completes across the boot dwell (a mount-snap
|
|
33
|
+
// miswire treating the initial measure as a qualifying commit fails here).
|
|
34
|
+
// 2. ONE FLIGHT — exactly one start and one complete for flight A; the start stamp follows
|
|
35
|
+
// the press stamp inside a bounded window (start fires at flight CREATION, not at the
|
|
36
|
+
// first frame or the settle) and strictly precedes the complete; the complete lands a
|
|
37
|
+
// full 3s-tween class after the start (a settle-deferred start reads complete − start ≈
|
|
38
|
+
// 0 and fails the floor; a per-frame start miswire fails starts === 1).
|
|
39
|
+
// 3. RETARGET — across flights B and C: starts reaches 3 while completes holds at 1 (the
|
|
40
|
+
// latched gap: B interrupted + C in flight), then C's settle brings completes to exactly
|
|
41
|
+
// 2 = starts − 1 (B's complete NEVER arrives, verified across a post-settle stability
|
|
42
|
+
// dwell), with C's complete strictly after C's start and a full 3s class after it (a
|
|
43
|
+
// retarget continuing B's clock completes early and fails the floor).
|
|
44
|
+
// 4. CLEAN RUN — zero refusal reports through the console tap (the U5/U7c pattern:
|
|
45
|
+
// console.error is the severity law's production error channel) across the whole
|
|
46
|
+
// timeline.
|
|
47
|
+
//
|
|
48
|
+
// A screenshot is the assertion; every stamp is mount-relative Date.now() at JS receipt, and
|
|
49
|
+
// every wait is a fail-closed poll with a loud deadline — a dropped callback is a timeout,
|
|
50
|
+
// never a silent skip. The console tap is evidence capture, never suppression.
|
|
51
|
+
|
|
52
|
+
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
53
|
+
import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native'
|
|
54
|
+
import { MotionView } from '../../native/motion/MotionView'
|
|
55
|
+
import { ambientNativeSeverity } from '../../native/motion/severity'
|
|
56
|
+
import { useCycle } from '../../native/motion/useCycle'
|
|
57
|
+
|
|
58
|
+
const ROW_MOUNT = 'mount quiet: no layout change fires neither start nor complete'
|
|
59
|
+
const ROW_FLIGHT = 'one flight: start fires exactly once, strictly before its complete (3s class)'
|
|
60
|
+
const ROW_RETARGET = 'mid-flight retarget: start re-fires, the stopped flight never completes'
|
|
61
|
+
const ROW_CLEAN = 'clean run: zero refusal reports through the console tap'
|
|
62
|
+
const ROWS = [ROW_MOUNT, ROW_FLIGHT, ROW_RETARGET, ROW_CLEAN] as const
|
|
63
|
+
|
|
64
|
+
// The catalog's transition verbatim (Animation-layout-nested-position.tsx:6).
|
|
65
|
+
const TRANSITION = { duration: 3, ease: 'circIn' } as const
|
|
66
|
+
|
|
67
|
+
// Row 2 bounds (ms): the start stamp must follow its press stamp (JS FIFO) inside a bounded
|
|
68
|
+
// window — the pin's web witness reads 3–5ms against a ~50ms Cypress witness; the native
|
|
69
|
+
// start crosses UI → JS at the commit edge, so the bound carries generous delivery slack
|
|
70
|
+
// while still failing a settle-deferred start (~3000ms late) loud. The flight floor is the
|
|
71
|
+
// W2 leg's verbatim: the 3s tween must run its course (> 2000); a settle-deferred start reads
|
|
72
|
+
// complete − start ≈ 0.
|
|
73
|
+
const START_LATENCY_BOUND_MS = 1000
|
|
74
|
+
const FLIGHT_FLOOR_MS = 2000
|
|
75
|
+
// Row 3's retarget spacing: flight C's press lands 150ms after flight B's START is observed
|
|
76
|
+
// (waiting for the start first avoids the pendingAnimation frame race — a retarget inside it
|
|
77
|
+
// would replace the pending flight and fire start only once, a fixture race, not the law).
|
|
78
|
+
const RETARGET_DELAY_MS = 150
|
|
79
|
+
// Post-settle stability dwell: a miswired late complete (a stop-fires-complete for the
|
|
80
|
+
// interrupted flight) flushes well inside this absence window.
|
|
81
|
+
const STABILITY_MS = 600
|
|
82
|
+
|
|
83
|
+
// Fail-closed deadlines (ms post-arm): a callback that never fires is a loud timeout, never
|
|
84
|
+
// a silent skip. The settle deadline comfortably covers the 3s tween plus delivery.
|
|
85
|
+
const START_DEADLINE_MS = 6000
|
|
86
|
+
const SETTLE_DEADLINE_MS = 12000
|
|
87
|
+
|
|
88
|
+
// Timeline: the mount-quiet dwell doubles as row 1's absence window (2000ms is many times
|
|
89
|
+
// every observed start latency); flight A's press fires at its end. Every later step is
|
|
90
|
+
// chained off fail-closed polls, never fixed sleeps.
|
|
91
|
+
const T_TRIGGER_A = 2000
|
|
92
|
+
|
|
93
|
+
interface RowResult {
|
|
94
|
+
readonly pass: boolean
|
|
95
|
+
readonly detail: string
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// The latched readouts (the W2 fixture's data attributes, mirrored at native altitude):
|
|
99
|
+
// counts plus mount-relative stamps (-1 = not yet). JS receipt order is the ordering law.
|
|
100
|
+
interface Latches {
|
|
101
|
+
presses: number
|
|
102
|
+
starts: number
|
|
103
|
+
completes: number
|
|
104
|
+
firstPressAt: number
|
|
105
|
+
lastPressAt: number
|
|
106
|
+
firstStartAt: number
|
|
107
|
+
lastStartAt: number
|
|
108
|
+
lastCompleteAt: number
|
|
109
|
+
startAts: number[]
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const freshLatches = (): Latches => ({
|
|
113
|
+
presses: 0,
|
|
114
|
+
starts: 0,
|
|
115
|
+
completes: 0,
|
|
116
|
+
firstPressAt: -1,
|
|
117
|
+
lastPressAt: -1,
|
|
118
|
+
firstStartAt: -1,
|
|
119
|
+
lastStartAt: -1,
|
|
120
|
+
lastCompleteAt: -1,
|
|
121
|
+
startAts: [],
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
// Latch rendering: mount-relative stamp, — while unfired.
|
|
125
|
+
const stamp = (at: number): string => (at === -1 ? '—' : `+${at.toFixed(0)}ms`)
|
|
126
|
+
|
|
127
|
+
export function LayoutAnimationStartChecksScreen() {
|
|
128
|
+
const severity = ambientNativeSeverity()
|
|
129
|
+
|
|
130
|
+
const [isOpen, toggleIsOpen] = useCycle(true, false)
|
|
131
|
+
const [stage, setStage] = useState('boot: mount-quiet dwell (open tree, no layout change)')
|
|
132
|
+
const [live, setLive] = useState<Latches>(freshLatches())
|
|
133
|
+
const [rows, setRows] = useState<Partial<Record<(typeof ROWS)[number], RowResult>>>({})
|
|
134
|
+
|
|
135
|
+
const mountStampRef = useRef(Date.now())
|
|
136
|
+
const latchesRef = useRef<Latches | null>(null)
|
|
137
|
+
if (latchesRef.current === null) latchesRef.current = freshLatches()
|
|
138
|
+
|
|
139
|
+
// Row 3's evidence: starts − completes latched at flight C's start (expect 2 — flight B
|
|
140
|
+
// interrupted + flight C in flight).
|
|
141
|
+
const retargetGapRef = useRef(-1)
|
|
142
|
+
|
|
143
|
+
const rafsRef = useRef<ReturnType<typeof requestAnimationFrame>[]>([])
|
|
144
|
+
const reportsRef = useRef<string[]>([])
|
|
145
|
+
|
|
146
|
+
const trackRaf = (id: ReturnType<typeof requestAnimationFrame>): void => {
|
|
147
|
+
rafsRef.current.push(id)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const now = useCallback((): number => Date.now() - mountStampRef.current, [])
|
|
151
|
+
|
|
152
|
+
const publish = useCallback((): void => {
|
|
153
|
+
setLive({ ...latchesRef.current! })
|
|
154
|
+
}, [])
|
|
155
|
+
|
|
156
|
+
// The toggle IS the retarget trigger: the on-screen control and the auto timeline share it.
|
|
157
|
+
const press = useCallback((): void => {
|
|
158
|
+
const latches = latchesRef.current!
|
|
159
|
+
const at = now()
|
|
160
|
+
latches.presses += 1
|
|
161
|
+
if (latches.firstPressAt === -1) latches.firstPressAt = at
|
|
162
|
+
latches.lastPressAt = at
|
|
163
|
+
publish()
|
|
164
|
+
toggleIsOpen()
|
|
165
|
+
}, [now, publish, toggleIsOpen])
|
|
166
|
+
|
|
167
|
+
const onStart = useCallback((): void => {
|
|
168
|
+
const latches = latchesRef.current!
|
|
169
|
+
const at = now()
|
|
170
|
+
latches.starts += 1
|
|
171
|
+
if (latches.firstStartAt === -1) latches.firstStartAt = at
|
|
172
|
+
latches.lastStartAt = at
|
|
173
|
+
latches.startAts = [...latches.startAts, at]
|
|
174
|
+
publish()
|
|
175
|
+
}, [now, publish])
|
|
176
|
+
|
|
177
|
+
const onComplete = useCallback((): void => {
|
|
178
|
+
const latches = latchesRef.current!
|
|
179
|
+
latches.completes += 1
|
|
180
|
+
latches.lastCompleteAt = now()
|
|
181
|
+
publish()
|
|
182
|
+
}, [now, publish])
|
|
183
|
+
|
|
184
|
+
const finalize = (id: (typeof ROWS)[number], pass: boolean, detail: string): void => {
|
|
185
|
+
setRows((previous) => ({ ...previous, [id]: { pass, detail } }))
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
useEffect(() => {
|
|
189
|
+
const latches = latchesRef.current!
|
|
190
|
+
|
|
191
|
+
// The production error channel tap (the U5/U7c pattern): refusal reports ride
|
|
192
|
+
// consoleReporter → console.error. Installed for this screen's lifetime only; everything
|
|
193
|
+
// passes through.
|
|
194
|
+
const originalError = console.error
|
|
195
|
+
console.error = (...args: unknown[]) => {
|
|
196
|
+
for (const arg of args) {
|
|
197
|
+
const message = arg instanceof Error ? arg.message : typeof arg === 'string' ? arg : null
|
|
198
|
+
if (message !== null && /motion/i.test(message)) reportsRef.current.push(message)
|
|
199
|
+
}
|
|
200
|
+
originalError(...args)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const timers: ReturnType<typeof setTimeout>[] = []
|
|
204
|
+
const at = (ms: number, run: () => void): void => {
|
|
205
|
+
timers.push(setTimeout(run, ms))
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Fail-closed observation primitive: poll per rAF until the condition holds or the
|
|
209
|
+
// deadline passes. `onResolve` fires exactly once with ok=false on the deadline.
|
|
210
|
+
const pollUntil = (
|
|
211
|
+
condition: () => boolean,
|
|
212
|
+
deadlineAt: number,
|
|
213
|
+
onResolve: (ok: boolean) => void,
|
|
214
|
+
): void => {
|
|
215
|
+
const step = (): void => {
|
|
216
|
+
if (condition()) {
|
|
217
|
+
onResolve(true)
|
|
218
|
+
return
|
|
219
|
+
}
|
|
220
|
+
if (now() >= deadlineAt) {
|
|
221
|
+
onResolve(false)
|
|
222
|
+
return
|
|
223
|
+
}
|
|
224
|
+
trackRaf(requestAnimationFrame(step))
|
|
225
|
+
}
|
|
226
|
+
trackRaf(requestAnimationFrame(step))
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// 4. CLEAN RUN + terminal stage, scheduled by whichever chain end lands last.
|
|
230
|
+
const finish = (): void => {
|
|
231
|
+
at(STABILITY_MS, () => {
|
|
232
|
+
const reports = reportsRef.current
|
|
233
|
+
finalize(
|
|
234
|
+
ROW_CLEAN,
|
|
235
|
+
reports.length === 0,
|
|
236
|
+
reports.length === 0
|
|
237
|
+
? `0 refusal reports through the console tap across ${now().toFixed(0)}ms of ` +
|
|
238
|
+
'the mount dwell, flight A, and the B/C retarget pair'
|
|
239
|
+
: `${reports.length} report(s): ${reports
|
|
240
|
+
.map((message) => `"${message.slice(0, 80)}…"`)
|
|
241
|
+
.join(' | ')}`,
|
|
242
|
+
)
|
|
243
|
+
setStage('done')
|
|
244
|
+
})
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const countsLine = (): string =>
|
|
248
|
+
`presses ${latches.presses} · starts ${latches.starts} · completes ${latches.completes}`
|
|
249
|
+
|
|
250
|
+
// 3. RETARGET — flight B, then flight C RETARGET_DELAY_MS after B's start is observed.
|
|
251
|
+
// Finalizes after C's settle plus the stability dwell: starts 3, completes EXACTLY 2
|
|
252
|
+
// (B's complete never arrives), the latched gap 2, C's complete strictly after C's
|
|
253
|
+
// start and a full 3s class after it.
|
|
254
|
+
const runRetargetPhase = (): void => {
|
|
255
|
+
setStage('flight A settled — pressing flight B (the retarget source)')
|
|
256
|
+
press()
|
|
257
|
+
pollUntil(
|
|
258
|
+
() => latches.starts >= 2,
|
|
259
|
+
now() + START_DEADLINE_MS,
|
|
260
|
+
(okB) => {
|
|
261
|
+
if (!okB) {
|
|
262
|
+
finalize(
|
|
263
|
+
ROW_RETARGET,
|
|
264
|
+
false,
|
|
265
|
+
`flight B's start never fired within the ${START_DEADLINE_MS}ms deadline ` +
|
|
266
|
+
`(${countsLine()}) — the per-flight law is unwitnessable (a start-once-per-` +
|
|
267
|
+
'component miswire leaves starts at 1)',
|
|
268
|
+
)
|
|
269
|
+
setStage('retarget row finalized (FAIL)')
|
|
270
|
+
finish()
|
|
271
|
+
return
|
|
272
|
+
}
|
|
273
|
+
at(RETARGET_DELAY_MS, () => {
|
|
274
|
+
setStage('flight B in flight — pressing flight C (the mid-flight retarget)')
|
|
275
|
+
press()
|
|
276
|
+
pollUntil(
|
|
277
|
+
() => latches.starts >= 3,
|
|
278
|
+
now() + START_DEADLINE_MS,
|
|
279
|
+
(okC) => {
|
|
280
|
+
if (!okC) {
|
|
281
|
+
finalize(
|
|
282
|
+
ROW_RETARGET,
|
|
283
|
+
false,
|
|
284
|
+
`flight C's start never fired within the ${START_DEADLINE_MS}ms deadline ` +
|
|
285
|
+
`(${countsLine()}) — the retarget did not start a new flight`,
|
|
286
|
+
)
|
|
287
|
+
setStage('retarget row finalized (FAIL)')
|
|
288
|
+
finish()
|
|
289
|
+
return
|
|
290
|
+
}
|
|
291
|
+
retargetGapRef.current = latches.starts - latches.completes
|
|
292
|
+
setStage(
|
|
293
|
+
`flight C in flight (starts − completes = ${retargetGapRef.current}: ` +
|
|
294
|
+
'B interrupted + C in flight) — awaiting its settle',
|
|
295
|
+
)
|
|
296
|
+
pollUntil(
|
|
297
|
+
() => latches.completes >= 2,
|
|
298
|
+
now() + SETTLE_DEADLINE_MS,
|
|
299
|
+
(okSettle) => {
|
|
300
|
+
if (!okSettle) {
|
|
301
|
+
finalize(
|
|
302
|
+
ROW_RETARGET,
|
|
303
|
+
false,
|
|
304
|
+
`flight C never completed within the ${SETTLE_DEADLINE_MS}ms deadline ` +
|
|
305
|
+
`(${countsLine()}) — the retargeted flight never settled`,
|
|
306
|
+
)
|
|
307
|
+
setStage('retarget row finalized (FAIL)')
|
|
308
|
+
finish()
|
|
309
|
+
return
|
|
310
|
+
}
|
|
311
|
+
// The stability dwell: a stop-fires-complete miswire for the interrupted
|
|
312
|
+
// flight B flushes a THIRD complete inside this window.
|
|
313
|
+
at(STABILITY_MS, () => {
|
|
314
|
+
const gap = retargetGapRef.current
|
|
315
|
+
const flightC = latches.lastCompleteAt - latches.lastStartAt
|
|
316
|
+
let bad: string | null = null
|
|
317
|
+
if (latches.starts !== 3 || latches.completes !== 2) {
|
|
318
|
+
bad =
|
|
319
|
+
`post-settle counts read ${countsLine()}, want starts 3 / completes ` +
|
|
320
|
+
'2 — ' +
|
|
321
|
+
(latches.completes > 2
|
|
322
|
+
? "the interrupted flight B's complete FIRED (a stop()-fires-" +
|
|
323
|
+
'complete miswire; the pin stops the in-flight animation first ' +
|
|
324
|
+
'and stop never fires complete)'
|
|
325
|
+
: 'a flight fired start without ever completing or a start was ' +
|
|
326
|
+
'dropped')
|
|
327
|
+
} else if (gap !== 2) {
|
|
328
|
+
bad =
|
|
329
|
+
`the interruption gap latched ${gap}, want 2 (B interrupted + C in ` +
|
|
330
|
+
'flight while completes held at 1) — start count never led complete ' +
|
|
331
|
+
'count across the retarget'
|
|
332
|
+
} else if (latches.lastCompleteAt <= latches.lastStartAt) {
|
|
333
|
+
bad =
|
|
334
|
+
`flight C's complete (${latches.lastCompleteAt.toFixed(0)}ms) did ` +
|
|
335
|
+
`not strictly follow its start (${latches.lastStartAt.toFixed(0)}ms)`
|
|
336
|
+
} else if (flightC <= FLIGHT_FLOOR_MS) {
|
|
337
|
+
bad =
|
|
338
|
+
`flight C ran ${flightC.toFixed(0)}ms (floor ${FLIGHT_FLOOR_MS}ms) — ` +
|
|
339
|
+
"the retarget continued flight B's clock instead of starting a " +
|
|
340
|
+
'fresh 3s tween'
|
|
341
|
+
}
|
|
342
|
+
finalize(
|
|
343
|
+
ROW_RETARGET,
|
|
344
|
+
bad === null,
|
|
345
|
+
bad ??
|
|
346
|
+
`two flights, two fresh starts: starts 3 / completes 2 = starts − 1 ` +
|
|
347
|
+
`(the interrupted flight B never completed, stable across a ` +
|
|
348
|
+
`${STABILITY_MS}ms post-settle dwell); the gap latched 2 at C's ` +
|
|
349
|
+
`start; C's start +${latches.lastStartAt.toFixed(0)}ms strictly ` +
|
|
350
|
+
`precedes its complete +${latches.lastCompleteAt.toFixed(0)}ms, a ` +
|
|
351
|
+
`${flightC.toFixed(0)}ms flight (floor ${FLIGHT_FLOOR_MS}ms — a ` +
|
|
352
|
+
'FRESH 3s tween, not B’s clock)',
|
|
353
|
+
)
|
|
354
|
+
setStage('retarget row finalized')
|
|
355
|
+
finish()
|
|
356
|
+
})
|
|
357
|
+
},
|
|
358
|
+
)
|
|
359
|
+
},
|
|
360
|
+
)
|
|
361
|
+
})
|
|
362
|
+
},
|
|
363
|
+
)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// THE TRIGGER — row 1 finalizes on the dwell's evidence, then flight A's press fires and
|
|
367
|
+
// row 2 awaits its settle with a fail-closed poll.
|
|
368
|
+
at(T_TRIGGER_A, () => {
|
|
369
|
+
finalize(
|
|
370
|
+
ROW_MOUNT,
|
|
371
|
+
latches.starts === 0 && latches.completes === 0,
|
|
372
|
+
latches.starts === 0 && latches.completes === 0
|
|
373
|
+
? `0 starts, 0 completes across the ${T_TRIGGER_A}ms boot dwell with the open tree ` +
|
|
374
|
+
'mounted — the initial measure never qualified as a layout flight'
|
|
375
|
+
: `mount fired ${latches.starts} start(s) / ${latches.completes} complete(s) with ` +
|
|
376
|
+
'no layout change — the initial measure was treated as a qualifying commit ' +
|
|
377
|
+
'(the mount-snap miswire)',
|
|
378
|
+
)
|
|
379
|
+
setStage('mount-quiet row finalized — pressing flight A (open → closed)')
|
|
380
|
+
const pressAAt = now()
|
|
381
|
+
press()
|
|
382
|
+
// A failed flight A leaves the retarget sequence unwitnessable — fail the row loud,
|
|
383
|
+
// never leave it pending (a pending row holds the banner at RUNNING forever).
|
|
384
|
+
const failRetargetUnwitnessable = (why: string): void => {
|
|
385
|
+
finalize(ROW_RETARGET, false, `unwitnessable — ${why}`)
|
|
386
|
+
}
|
|
387
|
+
pollUntil(
|
|
388
|
+
() => latches.starts >= 1,
|
|
389
|
+
now() + START_DEADLINE_MS,
|
|
390
|
+
(okStart) => {
|
|
391
|
+
if (!okStart) {
|
|
392
|
+
const why =
|
|
393
|
+
`flight A's start never fired within the ${START_DEADLINE_MS}ms deadline ` +
|
|
394
|
+
`(${countsLine()}) — the pre-W1 class (the prop leaking to the host, never ` +
|
|
395
|
+
'composed at the commit edge)'
|
|
396
|
+
finalize(ROW_FLIGHT, false, why)
|
|
397
|
+
failRetargetUnwitnessable(why)
|
|
398
|
+
setStage('flight row finalized (FAIL)')
|
|
399
|
+
finish()
|
|
400
|
+
return
|
|
401
|
+
}
|
|
402
|
+
pollUntil(
|
|
403
|
+
() => latches.completes >= 1,
|
|
404
|
+
now() + SETTLE_DEADLINE_MS,
|
|
405
|
+
(okSettle) => {
|
|
406
|
+
if (!okSettle) {
|
|
407
|
+
const why =
|
|
408
|
+
`flight A never completed within the ${SETTLE_DEADLINE_MS}ms deadline ` +
|
|
409
|
+
`(${countsLine()}) — the flight never settled`
|
|
410
|
+
finalize(ROW_FLIGHT, false, why)
|
|
411
|
+
failRetargetUnwitnessable(why)
|
|
412
|
+
setStage('flight row finalized (FAIL)')
|
|
413
|
+
finish()
|
|
414
|
+
return
|
|
415
|
+
}
|
|
416
|
+
const startAt = latches.firstStartAt
|
|
417
|
+
const completeAt = latches.lastCompleteAt
|
|
418
|
+
const startLatency = startAt - pressAAt
|
|
419
|
+
const flightMs = completeAt - startAt
|
|
420
|
+
let bad: string | null = null
|
|
421
|
+
if (latches.starts !== 1 || latches.completes !== 1) {
|
|
422
|
+
bad =
|
|
423
|
+
`one flight latched ${countsLine()}, want starts 1 / completes 1 — a ` +
|
|
424
|
+
'per-frame or re-fire-at-settle start miswire inflates starts'
|
|
425
|
+
} else if (startLatency < 0) {
|
|
426
|
+
bad =
|
|
427
|
+
`the start stamp (${startAt.toFixed(0)}ms) PRECEDES its press stamp ` +
|
|
428
|
+
`(${pressAAt.toFixed(0)}ms) — impossible on the JS thread's FIFO`
|
|
429
|
+
} else if (startLatency >= START_LATENCY_BOUND_MS) {
|
|
430
|
+
bad =
|
|
431
|
+
`start fired ${startLatency.toFixed(0)}ms after the press (bound ` +
|
|
432
|
+
`${START_LATENCY_BOUND_MS}ms) — start must fire at flight CREATION (the ` +
|
|
433
|
+
'commit edge), not at the first frame or the settle (~3000ms late)'
|
|
434
|
+
} else if (completeAt <= startAt) {
|
|
435
|
+
bad =
|
|
436
|
+
`complete (${completeAt.toFixed(0)}ms) did not strictly follow start ` +
|
|
437
|
+
`(${startAt.toFixed(0)}ms) — start must precede its own flight's complete`
|
|
438
|
+
} else if (flightMs <= FLIGHT_FLOOR_MS) {
|
|
439
|
+
bad =
|
|
440
|
+
`the flight ran ${flightMs.toFixed(0)}ms (floor ${FLIGHT_FLOOR_MS}ms) — the ` +
|
|
441
|
+
'3s circIn tween did not run its course (a settle-deferred start reads ' +
|
|
442
|
+
'complete − start ≈ 0)'
|
|
443
|
+
}
|
|
444
|
+
finalize(
|
|
445
|
+
ROW_FLIGHT,
|
|
446
|
+
bad === null,
|
|
447
|
+
bad ??
|
|
448
|
+
`flight A: exactly one start (+${startAt.toFixed(0)}ms, ` +
|
|
449
|
+
`${startLatency.toFixed(0)}ms after the press — flight creation, bound ` +
|
|
450
|
+
`${START_LATENCY_BOUND_MS}ms) STRICTLY BEFORE its one complete (` +
|
|
451
|
+
`+${completeAt.toFixed(0)}ms), a ${flightMs.toFixed(0)}ms flight (floor ` +
|
|
452
|
+
`${FLIGHT_FLOOR_MS}ms — the 3s circIn tween ran its course)`,
|
|
453
|
+
)
|
|
454
|
+
setStage('flight row finalized')
|
|
455
|
+
runRetargetPhase()
|
|
456
|
+
},
|
|
457
|
+
)
|
|
458
|
+
},
|
|
459
|
+
)
|
|
460
|
+
})
|
|
461
|
+
|
|
462
|
+
return () => {
|
|
463
|
+
for (const timer of timers) clearTimeout(timer)
|
|
464
|
+
for (const raf of rafsRef.current) cancelAnimationFrame(raf)
|
|
465
|
+
console.error = originalError
|
|
466
|
+
}
|
|
467
|
+
// All captured values are refs or stable callbacks; the timeline runs once per mount.
|
|
468
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
469
|
+
}, [])
|
|
470
|
+
|
|
471
|
+
const done = ROWS.every((id) => rows[id] !== undefined)
|
|
472
|
+
const failing = ROWS.filter((id) => rows[id] !== undefined && !rows[id]!.pass)
|
|
473
|
+
const verdict = !done ? 'RUNNING' : failing.length === 0 ? 'PASS' : 'FAIL'
|
|
474
|
+
const passed = Object.keys(rows).length - failing.length
|
|
475
|
+
|
|
476
|
+
return (
|
|
477
|
+
<ScrollView style={styles.scroll} contentContainerStyle={styles.container}>
|
|
478
|
+
<Text style={styles.title}>U7e layout-animation-start checks</Text>
|
|
479
|
+
<Text style={styles.subtitle}>
|
|
480
|
+
the catalog nested-position tree witnesses per-flight start/complete ({severity} severity)
|
|
481
|
+
</Text>
|
|
482
|
+
<View
|
|
483
|
+
style={[
|
|
484
|
+
styles.banner,
|
|
485
|
+
verdict === 'PASS' ? styles.pass : verdict === 'FAIL' ? styles.fail : styles.running,
|
|
486
|
+
]}
|
|
487
|
+
>
|
|
488
|
+
<Text style={styles.bannerText}>{verdict}</Text>
|
|
489
|
+
</View>
|
|
490
|
+
<Text style={styles.tally}>
|
|
491
|
+
{done
|
|
492
|
+
? `${passed} PASS / ${failing.length} FAIL of ${ROWS.length} rows`
|
|
493
|
+
: `${Object.keys(rows).length}/${ROWS.length} rows complete — RUNNING`}
|
|
494
|
+
</Text>
|
|
495
|
+
<Text style={styles.tallyDetail}>
|
|
496
|
+
{done
|
|
497
|
+
? failing.length === 0
|
|
498
|
+
? 'no failing rows'
|
|
499
|
+
: `FAILING: ${failing.join(' | ')}`
|
|
500
|
+
: `stage: ${stage}`}
|
|
501
|
+
</Text>
|
|
502
|
+
<Text style={styles.status}>
|
|
503
|
+
u7e stage: {stage} · presses={live.presses} starts={live.starts} completes=
|
|
504
|
+
{live.completes}
|
|
505
|
+
</Text>
|
|
506
|
+
<Text style={styles.status}>
|
|
507
|
+
press {stamp(live.firstPressAt)}/{stamp(live.lastPressAt)} · start{' '}
|
|
508
|
+
{stamp(live.firstStartAt)}/{stamp(live.lastStartAt)} · complete {stamp(live.lastCompleteAt)}
|
|
509
|
+
</Text>
|
|
510
|
+
<Text style={styles.status} accessibilityLabel="u7e-layout-start-stamps">
|
|
511
|
+
starts@[{live.startAts.map((at) => `+${at.toFixed(0)}`).join(',')}]
|
|
512
|
+
</Text>
|
|
513
|
+
|
|
514
|
+
{/* The on-screen control (the catalog's onClick-on-parent, made explicit): a mid-flight
|
|
515
|
+
press IS the retarget trigger. The auto timeline drives the same press for the
|
|
516
|
+
hands-free proof. */}
|
|
517
|
+
<Pressable style={styles.button} accessibilityLabel="u7e-layout-start-toggle" onPress={press}>
|
|
518
|
+
<Text style={styles.buttonText}>TOGGLE LAYOUT (a mid-flight press retargets)</Text>
|
|
519
|
+
</Pressable>
|
|
520
|
+
|
|
521
|
+
{/* The catalog's three-deep `layout` tree, geometry halved (the W2 card's adaptation):
|
|
522
|
+
parent 200↔100 at left 200↔0, child 75, grandchild 30% tall at 50%↔100% wide — the
|
|
523
|
+
grandchildren animate on parent size-delta propagation. Callbacks on the PARENT,
|
|
524
|
+
verbatim. The parent is white-on-dark (the catalog reads white-on-white; the screen
|
|
525
|
+
is dark), the blue child and red/yellow grandchild carry the visual. */}
|
|
526
|
+
<View style={styles.stage} accessibilityLabel="u7e-layout-start-stage">
|
|
527
|
+
<MotionView
|
|
528
|
+
layout
|
|
529
|
+
transition={TRANSITION}
|
|
530
|
+
onLayoutAnimationStart={onStart}
|
|
531
|
+
onLayoutAnimationComplete={onComplete}
|
|
532
|
+
style={[styles.parent, isOpen ? styles.parentOpen : styles.parentClosed]}
|
|
533
|
+
accessibilityLabel="u7e-layout-start-parent"
|
|
534
|
+
>
|
|
535
|
+
<MotionView
|
|
536
|
+
layout
|
|
537
|
+
transition={TRANSITION}
|
|
538
|
+
style={[styles.child, isOpen ? styles.childOpen : styles.childClosed]}
|
|
539
|
+
accessibilityLabel="u7e-layout-start-child"
|
|
540
|
+
>
|
|
541
|
+
<MotionView
|
|
542
|
+
layout
|
|
543
|
+
transition={TRANSITION}
|
|
544
|
+
style={[styles.grandchild, isOpen ? styles.grandchildOpen : styles.grandchildClosed]}
|
|
545
|
+
accessibilityLabel="u7e-layout-start-grandchild"
|
|
546
|
+
/>
|
|
547
|
+
</MotionView>
|
|
548
|
+
</MotionView>
|
|
549
|
+
</View>
|
|
550
|
+
|
|
551
|
+
{ROWS.map((id) => {
|
|
552
|
+
const row = rows[id]
|
|
553
|
+
return (
|
|
554
|
+
<View key={id} style={styles.row}>
|
|
555
|
+
<Text
|
|
556
|
+
style={[
|
|
557
|
+
styles.verdict,
|
|
558
|
+
row === undefined ? styles.pending : row.pass ? styles.ok : styles.bad,
|
|
559
|
+
]}
|
|
560
|
+
>
|
|
561
|
+
{row === undefined ? '…' : row.pass ? 'PASS' : 'FAIL'}
|
|
562
|
+
</Text>
|
|
563
|
+
<View style={styles.rowBody}>
|
|
564
|
+
<Text style={styles.rowId}>{id}</Text>
|
|
565
|
+
<Text style={styles.rowDetail} numberOfLines={4}>
|
|
566
|
+
{row === undefined ? 'pending' : row.detail}
|
|
567
|
+
</Text>
|
|
568
|
+
</View>
|
|
569
|
+
</View>
|
|
570
|
+
)
|
|
571
|
+
})}
|
|
572
|
+
</ScrollView>
|
|
573
|
+
)
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const styles = StyleSheet.create({
|
|
577
|
+
scroll: { flex: 1, backgroundColor: '#0b0b12' },
|
|
578
|
+
container: { paddingVertical: 72, paddingHorizontal: 8 },
|
|
579
|
+
title: { color: '#fff', fontSize: 22, fontWeight: '700', textAlign: 'center' },
|
|
580
|
+
subtitle: { color: '#8a8aa0', fontSize: 12, marginTop: 6, textAlign: 'center' },
|
|
581
|
+
banner: {
|
|
582
|
+
alignSelf: 'center',
|
|
583
|
+
paddingHorizontal: 36,
|
|
584
|
+
paddingVertical: 10,
|
|
585
|
+
borderRadius: 10,
|
|
586
|
+
marginTop: 18,
|
|
587
|
+
marginBottom: 10,
|
|
588
|
+
},
|
|
589
|
+
pass: { backgroundColor: '#1d7a3e' },
|
|
590
|
+
fail: { backgroundColor: '#a32633' },
|
|
591
|
+
running: { backgroundColor: '#7a641d' },
|
|
592
|
+
bannerText: { color: '#fff', fontSize: 22, fontWeight: '800', letterSpacing: 2 },
|
|
593
|
+
tally: { color: '#e6e6f0', fontSize: 14, fontWeight: '700', textAlign: 'center' },
|
|
594
|
+
tallyDetail: { color: '#f87171', fontSize: 11, marginTop: 3, textAlign: 'center' },
|
|
595
|
+
status: {
|
|
596
|
+
color: '#c9c9d8',
|
|
597
|
+
fontSize: 11,
|
|
598
|
+
marginTop: 8,
|
|
599
|
+
textAlign: 'center',
|
|
600
|
+
fontFamily: 'Menlo',
|
|
601
|
+
},
|
|
602
|
+
button: {
|
|
603
|
+
alignSelf: 'center',
|
|
604
|
+
marginTop: 14,
|
|
605
|
+
backgroundColor: '#1f2a44',
|
|
606
|
+
borderRadius: 8,
|
|
607
|
+
paddingHorizontal: 14,
|
|
608
|
+
paddingVertical: 10,
|
|
609
|
+
},
|
|
610
|
+
buttonText: { color: '#e6e6f0', fontSize: 12, fontWeight: '700', letterSpacing: 0.5 },
|
|
611
|
+
// The stage holds the open parent (200px at left 200 within the ~360px column) with room
|
|
612
|
+
// to spare; the parent is absolutely positioned inside it like the catalog's fixed stage.
|
|
613
|
+
stage: { height: 220, marginTop: 14 },
|
|
614
|
+
parent: {
|
|
615
|
+
position: 'absolute',
|
|
616
|
+
top: 0,
|
|
617
|
+
backgroundColor: '#e6e6f0',
|
|
618
|
+
display: 'flex',
|
|
619
|
+
},
|
|
620
|
+
parentOpen: {
|
|
621
|
+
width: 200,
|
|
622
|
+
height: 200,
|
|
623
|
+
left: 200,
|
|
624
|
+
alignItems: 'flex-end',
|
|
625
|
+
justifyContent: 'flex-end',
|
|
626
|
+
},
|
|
627
|
+
parentClosed: {
|
|
628
|
+
width: 100,
|
|
629
|
+
height: 100,
|
|
630
|
+
left: 0,
|
|
631
|
+
alignItems: 'flex-start',
|
|
632
|
+
justifyContent: 'flex-start',
|
|
633
|
+
},
|
|
634
|
+
child: { width: 75, height: 75, backgroundColor: '#3b82f6', display: 'flex' },
|
|
635
|
+
childOpen: { alignItems: 'center', justifyContent: 'center' },
|
|
636
|
+
childClosed: { alignItems: 'flex-start', justifyContent: 'flex-start' },
|
|
637
|
+
grandchild: { height: '30%' },
|
|
638
|
+
grandchildOpen: { width: '50%', backgroundColor: '#eab308' },
|
|
639
|
+
grandchildClosed: { width: '100%', backgroundColor: '#dc2626' },
|
|
640
|
+
row: { flexDirection: 'row', marginTop: 10 },
|
|
641
|
+
verdict: { width: 52, fontSize: 12, fontWeight: '800', paddingTop: 1 },
|
|
642
|
+
ok: { color: '#4ade80' },
|
|
643
|
+
bad: { color: '#f87171' },
|
|
644
|
+
pending: { color: '#8a8aa0' },
|
|
645
|
+
rowBody: { flex: 1 },
|
|
646
|
+
rowId: { color: '#e6e6f0', fontSize: 13, fontWeight: '600' },
|
|
647
|
+
rowDetail: { color: '#6f6f85', fontSize: 11, marginTop: 2 },
|
|
648
|
+
})
|