rnxsim 0.0.0 → 0.1.312
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 +21 -0
- package/README.md +512 -0
- package/detox/colors.ts +54 -0
- package/detox/config-loader.ts +135 -0
- package/detox/element-types.ts +39 -0
- package/detox/expectations.ts +537 -0
- package/detox/gestures.ts +480 -0
- package/detox/index.ts +2118 -0
- package/detox/jest-preset.cjs +51 -0
- package/detox/jest-setup-after-env.cjs +18 -0
- package/detox/matchers.ts +30 -0
- package/detox/motion-change.cjs +109 -0
- package/detox/motion-change.d.cts +35 -0
- package/detox/navigation.ts +43 -0
- package/detox/screenshots/animated-color-test-rest-norngh.png +0 -0
- package/detox/screenshots/color-test-after-drag-norngh.png +0 -0
- package/detox/screenshots/color-test-rest-norngh.png +0 -0
- package/detox/screenshots/theme-blue-toggle.png +0 -0
- package/detox/screenshots/theme-blue.png +0 -0
- package/detox/screenshots/theme-red-toggle.png +0 -0
- package/detox/screenshots/theme-red.png +0 -0
- package/dist-cli/bin.js +15 -0
- package/dist-cli/chunks/agent-NXVDDCFG.js +48 -0
- package/dist-cli/chunks/agent-wrapper-DXJCFKL3.js +17 -0
- package/dist-cli/chunks/app-fonts-W5AJZQJF.js +4 -0
- package/dist-cli/chunks/assert-3LOJEUDT.js +49 -0
- package/dist-cli/chunks/auth-MFTYYPNX.js +12 -0
- package/dist-cli/chunks/beta-GT3NN67B.js +4 -0
- package/dist-cli/chunks/camera-6EAK7CUL.js +22 -0
- package/dist-cli/chunks/chunk-274UD5FD.js +4 -0
- package/dist-cli/chunks/chunk-2SA2VTGP.js +5 -0
- package/dist-cli/chunks/chunk-33FKQH54.js +5 -0
- package/dist-cli/chunks/chunk-37I5AIS7.js +3 -0
- package/dist-cli/chunks/chunk-3D57IBPC.js +4 -0
- package/dist-cli/chunks/chunk-4BXCZHAU.js +4 -0
- package/dist-cli/chunks/chunk-5OX7ANRA.js +5 -0
- package/dist-cli/chunks/chunk-66NF2KNG.js +9 -0
- package/dist-cli/chunks/chunk-6UKJB2VC.js +125 -0
- package/dist-cli/chunks/chunk-7INBNA3U.js +26 -0
- package/dist-cli/chunks/chunk-7VWOSTD5.js +5 -0
- package/dist-cli/chunks/chunk-AY47WKS5.js +23 -0
- package/dist-cli/chunks/chunk-B7KMNHX7.js +4 -0
- package/dist-cli/chunks/chunk-BKNOLP3J.js +4 -0
- package/dist-cli/chunks/chunk-BRIZOR4K.js +28 -0
- package/dist-cli/chunks/chunk-DM6KUAOZ.js +4 -0
- package/dist-cli/chunks/chunk-EUAVBTKU.js +4 -0
- package/dist-cli/chunks/chunk-FTC4M26Q.js +4 -0
- package/dist-cli/chunks/chunk-FWYYCUAZ.js +4 -0
- package/dist-cli/chunks/chunk-G74YIEVS.js +7 -0
- package/dist-cli/chunks/chunk-GZDHEONW.js +507 -0
- package/dist-cli/chunks/chunk-HG7ORJMT.js +5 -0
- package/dist-cli/chunks/chunk-HMDOLCKG.js +15 -0
- package/dist-cli/chunks/chunk-JG6S4RSH.js +4 -0
- package/dist-cli/chunks/chunk-KR5BGMBZ.js +4 -0
- package/dist-cli/chunks/chunk-KZW5WBGU.js +983 -0
- package/dist-cli/chunks/chunk-L24Z3CRS.js +30 -0
- package/dist-cli/chunks/chunk-LDQ7HVCW.js +4 -0
- package/dist-cli/chunks/chunk-MENV7VLD.js +4 -0
- package/dist-cli/chunks/chunk-MNGQ42QE.js +568 -0
- package/dist-cli/chunks/chunk-N6A3SXMR.js +4 -0
- package/dist-cli/chunks/chunk-NLEMQ4PY.js +6 -0
- package/dist-cli/chunks/chunk-NPVGMH2N.js +101 -0
- package/dist-cli/chunks/chunk-OR3XTXEB.js +107 -0
- package/dist-cli/chunks/chunk-PEPUGJVI.js +4 -0
- package/dist-cli/chunks/chunk-Q3RGLERJ.js +142 -0
- package/dist-cli/chunks/chunk-SI3BUTQR.js +4 -0
- package/dist-cli/chunks/chunk-T4FT6CM7.js +4 -0
- package/dist-cli/chunks/chunk-TBMVBI26.js +7 -0
- package/dist-cli/chunks/chunk-TCWQJILS.js +5 -0
- package/dist-cli/chunks/chunk-TGQNQ6XT.js +766 -0
- package/dist-cli/chunks/chunk-TYE3WBSR.js +5 -0
- package/dist-cli/chunks/chunk-U4PN6JOT.js +6 -0
- package/dist-cli/chunks/chunk-VUFRB347.js +4 -0
- package/dist-cli/chunks/chunk-VZBLFMYC.js +10 -0
- package/dist-cli/chunks/chunk-WBT32R6N.js +25 -0
- package/dist-cli/chunks/chunk-XRGEEPJ5.js +38 -0
- package/dist-cli/chunks/chunk-Y2FSZSO7.js +71 -0
- package/dist-cli/chunks/chunk-YRV7HR3P.js +74 -0
- package/dist-cli/chunks/chunk-YZRFF4YA.js +14 -0
- package/dist-cli/chunks/chunk-ZMZTHU7V.js +4 -0
- package/dist-cli/chunks/cleanup-H3GWQB63.js +25 -0
- package/dist-cli/chunks/cli-version-3QPAJ3LC.js +4 -0
- package/dist-cli/chunks/compat-5WG2XJV7.js +20 -0
- package/dist-cli/chunks/config-ANMBVVLL.js +13 -0
- package/dist-cli/chunks/control-NSGNQFJG.js +4 -0
- package/dist-cli/chunks/daemon-JHF4BJ3U.js +4 -0
- package/dist-cli/chunks/debug-CHV2O2KF.js +200 -0
- package/dist-cli/chunks/desktop-FIPIK4CH.js +31 -0
- package/dist-cli/chunks/detox-CVUJP7K6.js +52 -0
- package/dist-cli/chunks/device-6WCUM4W6.js +18 -0
- package/dist-cli/chunks/diagnose-I6EMWEII.js +43 -0
- package/dist-cli/chunks/disk-cleanup-P27CYFTN.js +4 -0
- package/dist-cli/chunks/drivers-HQ6AKZT6.js +4 -0
- package/dist-cli/chunks/film-UOQFFYNF.js +74 -0
- package/dist-cli/chunks/flow-N27MRRNL.js +4 -0
- package/dist-cli/chunks/help-SF6FEGEE.js +4 -0
- package/dist-cli/chunks/hidden-runtime-alias-NJYFSJKZ.js +4 -0
- package/dist-cli/chunks/home-paths-XOV44JEF.js +4 -0
- package/dist-cli/chunks/inspect-CWFRFQTY.js +4 -0
- package/dist-cli/chunks/install-desktop-KLQDNXZQ.js +4 -0
- package/dist-cli/chunks/login-BPFQENCX.js +28 -0
- package/dist-cli/chunks/logout-5A45ZGY7.js +4 -0
- package/dist-cli/chunks/maestro-E4CSDMLM.js +104 -0
- package/dist-cli/chunks/maestro-generate-CKMOJQX2.js +36 -0
- package/dist-cli/chunks/mode-SZYNMIQF.js +26 -0
- package/dist-cli/chunks/optional-demo-registry-FRH4SFVG.js +4 -0
- package/dist-cli/chunks/perf-VXU2XJAX.js +48 -0
- package/dist-cli/chunks/permissions-IRIAOVND.js +13 -0
- package/dist-cli/chunks/record-RY7FPLLH.js +72 -0
- package/dist-cli/chunks/report-issue-KNWXOE53.js +29 -0
- package/dist-cli/chunks/runtime-AR3GJTGK.js +4 -0
- package/dist-cli/chunks/screenshot-command-QLALR6GY.js +159 -0
- package/dist-cli/chunks/screenshot-layers-OQFMFVRL.js +20 -0
- package/dist-cli/chunks/screenshots-capture-6TSRJFGB.js +42 -0
- package/dist-cli/chunks/serve-F54J2NQO.js +44 -0
- package/dist-cli/chunks/setup-7V6UKX4U.js +32 -0
- package/dist-cli/chunks/skills-QQBOSETH.js +50 -0
- package/dist-cli/chunks/storage-TAPMKK5O.js +37 -0
- package/dist-cli/chunks/store-7NX3S43H.js +4 -0
- package/dist-cli/chunks/telemetry-E7BUK6RA.js +4 -0
- package/dist-cli/chunks/timeline-IAM564NA.js +24 -0
- package/dist-cli/chunks/upgrade-CD55QXIX.js +7 -0
- package/dist-cli/chunks/upload-GZ6LIMYM.js +4 -0
- package/dist-cli/chunks/version-3DYJ5AS3.js +6 -0
- package/dist-cli/chunks/web-6NW6YLPI.js +4 -0
- package/dist-cli/chunks/what-happened-CZZDOOVG.js +25 -0
- package/dist-lib/agent-daemon-client.cjs +468 -0
- package/dist-lib/agent-events.cjs +48 -0
- package/dist-lib/agent-identity.cjs +53 -0
- package/dist-lib/agent-sessions.cjs +725 -0
- package/dist-lib/attached-projects.cjs +526 -0
- package/dist-lib/auth/shared-session.cjs +195 -0
- package/dist-lib/backend-origin.cjs +70 -0
- package/dist-lib/beta.cjs +41 -0
- package/dist-lib/beta.mjs +13 -0
- package/dist-lib/bridge-constants.cjs +161 -0
- package/dist-lib/cli-constants.cjs +32 -0
- package/dist-lib/config.cjs +96 -0
- package/dist-lib/detox/index.cjs +2863 -0
- package/dist-lib/detox/jest-preset.cjs +51 -0
- package/dist-lib/dev-bundle-resolution.cjs +223 -0
- package/dist-lib/home-paths.cjs +421 -0
- package/dist-lib/host/bridge-host.cjs +5600 -0
- package/dist-lib/host/fetch-proxy-handler.cjs +373 -0
- package/dist-lib/host/fetch-proxy-overrides.cjs +63 -0
- package/dist-lib/host/fetch-proxy-overrides.mjs +37 -0
- package/dist-lib/host/websocket-proxy.cjs +214 -0
- package/dist-lib/index.cjs +513 -0
- package/dist-lib/jump-to-source-babel.cjs +93 -0
- package/dist-lib/menu.cjs +620 -0
- package/dist-lib/menu.mjs +581 -0
- package/dist-lib/metro.cjs +297 -0
- package/dist-lib/profiles.cjs +246 -0
- package/dist-lib/public-brand.cjs +39 -0
- package/dist-lib/render-mode.cjs +54 -0
- package/dist-lib/scripts/dev-server-scanner.cjs +1034 -0
- package/dist-lib/sdk.cjs +2333 -0
- package/dist-lib/sdk.mjs +2240 -0
- package/dist-lib/skills.cjs +23911 -0
- package/dist-lib/vite.cjs +358 -0
- package/package.json +189 -18
- package/scripts/dev-server-scanner.ts +941 -0
- package/scripts/optional-demo-registry.ts +113 -0
- package/scripts/postinstall.cjs +3 -0
- package/scripts/preuninstall.cjs +118 -0
- package/skills/contrast/SKILL.md +171 -0
- package/skills/rnx-debug/SKILL.md +425 -0
- package/skills/rnx-perf/SKILL.md +295 -0
- package/skills/rnx-setup/SKILL.md +160 -0
- package/skills/rnx-test/SKILL.md +292 -0
- package/skills/rnx-visual/SKILL.md +122 -0
- package/src/agent-daemon-client.ts +410 -0
- package/src/agent-events.ts +71 -0
- package/src/agent-identity.ts +23 -0
- package/src/agent-prompt.ts +71 -0
- package/src/agent-sessions.ts +573 -0
- package/src/attached-projects.ts +539 -0
- package/src/auth/shared-session.ts +204 -0
- package/src/backend-origin.ts +49 -0
- package/src/beta.ts +19 -0
- package/src/bridge-constants.ts +171 -0
- package/src/browser-cache.ts +16 -0
- package/src/cli-constants.ts +1 -0
- package/src/cli-version.ts +32 -0
- package/src/codex-client.ts +215 -0
- package/src/config.ts +134 -0
- package/src/dev-bundle-resolution.ts +236 -0
- package/src/disk-cleanup.ts +466 -0
- package/src/home-paths.ts +552 -0
- package/src/host/agent-host.ts +666 -0
- package/src/host/bridge-host.ts +2626 -0
- package/src/host/fetch-proxy-handler.ts +397 -0
- package/src/host/fetch-proxy-overrides.ts +51 -0
- package/src/host/open-url.ts +89 -0
- package/src/host/websocket-proxy.ts +210 -0
- package/src/index.ts +12 -705
- package/src/jump-to-source-babel.ts +135 -0
- package/src/menu.ts +734 -0
- package/src/metro-plugin.ts +139 -0
- package/src/native-dev-bundle-url.ts +96 -0
- package/src/native-seam-manifest.ts +341 -0
- package/src/plist.ts +27 -0
- package/src/poll-until.ts +92 -0
- package/src/profiles.ts +179 -0
- package/src/public-brand.ts +9 -0
- package/src/render-mode.ts +27 -0
- package/src/runtime-assets.ts +228 -0
- package/src/runtime-delivery.ts +54 -0
- package/src/screenshots/compose.ts +422 -0
- package/src/screenshots/frame-compose.ts +454 -0
- package/src/screenshots/orchestrate.ts +256 -0
- package/src/screenshots/org-deck.ts +91 -0
- package/src/screenshots/registry.ts +58 -0
- package/src/screenshots/schema.ts +364 -0
- package/src/sdk.ts +7 -0
- package/src/settings-bundle.ts +418 -0
- package/src/skills/builtin/a11y-review.ts +132 -0
- package/src/skills/builtin/compat-check.ts +71 -0
- package/src/skills/builtin/maestro-test.ts +121 -0
- package/src/skills/builtin/perf-profile.ts +90 -0
- package/src/skills/builtin/screenshot-all.ts +52 -0
- package/src/skills/builtin/visual-diff.ts +100 -0
- package/src/skills/registry.ts +107 -0
- package/src/skills/types.ts +41 -0
- package/src/team-machine-prompt.ts +73 -0
- package/src/vite-plugin-one.ts +190 -0
- package/src/vite-plugin.ts +1750 -0
- package/src/worklets-babel.ts +134 -0
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
import { pollUntil } from '../src/poll-until'
|
|
2
|
+
// pull in @sootsim/globals so window.__sootsimTest and
|
|
3
|
+
// window.__sootsimFindInteractiveCanvas are typed inside page.evaluate
|
|
4
|
+
// bodies. all sootsim-namespaced globals are declared canonically there —
|
|
5
|
+
// add new entries to packages/sootsim-globals/src/index.ts, not here.
|
|
6
|
+
import type {} from '@sootsim/globals'
|
|
7
|
+
import type { Page } from 'playwright'
|
|
8
|
+
|
|
9
|
+
// shape of the surface-metrics bridge response we care about. the bridge
|
|
10
|
+
// types it as `Record<string, unknown> | null` (the contract is open) so we
|
|
11
|
+
// narrow at the call sites by casting to this local view.
|
|
12
|
+
type SurfaceMetricsView = {
|
|
13
|
+
window?: { width?: number; height?: number; scale?: number }
|
|
14
|
+
device?: { homeIndicatorHeight?: number }
|
|
15
|
+
safeAreaInsets?: { bottom?: number }
|
|
16
|
+
visibleRect?: { y?: number; height?: number }
|
|
17
|
+
}
|
|
18
|
+
type SurfaceMetricsResult = SurfaceMetricsView | null | undefined
|
|
19
|
+
|
|
20
|
+
type SootsimPageMetrics = {
|
|
21
|
+
canvas: { left: number; top: number; width: number; height: number }
|
|
22
|
+
window: { width: number; height: number }
|
|
23
|
+
interactiveWindow: { width: number; height: number }
|
|
24
|
+
}
|
|
25
|
+
type WorkletCallbackStats = {
|
|
26
|
+
tenantReceived: number
|
|
27
|
+
shellSent: number
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type ShellGestureSeamDebug = {
|
|
31
|
+
attached?: Array<{
|
|
32
|
+
viewTag?: unknown
|
|
33
|
+
nodeFound?: unknown
|
|
34
|
+
handlers?: Array<{ name?: unknown }>
|
|
35
|
+
}>
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function waitForSootsimGestureHandler(
|
|
39
|
+
page: Page,
|
|
40
|
+
viewTag: number,
|
|
41
|
+
handlerName: string,
|
|
42
|
+
): Promise<void> {
|
|
43
|
+
await pollUntil(
|
|
44
|
+
() =>
|
|
45
|
+
page.evaluate(
|
|
46
|
+
async ({ viewTag, handlerName }) => {
|
|
47
|
+
const host = (
|
|
48
|
+
window as typeof window & {
|
|
49
|
+
SootSim?: {
|
|
50
|
+
bridges?: {
|
|
51
|
+
shellHost?: {
|
|
52
|
+
callTestBridge?: (
|
|
53
|
+
method: string,
|
|
54
|
+
...args: unknown[]
|
|
55
|
+
) => Promise<ShellGestureSeamDebug>
|
|
56
|
+
} | null
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
).SootSim?.bridges?.shellHost
|
|
61
|
+
if (typeof host?.callTestBridge !== 'function') return false
|
|
62
|
+
let debug: ShellGestureSeamDebug | null = null
|
|
63
|
+
try {
|
|
64
|
+
debug = await host.callTestBridge('getShellGestureSeamDebug')
|
|
65
|
+
} catch {
|
|
66
|
+
return false
|
|
67
|
+
}
|
|
68
|
+
return (
|
|
69
|
+
debug?.attached?.some(
|
|
70
|
+
(entry) =>
|
|
71
|
+
entry.viewTag === viewTag &&
|
|
72
|
+
entry.nodeFound !== false &&
|
|
73
|
+
entry.handlers?.some((handler) => handler.name === handlerName),
|
|
74
|
+
) === true
|
|
75
|
+
)
|
|
76
|
+
},
|
|
77
|
+
{ viewTag, handlerName },
|
|
78
|
+
),
|
|
79
|
+
Boolean,
|
|
80
|
+
{
|
|
81
|
+
timeoutMs: 5000,
|
|
82
|
+
intervalMs: 100,
|
|
83
|
+
label: `${handlerName} gesture handler on view ${viewTag}`,
|
|
84
|
+
},
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function readWorkletCallbackStats(
|
|
89
|
+
page: Page,
|
|
90
|
+
): Promise<WorkletCallbackStats | null> {
|
|
91
|
+
return page.evaluate(async () => {
|
|
92
|
+
type Stats = {
|
|
93
|
+
runJSCallbackSent?: unknown
|
|
94
|
+
runJSCallbackReceived?: unknown
|
|
95
|
+
} | null
|
|
96
|
+
const g = window as typeof window & {
|
|
97
|
+
__sootsimTest?: { getWorkletSlotStats?: () => Promise<Stats> | Stats }
|
|
98
|
+
SootSim?: {
|
|
99
|
+
bridges?: {
|
|
100
|
+
shellHost?: {
|
|
101
|
+
callTestBridge?: (method: string, ...args: unknown[]) => Promise<Stats>
|
|
102
|
+
} | null
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const tenant =
|
|
107
|
+
typeof g.__sootsimTest?.getWorkletSlotStats === 'function'
|
|
108
|
+
? await g.__sootsimTest.getWorkletSlotStats()
|
|
109
|
+
: null
|
|
110
|
+
const shellHost = g.SootSim?.bridges?.shellHost
|
|
111
|
+
let shell: Stats = null
|
|
112
|
+
if (typeof shellHost?.callTestBridge === 'function') {
|
|
113
|
+
try {
|
|
114
|
+
shell = await shellHost.callTestBridge('getWorkletSlotStats')
|
|
115
|
+
} catch {}
|
|
116
|
+
}
|
|
117
|
+
const tenantReceived =
|
|
118
|
+
typeof tenant?.runJSCallbackReceived === 'number' ? tenant.runJSCallbackReceived : 0
|
|
119
|
+
const shellSent =
|
|
120
|
+
typeof shell?.runJSCallbackSent === 'number' ? shell.runJSCallbackSent : 0
|
|
121
|
+
return { tenantReceived, shellSent }
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function waitForWorkletCallbackDrain(
|
|
126
|
+
page: Page,
|
|
127
|
+
before: WorkletCallbackStats | null,
|
|
128
|
+
timeoutMs: number,
|
|
129
|
+
): Promise<void> {
|
|
130
|
+
if (!before) {
|
|
131
|
+
await page.waitForTimeout(250)
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
let lastShellSent = before.shellSent
|
|
135
|
+
let stableSince = Date.now()
|
|
136
|
+
let sawCallback = false
|
|
137
|
+
|
|
138
|
+
await pollUntil(
|
|
139
|
+
() => readWorkletCallbackStats(page),
|
|
140
|
+
(current) => {
|
|
141
|
+
if (!current) return false
|
|
142
|
+
if (current.shellSent !== lastShellSent) {
|
|
143
|
+
lastShellSent = current.shellSent
|
|
144
|
+
stableSince = Date.now()
|
|
145
|
+
}
|
|
146
|
+
const sentDelta = current.shellSent - before.shellSent
|
|
147
|
+
if (sentDelta > 0) sawCallback = true
|
|
148
|
+
const receivedDelta = current.tenantReceived - before.tenantReceived
|
|
149
|
+
return sawCallback && receivedDelta >= sentDelta && Date.now() - stableSince >= 100
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
timeoutMs,
|
|
153
|
+
intervalMs: 20,
|
|
154
|
+
label: 'worklet callback drain',
|
|
155
|
+
},
|
|
156
|
+
).catch(() => {})
|
|
157
|
+
|
|
158
|
+
if (!sawCallback) {
|
|
159
|
+
// Some gestures are purely native and do not schedule RN callbacks. A
|
|
160
|
+
// bounded fallback still preserves Detox's "gesture has settled" contract.
|
|
161
|
+
await page.waitForTimeout(250)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// helper script injected via page.addInitScript so every page.evaluate
|
|
166
|
+
// can reach it as `__sootsimFindInteractiveCanvas()`. picks the canvas
|
|
167
|
+
// that actually receives pointer input. sootsim renders multiple
|
|
168
|
+
// <canvas> layers per surface (background/render targets, hidden
|
|
169
|
+
// offscreen, overlay) and `document.querySelector('canvas')` returns
|
|
170
|
+
// the first one in DOM order — usually a non-interactive layer with
|
|
171
|
+
// `pointer-events: none`. dispatching synthetic PointerEvents onto
|
|
172
|
+
// that layer silently no-ops because the gesture system only listens
|
|
173
|
+
// on the interactive layer (pointer-events: auto, non-zero rect).
|
|
174
|
+
const sootsimCanvasFinderInstalled = new WeakSet<Page>()
|
|
175
|
+
function installInteractiveCanvasFinder() {
|
|
176
|
+
window.__sootsimFindInteractiveCanvas = (): HTMLCanvasElement | null => {
|
|
177
|
+
const interactive: Array<{ c: HTMLCanvasElement; z: number; app: boolean }> = []
|
|
178
|
+
const appVisible: Array<{ c: HTMLCanvasElement; z: number; app: boolean }> = []
|
|
179
|
+
for (const c of Array.from(document.querySelectorAll('canvas'))) {
|
|
180
|
+
const s = window.getComputedStyle(c)
|
|
181
|
+
if (s.display === 'none' || s.visibility === 'hidden') continue
|
|
182
|
+
const r = c.getBoundingClientRect()
|
|
183
|
+
if (r.width === 0 || r.height === 0) continue
|
|
184
|
+
const entry = {
|
|
185
|
+
c,
|
|
186
|
+
z: parseInt(s.zIndex, 10) || 0,
|
|
187
|
+
app: c.dataset.surfaceId?.startsWith('app:') === true,
|
|
188
|
+
}
|
|
189
|
+
if (entry.app) appVisible.push(entry)
|
|
190
|
+
if (s.pointerEvents !== 'none') interactive.push(entry)
|
|
191
|
+
}
|
|
192
|
+
const list = appVisible.length > 0 ? appVisible : interactive
|
|
193
|
+
if (list.length === 0) return null
|
|
194
|
+
list.sort((a, b) => Number(b.app) - Number(a.app) || b.z - a.z)
|
|
195
|
+
return list[0].c
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function ensureCanvasFinder(page: Page) {
|
|
200
|
+
if (!sootsimCanvasFinderInstalled.has(page)) {
|
|
201
|
+
sootsimCanvasFinderInstalled.add(page)
|
|
202
|
+
await page.addInitScript(installInteractiveCanvasFinder)
|
|
203
|
+
}
|
|
204
|
+
await page.evaluate(installInteractiveCanvasFinder)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function readSootsimPageMetrics(
|
|
208
|
+
page: Page,
|
|
209
|
+
timeoutMs: number,
|
|
210
|
+
): Promise<SootsimPageMetrics> {
|
|
211
|
+
await ensureCanvasFinder(page)
|
|
212
|
+
try {
|
|
213
|
+
const metrics = await pollUntil(
|
|
214
|
+
() =>
|
|
215
|
+
page.evaluate(async () => {
|
|
216
|
+
const canvas =
|
|
217
|
+
window.__sootsimFindInteractiveCanvas?.() ?? document.querySelector('canvas')
|
|
218
|
+
if (!canvas) return null
|
|
219
|
+
const rect = canvas.getBoundingClientRect()
|
|
220
|
+
if (rect.width <= 0 || rect.height <= 0) return null
|
|
221
|
+
const surface =
|
|
222
|
+
(await window.__sootsimTest?.getSurfaceMetricsSnapshot?.()) as SurfaceMetricsResult
|
|
223
|
+
const screenWidth = surface?.window?.width
|
|
224
|
+
const screenHeight = surface?.window?.height
|
|
225
|
+
if (
|
|
226
|
+
typeof screenWidth !== 'number' ||
|
|
227
|
+
typeof screenHeight !== 'number' ||
|
|
228
|
+
screenWidth <= 0 ||
|
|
229
|
+
screenHeight <= 0
|
|
230
|
+
) {
|
|
231
|
+
return null
|
|
232
|
+
}
|
|
233
|
+
const homeIndicatorHeight = Number(surface?.device?.homeIndicatorHeight) || 0
|
|
234
|
+
const safeAreaBottom = Number(surface?.safeAreaInsets?.bottom) || 0
|
|
235
|
+
const bottomInset = Math.max(0, homeIndicatorHeight, safeAreaBottom)
|
|
236
|
+
const visibleRectBottom =
|
|
237
|
+
Number(surface?.visibleRect?.y) + Number(surface?.visibleRect?.height)
|
|
238
|
+
const interactiveHeight =
|
|
239
|
+
Number.isFinite(visibleRectBottom) && visibleRectBottom > 0
|
|
240
|
+
? Math.min(screenHeight, visibleRectBottom)
|
|
241
|
+
: Math.max(0, screenHeight - bottomInset)
|
|
242
|
+
return {
|
|
243
|
+
canvas: {
|
|
244
|
+
left: rect.left,
|
|
245
|
+
top: rect.top,
|
|
246
|
+
width: rect.width,
|
|
247
|
+
height: rect.height,
|
|
248
|
+
},
|
|
249
|
+
window: {
|
|
250
|
+
width: screenWidth,
|
|
251
|
+
height: screenHeight,
|
|
252
|
+
},
|
|
253
|
+
interactiveWindow: {
|
|
254
|
+
width: screenWidth,
|
|
255
|
+
height: interactiveHeight,
|
|
256
|
+
},
|
|
257
|
+
}
|
|
258
|
+
}),
|
|
259
|
+
(metrics) => metrics !== null,
|
|
260
|
+
{
|
|
261
|
+
timeoutMs,
|
|
262
|
+
intervalMs: 50,
|
|
263
|
+
label: 'sootsim surface metrics',
|
|
264
|
+
},
|
|
265
|
+
)
|
|
266
|
+
if (metrics === null) throw new Error('surface metrics poll settled without metrics')
|
|
267
|
+
return metrics
|
|
268
|
+
} catch (error) {
|
|
269
|
+
throw new Error('sootsim surface metrics are not available', { cause: error })
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export async function readSootsimViewport(
|
|
274
|
+
page: Page,
|
|
275
|
+
timeoutMs = 5000,
|
|
276
|
+
): Promise<{ width: number; height: number }> {
|
|
277
|
+
return (await readSootsimPageMetrics(page, timeoutMs)).window
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export async function readSootsimInteractiveViewport(
|
|
281
|
+
page: Page,
|
|
282
|
+
timeoutMs = 5000,
|
|
283
|
+
): Promise<{ width: number; height: number }> {
|
|
284
|
+
return (await readSootsimPageMetrics(page, timeoutMs)).interactiveWindow
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export async function sootsimToPage(
|
|
288
|
+
page: Page,
|
|
289
|
+
sootsimX: number,
|
|
290
|
+
sootsimY: number,
|
|
291
|
+
timeoutMs = 5000,
|
|
292
|
+
): Promise<{ x: number; y: number }> {
|
|
293
|
+
const metrics = await readSootsimPageMetrics(page, timeoutMs)
|
|
294
|
+
const scaleX = metrics.canvas.width / metrics.window.width
|
|
295
|
+
const scaleY = metrics.canvas.height / metrics.window.height
|
|
296
|
+
return {
|
|
297
|
+
x: metrics.canvas.left + sootsimX * scaleX,
|
|
298
|
+
y: metrics.canvas.top + sootsimY * scaleY,
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export async function dragScrollNode(
|
|
303
|
+
page: Page,
|
|
304
|
+
node: {
|
|
305
|
+
absolutePosition: { x: number; y: number }
|
|
306
|
+
layout: { width: number; height: number }
|
|
307
|
+
},
|
|
308
|
+
pixels: number,
|
|
309
|
+
direction: 'up' | 'down' | 'left' | 'right',
|
|
310
|
+
): Promise<void> {
|
|
311
|
+
const startSootSimX = node.absolutePosition.x + node.layout.width / 2
|
|
312
|
+
const startSootSimY = node.absolutePosition.y + node.layout.height / 2
|
|
313
|
+
let endSootSimX = startSootSimX
|
|
314
|
+
let endSootSimY = startSootSimY
|
|
315
|
+
|
|
316
|
+
switch (direction) {
|
|
317
|
+
case 'down':
|
|
318
|
+
endSootSimY -= pixels
|
|
319
|
+
break
|
|
320
|
+
case 'up':
|
|
321
|
+
endSootSimY += pixels
|
|
322
|
+
break
|
|
323
|
+
case 'left':
|
|
324
|
+
endSootSimX += pixels
|
|
325
|
+
break
|
|
326
|
+
case 'right':
|
|
327
|
+
endSootSimX -= pixels
|
|
328
|
+
break
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const startPage = await sootsimToPage(page, startSootSimX, startSootSimY)
|
|
332
|
+
const endPage = await sootsimToPage(page, endSootSimX, endSootSimY)
|
|
333
|
+
const steps = Math.max(16, Math.ceil(pixels / 8))
|
|
334
|
+
|
|
335
|
+
await page.mouse.move(startPage.x, startPage.y)
|
|
336
|
+
await page.mouse.down()
|
|
337
|
+
await page.waitForTimeout(20)
|
|
338
|
+
|
|
339
|
+
const dx = (endPage.x - startPage.x) / steps
|
|
340
|
+
const dy = (endPage.y - startPage.y) / steps
|
|
341
|
+
for (let i = 1; i <= steps; i++) {
|
|
342
|
+
await page.mouse.move(startPage.x + dx * i, startPage.y + dy * i)
|
|
343
|
+
await page.waitForTimeout(24)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
await page.mouse.up()
|
|
347
|
+
await page.waitForTimeout(180)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Fire synthetic two-finger pointer events directly on the sootsim canvas. We
|
|
351
|
+
// use PointerEvents (not page.mouse) because page.mouse is single-cursor;
|
|
352
|
+
// sootsim's touch tracker keys on pointerId, so we just need to dispatch
|
|
353
|
+
// events with two distinct ids and pointerType 'touch'. Used for pinch /
|
|
354
|
+
// rotation drivers — RNGH's PinchGesture/RotationGesture both require ≥2
|
|
355
|
+
// concurrent active touches on iOS UIPinchGestureRecognizer parity.
|
|
356
|
+
export async function dispatchTwoFingerGesture(
|
|
357
|
+
page: Page,
|
|
358
|
+
startA: { x: number; y: number },
|
|
359
|
+
startB: { x: number; y: number },
|
|
360
|
+
endA: { x: number; y: number },
|
|
361
|
+
endB: { x: number; y: number },
|
|
362
|
+
options: {
|
|
363
|
+
steps?: number
|
|
364
|
+
stepDelayMs?: number
|
|
365
|
+
workletStatsTimeoutMs?: number
|
|
366
|
+
} = {},
|
|
367
|
+
): Promise<void> {
|
|
368
|
+
const steps = options.steps ?? 10
|
|
369
|
+
const stepDelay = options.stepDelayMs ?? 18
|
|
370
|
+
const workletStatsTimeoutMs = options.workletStatsTimeoutMs ?? 1500
|
|
371
|
+
await ensureCanvasFinder(page)
|
|
372
|
+
const beforeWorkletStats = await pollUntil(
|
|
373
|
+
() => readWorkletCallbackStats(page),
|
|
374
|
+
() => true,
|
|
375
|
+
{
|
|
376
|
+
timeoutMs: workletStatsTimeoutMs,
|
|
377
|
+
intervalMs: 0,
|
|
378
|
+
label: 'pre-dispatch worklet callback stats',
|
|
379
|
+
},
|
|
380
|
+
).catch(() => null)
|
|
381
|
+
|
|
382
|
+
await page.evaluate(
|
|
383
|
+
({ startA, startB }) => {
|
|
384
|
+
const canvas: HTMLCanvasElement | null =
|
|
385
|
+
window.__sootsimFindInteractiveCanvas?.() ?? document.querySelector('canvas')
|
|
386
|
+
if (!canvas) throw new Error('sootsim interactive canvas not found')
|
|
387
|
+
const fire = (point: { x: number; y: number }, id: number, primary: boolean) => {
|
|
388
|
+
canvas.dispatchEvent(
|
|
389
|
+
new PointerEvent('pointerdown', {
|
|
390
|
+
bubbles: true,
|
|
391
|
+
cancelable: true,
|
|
392
|
+
clientX: point.x,
|
|
393
|
+
clientY: point.y,
|
|
394
|
+
isPrimary: primary,
|
|
395
|
+
pointerId: id,
|
|
396
|
+
pointerType: 'touch',
|
|
397
|
+
}),
|
|
398
|
+
)
|
|
399
|
+
}
|
|
400
|
+
fire(startA, 1001, true)
|
|
401
|
+
fire(startB, 1002, false)
|
|
402
|
+
},
|
|
403
|
+
{ startA, startB },
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
for (let i = 1; i <= steps; i++) {
|
|
407
|
+
const t = i / steps
|
|
408
|
+
const ax = startA.x + (endA.x - startA.x) * t
|
|
409
|
+
const ay = startA.y + (endA.y - startA.y) * t
|
|
410
|
+
const bx = startB.x + (endB.x - startB.x) * t
|
|
411
|
+
const by = startB.y + (endB.y - startB.y) * t
|
|
412
|
+
await page.evaluate(
|
|
413
|
+
({ ax, ay, bx, by }) => {
|
|
414
|
+
const canvas: HTMLCanvasElement | null =
|
|
415
|
+
window.__sootsimFindInteractiveCanvas?.() ?? document.querySelector('canvas')
|
|
416
|
+
if (!canvas) return
|
|
417
|
+
canvas.dispatchEvent(
|
|
418
|
+
new PointerEvent('pointermove', {
|
|
419
|
+
bubbles: true,
|
|
420
|
+
cancelable: true,
|
|
421
|
+
clientX: ax,
|
|
422
|
+
clientY: ay,
|
|
423
|
+
isPrimary: true,
|
|
424
|
+
pointerId: 1001,
|
|
425
|
+
pointerType: 'touch',
|
|
426
|
+
}),
|
|
427
|
+
)
|
|
428
|
+
canvas.dispatchEvent(
|
|
429
|
+
new PointerEvent('pointermove', {
|
|
430
|
+
bubbles: true,
|
|
431
|
+
cancelable: true,
|
|
432
|
+
clientX: bx,
|
|
433
|
+
clientY: by,
|
|
434
|
+
isPrimary: false,
|
|
435
|
+
pointerId: 1002,
|
|
436
|
+
pointerType: 'touch',
|
|
437
|
+
}),
|
|
438
|
+
)
|
|
439
|
+
},
|
|
440
|
+
{ ax, ay, bx, by },
|
|
441
|
+
)
|
|
442
|
+
await page.waitForTimeout(stepDelay)
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
await page.evaluate(
|
|
446
|
+
({ endA, endB }) => {
|
|
447
|
+
const canvas: HTMLCanvasElement | null =
|
|
448
|
+
window.__sootsimFindInteractiveCanvas?.() ?? document.querySelector('canvas')
|
|
449
|
+
if (!canvas) return
|
|
450
|
+
canvas.dispatchEvent(
|
|
451
|
+
new PointerEvent('pointerup', {
|
|
452
|
+
bubbles: true,
|
|
453
|
+
cancelable: true,
|
|
454
|
+
clientX: endA.x,
|
|
455
|
+
clientY: endA.y,
|
|
456
|
+
isPrimary: true,
|
|
457
|
+
pointerId: 1001,
|
|
458
|
+
pointerType: 'touch',
|
|
459
|
+
}),
|
|
460
|
+
)
|
|
461
|
+
canvas.dispatchEvent(
|
|
462
|
+
new PointerEvent('pointerup', {
|
|
463
|
+
bubbles: true,
|
|
464
|
+
cancelable: true,
|
|
465
|
+
clientX: endB.x,
|
|
466
|
+
clientY: endB.y,
|
|
467
|
+
isPrimary: false,
|
|
468
|
+
pointerId: 1002,
|
|
469
|
+
pointerType: 'touch',
|
|
470
|
+
}),
|
|
471
|
+
)
|
|
472
|
+
},
|
|
473
|
+
{ endA, endB },
|
|
474
|
+
)
|
|
475
|
+
await waitForWorkletCallbackDrain(page, beforeWorkletStats, workletStatsTimeoutMs)
|
|
476
|
+
// runOnJS delivery increments before the callback's React setState commits.
|
|
477
|
+
// Detox gestures settle after native UI has observed the gesture result, so
|
|
478
|
+
// leave one bounded commit window after the worklet bridge drains.
|
|
479
|
+
await page.waitForTimeout(250)
|
|
480
|
+
}
|