sootsim 0.1.83 → 0.1.84
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 +0 -1
- package/detox/colors.ts +54 -0
- package/detox/config-loader.ts +135 -0
- package/detox/expectations.ts +477 -0
- package/detox/gestures.ts +442 -0
- package/detox/index.ts +1436 -0
- package/detox/jest-environment.ts +86 -0
- package/detox/jest-preset.cjs +50 -0
- package/detox/matchers.ts +29 -0
- package/detox/navigation.ts +43 -0
- package/detox/run-test.ts +113 -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 +3 -3
- package/dist-cli/chunks/{agent-MQ7GLVIB.js → agent-2CWD6W6P.js} +2 -2
- package/dist-cli/chunks/{agent-wrapper-7KAFDQCN.js → agent-wrapper-5W3LOX6S.js} +2 -2
- package/dist-cli/chunks/{assert-TV46GUNU.js → assert-ZOMAMKRT.js} +2 -2
- package/dist-cli/chunks/auto-bootstrap-NYYSMTIM.js +2 -0
- package/dist-cli/chunks/beta-4K2SQACK.js +2 -0
- package/dist-cli/chunks/chunk-3HXQ7MJK.js +79 -0
- package/dist-cli/chunks/{chunk-4LS5MZAI.js → chunk-4K7BH2D4.js} +3 -3
- package/dist-cli/chunks/{chunk-FJYT7XL2.js → chunk-4OPRODFA.js} +2 -2
- package/dist-cli/chunks/{chunk-DP7O5MHK.js → chunk-4OWVPRZV.js} +2 -2
- package/dist-cli/chunks/{chunk-PM5NVKLP.js → chunk-5XCXOLG2.js} +2 -2
- package/dist-cli/chunks/chunk-67ZZ2CM5.js +1 -0
- package/dist-cli/chunks/{chunk-WN7M3QON.js → chunk-73UZXB4B.js} +2 -2
- package/dist-cli/chunks/{chunk-5DJXZIFZ.js → chunk-7NWNTUJF.js} +1 -1
- package/dist-cli/chunks/{chunk-Y2VJBRSP.js → chunk-7YHDJLO2.js} +1 -1
- package/dist-cli/chunks/{chunk-6NN2D4EJ.js → chunk-AJVTY6KY.js} +1 -1
- package/dist-cli/chunks/chunk-AWSQUOAS.js +67 -0
- package/dist-cli/chunks/{chunk-CJY3AVI7.js → chunk-BCBNVJVG.js} +1 -1
- package/dist-cli/chunks/{chunk-OYMFNU3M.js → chunk-BKBL6K2G.js} +1 -1
- package/dist-cli/chunks/{chunk-IBNRRAES.js → chunk-C3DPQZ4J.js} +2 -2
- package/dist-cli/chunks/chunk-D3ZSBIIY.js +2 -0
- package/dist-cli/chunks/{chunk-2AWQ7OB2.js → chunk-D4HUVLZR.js} +1 -1
- package/dist-cli/chunks/{chunk-F3HP444U.js → chunk-DUUSJDES.js} +1 -1
- package/dist-cli/chunks/{chunk-277XAALA.js → chunk-ELJLF4SG.js} +3 -3
- package/dist-cli/chunks/{chunk-RH4F2TF7.js → chunk-EQ7TFQ2F.js} +1 -1
- package/dist-cli/chunks/{chunk-HNWEELAE.js → chunk-EQCKGC4B.js} +1 -1
- package/dist-cli/chunks/chunk-FUCGLWNN.js +1 -0
- package/dist-cli/chunks/{chunk-FRM355UL.js → chunk-HYPJW65U.js} +2 -2
- package/dist-cli/chunks/chunk-IILJQCZA.js +2 -0
- package/dist-cli/chunks/{chunk-Y4BUVURT.js → chunk-KU6MSPAH.js} +2 -2
- package/dist-cli/chunks/{chunk-DM6WT7QM.js → chunk-OOOR7NT2.js} +1 -1
- package/dist-cli/chunks/{chunk-HAWOAQAG.js → chunk-P7WDNKOS.js} +3 -3
- package/dist-cli/chunks/{chunk-6TNANCQC.js → chunk-PPKKA5VW.js} +2 -2
- package/dist-cli/chunks/{chunk-JQ7ZXOXJ.js → chunk-PS2G44GT.js} +2 -2
- package/dist-cli/chunks/{chunk-ECJBV65H.js → chunk-QMSJR5R2.js} +2 -2
- package/dist-cli/chunks/{chunk-J2GYISVJ.js → chunk-RF4R2U46.js} +2 -2
- package/dist-cli/chunks/{chunk-VMXWC2JO.js → chunk-RIXUH3NK.js} +2 -2
- package/dist-cli/chunks/{chunk-2PY3UZVO.js → chunk-SFGUPL2X.js} +2 -2
- package/dist-cli/chunks/{chunk-572VSFNP.js → chunk-SQX5CAYG.js} +1 -1
- package/dist-cli/chunks/{chunk-NXATOWWF.js → chunk-SQZAC7C4.js} +1 -1
- package/dist-cli/chunks/{chunk-WTKTOL3C.js → chunk-SV7FOGJ3.js} +2 -2
- package/dist-cli/chunks/{chunk-JHJNODXN.js → chunk-TK3OJSEO.js} +2 -2
- package/dist-cli/chunks/{chunk-KASUZ5XV.js → chunk-TL7SIZ7S.js} +1 -1
- package/dist-cli/chunks/{chunk-6XZOEBTZ.js → chunk-V2GQ4WXJ.js} +2 -2
- package/dist-cli/chunks/{chunk-IP3QJLRH.js → chunk-VH7F45CN.js} +1 -1
- package/dist-cli/chunks/chunk-WNVNU2OW.js +4 -0
- package/dist-cli/chunks/{chunk-YUELRHGB.js → chunk-XQ2OBHBE.js} +2 -2
- package/dist-cli/chunks/{chunk-CYV6Y6YV.js → chunk-YCIA4BHJ.js} +2 -2
- package/dist-cli/chunks/chunk-ZSMMJMPA.js +1 -0
- package/dist-cli/chunks/cli-version-QB4VH24H.js +2 -0
- package/dist-cli/chunks/{compat-QLLWBTS3.js → compat-FWSEEGEH.js} +3 -3
- package/dist-cli/chunks/{config-2DSLDCXV.js → config-CYI2WAGP.js} +2 -2
- package/dist-cli/chunks/control-UXY7YQVX.js +2 -0
- package/dist-cli/chunks/{cpu-profile-GEIKHCPC.js → cpu-profile-IKAE3KTY.js} +2 -2
- package/dist-cli/chunks/{daemon-4EBUFN4D.js → daemon-ZUMF53YB.js} +2 -2
- package/dist-cli/chunks/{debug-WGD6XWOF.js → debug-P6KULKKS.js} +3 -3
- package/dist-cli/chunks/{detox-LNKGRZU6.js → detox-SPWAZCYG.js} +2 -2
- package/dist-cli/chunks/{device-AYKXKVIQ.js → device-JWEPK6I2.js} +2 -2
- package/dist-cli/chunks/{diagnose-TMXSDOOC.js → diagnose-IZODTXV2.js} +2 -2
- package/dist-cli/chunks/drivers-MK6WJKBC.js +2 -0
- package/dist-cli/chunks/{electron-QFPF7TBY.js → electron-R5GP6RVB.js} +3 -3
- package/dist-cli/chunks/flow-6O4GEOPJ.js +2 -0
- package/dist-cli/chunks/{hints-MXKRR4TG.js → hints-DYDNYX7N.js} +2 -2
- package/dist-cli/chunks/{home-paths-REMWQDAO.js → home-paths-GLMX5OKL.js} +2 -2
- package/dist-cli/chunks/{inspect-XGSQNFV7.js → inspect-FJOPCTY2.js} +3 -3
- package/dist-cli/chunks/install-A3TUGGHN.js +2 -0
- package/dist-cli/chunks/{install-desktop-NQG3RZSA.js → install-desktop-YPJZMZM5.js} +3 -3
- package/dist-cli/chunks/{keys-5QZWXL3F.js → keys-GSYPHWNY.js} +2 -2
- package/dist-cli/chunks/{launch-SBXOZWKO.js → launch-4G2PKW5X.js} +3 -3
- package/dist-cli/chunks/{login-EACQXE24.js → login-KJQGHA64.js} +4 -4
- package/dist-cli/chunks/{logout-IBQLMUML.js → logout-XM2SYH5C.js} +2 -2
- package/dist-cli/chunks/{maestro-LFYXUX7O.js → maestro-EOWGI7DG.js} +2 -2
- package/dist-cli/chunks/{preview-U4SBOEGQ.js → preview-F73TKK37.js} +2 -2
- package/dist-cli/chunks/{profile-GWS5ECMY.js → profile-22FDKBUO.js} +2 -2
- package/dist-cli/chunks/{react-QDHLMVYL.js → react-5L6VPFUP.js} +2 -2
- package/dist-cli/chunks/{record-BUEUWPDI.js → record-JZXCQ4IN.js} +2 -2
- package/dist-cli/chunks/runtime-EEBX7CFV.js +2 -0
- package/dist-cli/chunks/{runtime-delivery-G7L6RVZ7.js → runtime-delivery-LXUM3R4A.js} +2 -2
- package/dist-cli/chunks/{screenshot-T2HBA3VI.js → screenshot-HDRRG33Q.js} +2 -2
- package/dist-cli/chunks/{screenshot-mode-EG5HMIH3.js → screenshot-mode-WY63LZIX.js} +2 -2
- package/dist-cli/chunks/{screenshots-S52AFHTV.js → screenshots-MPV2ENL5.js} +2 -2
- package/dist-cli/chunks/{server-MFFVYUGG.js → server-5LBMCJ3G.js} +2 -2
- package/dist-cli/chunks/setup-repo-SZSYNKNI.js +2 -0
- package/dist-cli/chunks/{skills-HQGWBS2O.js → skills-BQ73YOBF.js} +2 -2
- package/dist-cli/chunks/{start-E3DRYY7W.js → start-2WU4W6ZU.js} +4 -4
- package/dist-cli/chunks/store-RE45SUBF.js +2 -0
- package/dist-cli/chunks/telemetry-DG6GJLCP.js +2 -0
- package/dist-cli/chunks/{test-ZY3EF62K.js → test-OVO4CQTG.js} +3 -3
- package/dist-cli/chunks/{three-mode-WSPKQCJ5.js → three-mode-BKM3KFM7.js} +2 -2
- package/dist-cli/chunks/{timeline-3XAB5EWZ.js → timeline-MDXGEDQL.js} +2 -2
- package/dist-cli/chunks/{upgrade-WNENPFM5.js → upgrade-JGQABWVF.js} +2 -2
- package/dist-cli/chunks/upload-UJNUA4ZV.js +2 -0
- package/dist-cli/chunks/{web-D2AOZY44.js → web-WYFAYQ72.js} +2 -2
- package/dist-cli/chunks/{what-happened-F43KNSG6.js → what-happened-PZW2KW6A.js} +2 -2
- package/dist-cli/chunks/{whoami-T22VBR7C.js → whoami-7ATWJQS6.js} +2 -2
- package/dist-lib/agent-daemon-client.cjs +1 -1
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/agent-sessions.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 +1 -1
- package/dist-lib/beta.cjs +44 -0
- package/dist-lib/bridge-constants.cjs +1 -1
- package/dist-lib/cli-constants.cjs +1 -1
- package/dist-lib/config.cjs +1 -1
- package/dist-lib/detox/index.cjs +1770 -0
- package/dist-lib/detox/jest-preset.cjs +50 -0
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +1 -1
- package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
- package/dist-lib/index.cjs +1 -1
- package/dist-lib/metro.cjs +1 -1
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/render-mode.cjs +1 -1
- package/dist-lib/scripts/demo-app-registry.cjs +809 -0
- package/dist-lib/scripts/dev-server-scanner.cjs +1269 -0
- package/dist-lib/skills.cjs +8322 -0
- package/dist-lib/vite-base.cjs +3 -3
- package/dist-lib/vite.cjs +1 -1
- package/package.json +39 -10
- package/scripts/demo-app-registry.ts +989 -0
- package/scripts/dev-server-scanner.ts +674 -0
- package/src/agent-daemon-client.ts +390 -0
- package/src/agent-events.ts +71 -0
- package/src/agent-prompt.ts +71 -0
- package/src/agent-sessions.ts +572 -0
- package/src/attached-projects.ts +536 -0
- package/src/auth/shared-session.ts +199 -0
- package/src/backend-origin.ts +49 -0
- package/src/beta.ts +21 -0
- package/src/bridge-constants.ts +10 -0
- package/src/cli-constants.ts +1 -0
- package/src/cli-version.ts +30 -0
- package/src/codex-client.ts +215 -0
- package/src/config.ts +110 -0
- package/src/dev-bundle-resolution.ts +180 -0
- package/src/home-paths.ts +382 -0
- package/src/host/agent-host.ts +576 -0
- package/src/host/bridge-host.ts +2293 -0
- package/src/host/fetch-proxy-handler.ts +288 -0
- package/src/host/fetch-proxy-overrides.ts +39 -0
- package/src/host/open-url.ts +234 -0
- package/src/index.ts +9 -0
- package/src/metro-plugin.ts +207 -0
- package/src/native-dev-bundle-url.ts +62 -0
- package/src/native-seam-manifest.ts +313 -0
- package/src/profiles.ts +179 -0
- package/src/render-mode.ts +27 -0
- package/src/runtime-delivery.ts +334 -0
- package/src/screenshots/compose.ts +422 -0
- package/src/screenshots/frame-compose.ts +438 -0
- package/src/screenshots/orchestrate.ts +244 -0
- package/src/screenshots/registry.ts +58 -0
- package/src/screenshots/schema.ts +364 -0
- package/src/skills/builtin/a11y-review.ts +126 -0
- package/src/skills/builtin/compat-check.ts +104 -0
- package/src/skills/builtin/perf-profile.ts +84 -0
- package/src/skills/builtin/screenshot-all.ts +46 -0
- package/src/skills/builtin/test-flow.ts +118 -0
- package/src/skills/builtin/visual-diff.ts +94 -0
- package/src/skills/registry.ts +107 -0
- package/src/skills/types.ts +41 -0
- package/src/vite-plugin-one.ts +187 -0
- package/src/vite-plugin.ts +1381 -0
- package/src/worklets-babel.ts +132 -0
- package/dist-cli/chunks/auto-bootstrap-FQS4ZD2K.js +0 -2
- package/dist-cli/chunks/beta-VG7CDY2U.js +0 -2
- package/dist-cli/chunks/chunk-2OIBDYHW.js +0 -1
- package/dist-cli/chunks/chunk-6BNLVMXA.js +0 -1
- package/dist-cli/chunks/chunk-6XD6CBJM.js +0 -2
- package/dist-cli/chunks/chunk-CHQTO426.js +0 -1
- package/dist-cli/chunks/chunk-FAPYGVIU.js +0 -4
- package/dist-cli/chunks/chunk-PEHFE3LG.js +0 -64
- package/dist-cli/chunks/chunk-RXH2SLKF.js +0 -2
- package/dist-cli/chunks/chunk-UXQWC5ZR.js +0 -79
- package/dist-cli/chunks/chunk-XFQL74PF.js +0 -5
- package/dist-cli/chunks/cli-version-PWF6I6LY.js +0 -2
- package/dist-cli/chunks/control-UIOXGYXU.js +0 -2
- package/dist-cli/chunks/demo-app-registry-G3BDOFWC.js +0 -2
- package/dist-cli/chunks/drivers-IDQF34HP.js +0 -2
- package/dist-cli/chunks/flow-3JN3Y7RF.js +0 -2
- package/dist-cli/chunks/install-2N3YOOSN.js +0 -2
- package/dist-cli/chunks/runtime-PVB4VGUH.js +0 -2
- package/dist-cli/chunks/setup-repo-YOF7NV5D.js +0 -2
- package/dist-cli/chunks/store-MAI6D3UO.js +0 -2
- package/dist-cli/chunks/telemetry-RCQKCJTH.js +0 -2
- package/dist-cli/chunks/upload-YLJ4RA73.js +0 -2
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
// pull in @soot/sootsim-globals so window.__sootsimTest and
|
|
2
|
+
// window.__sootsimFindInteractiveCanvas are typed inside page.evaluate
|
|
3
|
+
// bodies. all sootsim-namespaced globals are declared canonically there —
|
|
4
|
+
// add new entries to packages/sootsim-globals/src/index.ts, not here.
|
|
5
|
+
import type {} from '@soot/sootsim-globals'
|
|
6
|
+
import type { Page } from 'playwright'
|
|
7
|
+
|
|
8
|
+
// shape of the surface-metrics bridge response we care about. the bridge
|
|
9
|
+
// types it as `Record<string, unknown> | null` (the contract is open) so we
|
|
10
|
+
// narrow at the call sites by casting to this local view.
|
|
11
|
+
type SurfaceMetricsView = {
|
|
12
|
+
window?: { width?: number; height?: number; scale?: number }
|
|
13
|
+
device?: { homeIndicatorHeight?: number }
|
|
14
|
+
safeAreaInsets?: { bottom?: number }
|
|
15
|
+
visibleRect?: { y?: number; height?: number }
|
|
16
|
+
}
|
|
17
|
+
type SurfaceMetricsResult = SurfaceMetricsView | null | undefined
|
|
18
|
+
|
|
19
|
+
type SootsimPageMetrics = {
|
|
20
|
+
canvas: { left: number; top: number; width: number; height: number }
|
|
21
|
+
window: { width: number; height: number }
|
|
22
|
+
interactiveWindow: { width: number; height: number }
|
|
23
|
+
}
|
|
24
|
+
type WorkletCallbackStats = {
|
|
25
|
+
tenantReceived: number
|
|
26
|
+
shellSent: number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type ShellGestureSeamDebug = {
|
|
30
|
+
attached?: Array<{
|
|
31
|
+
viewTag?: unknown
|
|
32
|
+
nodeFound?: unknown
|
|
33
|
+
handlers?: Array<{ name?: unknown }>
|
|
34
|
+
}>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function waitForSootsimGestureHandler(
|
|
38
|
+
page: Page,
|
|
39
|
+
viewTag: number,
|
|
40
|
+
handlerName: string,
|
|
41
|
+
): Promise<void> {
|
|
42
|
+
await page.waitForFunction(
|
|
43
|
+
async ({ viewTag, handlerName }) => {
|
|
44
|
+
const host = (
|
|
45
|
+
window as typeof window & {
|
|
46
|
+
SootSim?: {
|
|
47
|
+
bridges?: {
|
|
48
|
+
shellHost?: {
|
|
49
|
+
callTestBridge?: (
|
|
50
|
+
method: string,
|
|
51
|
+
...args: unknown[]
|
|
52
|
+
) => Promise<ShellGestureSeamDebug>
|
|
53
|
+
} | null
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
).SootSim?.bridges?.shellHost
|
|
58
|
+
if (typeof host?.callTestBridge !== 'function') return false
|
|
59
|
+
let debug: ShellGestureSeamDebug | null = null
|
|
60
|
+
try {
|
|
61
|
+
debug = await host.callTestBridge('getShellGestureSeamDebug')
|
|
62
|
+
} catch {
|
|
63
|
+
return false
|
|
64
|
+
}
|
|
65
|
+
return (
|
|
66
|
+
debug?.attached?.some(
|
|
67
|
+
(entry) =>
|
|
68
|
+
entry.viewTag === viewTag &&
|
|
69
|
+
entry.nodeFound !== false &&
|
|
70
|
+
entry.handlers?.some((handler) => handler.name === handlerName),
|
|
71
|
+
) === true
|
|
72
|
+
)
|
|
73
|
+
},
|
|
74
|
+
{ viewTag, handlerName },
|
|
75
|
+
{ timeout: 5000 },
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function readWorkletCallbackStats(
|
|
80
|
+
page: Page,
|
|
81
|
+
): Promise<WorkletCallbackStats | null> {
|
|
82
|
+
return page.evaluate(async () => {
|
|
83
|
+
type Stats = {
|
|
84
|
+
runJSCallbackSent?: unknown
|
|
85
|
+
runJSCallbackReceived?: unknown
|
|
86
|
+
} | null
|
|
87
|
+
const g = window as typeof window & {
|
|
88
|
+
__sootsimTest?: { getWorkletSlotStats?: () => Promise<Stats> | Stats }
|
|
89
|
+
SootSim?: {
|
|
90
|
+
bridges?: {
|
|
91
|
+
shellHost?: {
|
|
92
|
+
callTestBridge?: (method: string, ...args: unknown[]) => Promise<Stats>
|
|
93
|
+
} | null
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const tenant =
|
|
98
|
+
typeof g.__sootsimTest?.getWorkletSlotStats === 'function'
|
|
99
|
+
? await g.__sootsimTest.getWorkletSlotStats()
|
|
100
|
+
: null
|
|
101
|
+
const shellHost = g.SootSim?.bridges?.shellHost
|
|
102
|
+
let shell: Stats = null
|
|
103
|
+
if (typeof shellHost?.callTestBridge === 'function') {
|
|
104
|
+
try {
|
|
105
|
+
shell = await shellHost.callTestBridge('getWorkletSlotStats')
|
|
106
|
+
} catch {}
|
|
107
|
+
}
|
|
108
|
+
const tenantReceived =
|
|
109
|
+
typeof tenant?.runJSCallbackReceived === 'number' ? tenant.runJSCallbackReceived : 0
|
|
110
|
+
const shellSent =
|
|
111
|
+
typeof shell?.runJSCallbackSent === 'number' ? shell.runJSCallbackSent : 0
|
|
112
|
+
return { tenantReceived, shellSent }
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function waitForWorkletCallbackDrain(
|
|
117
|
+
page: Page,
|
|
118
|
+
before: WorkletCallbackStats | null,
|
|
119
|
+
): Promise<void> {
|
|
120
|
+
if (!before) {
|
|
121
|
+
await page.waitForTimeout(250)
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
const deadline = Date.now() + 1500
|
|
125
|
+
let lastShellSent = before.shellSent
|
|
126
|
+
let stableSince = Date.now()
|
|
127
|
+
let sawCallback = false
|
|
128
|
+
|
|
129
|
+
while (Date.now() < deadline) {
|
|
130
|
+
const current = await readWorkletCallbackStats(page)
|
|
131
|
+
if (!current) {
|
|
132
|
+
await page.waitForTimeout(20)
|
|
133
|
+
continue
|
|
134
|
+
}
|
|
135
|
+
if (current.shellSent !== lastShellSent) {
|
|
136
|
+
lastShellSent = current.shellSent
|
|
137
|
+
stableSince = Date.now()
|
|
138
|
+
}
|
|
139
|
+
const sentDelta = current.shellSent - before.shellSent
|
|
140
|
+
if (sentDelta > 0) sawCallback = true
|
|
141
|
+
const receivedDelta = current.tenantReceived - before.tenantReceived
|
|
142
|
+
if (sawCallback && receivedDelta >= sentDelta && Date.now() - stableSince >= 100) {
|
|
143
|
+
return
|
|
144
|
+
}
|
|
145
|
+
await page.waitForTimeout(20)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (!sawCallback) {
|
|
149
|
+
// Some gestures are purely native and do not schedule RN callbacks. A
|
|
150
|
+
// bounded fallback still preserves Detox's "gesture has settled" contract.
|
|
151
|
+
await page.waitForTimeout(250)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// helper script injected via page.addInitScript so every page.evaluate
|
|
156
|
+
// can reach it as `__sootsimFindInteractiveCanvas()`. picks the canvas
|
|
157
|
+
// that actually receives pointer input. sootsim renders multiple
|
|
158
|
+
// <canvas> layers per surface (background/render targets, hidden
|
|
159
|
+
// offscreen, overlay) and `document.querySelector('canvas')` returns
|
|
160
|
+
// the first one in DOM order — usually a non-interactive layer with
|
|
161
|
+
// `pointer-events: none`. dispatching synthetic PointerEvents onto
|
|
162
|
+
// that layer silently no-ops because the gesture system only listens
|
|
163
|
+
// on the interactive layer (pointer-events: auto, non-zero rect).
|
|
164
|
+
const sootsimCanvasFinderInstalled = new WeakSet<Page>()
|
|
165
|
+
function installInteractiveCanvasFinder() {
|
|
166
|
+
window.__sootsimFindInteractiveCanvas = (): HTMLCanvasElement | null => {
|
|
167
|
+
const interactive: Array<{ c: HTMLCanvasElement; z: number; app: boolean }> = []
|
|
168
|
+
const appVisible: Array<{ c: HTMLCanvasElement; z: number; app: boolean }> = []
|
|
169
|
+
for (const c of Array.from(document.querySelectorAll('canvas'))) {
|
|
170
|
+
const s = window.getComputedStyle(c)
|
|
171
|
+
if (s.display === 'none' || s.visibility === 'hidden') continue
|
|
172
|
+
const r = c.getBoundingClientRect()
|
|
173
|
+
if (r.width === 0 || r.height === 0) continue
|
|
174
|
+
const entry = {
|
|
175
|
+
c,
|
|
176
|
+
z: parseInt(s.zIndex, 10) || 0,
|
|
177
|
+
app: c.dataset.surfaceId?.startsWith('app:') === true,
|
|
178
|
+
}
|
|
179
|
+
if (entry.app) appVisible.push(entry)
|
|
180
|
+
if (s.pointerEvents !== 'none') interactive.push(entry)
|
|
181
|
+
}
|
|
182
|
+
const list = appVisible.length > 0 ? appVisible : interactive
|
|
183
|
+
if (list.length === 0) return null
|
|
184
|
+
list.sort((a, b) => Number(b.app) - Number(a.app) || b.z - a.z)
|
|
185
|
+
return list[0].c
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function ensureCanvasFinder(page: Page) {
|
|
190
|
+
if (!sootsimCanvasFinderInstalled.has(page)) {
|
|
191
|
+
sootsimCanvasFinderInstalled.add(page)
|
|
192
|
+
await page.addInitScript(installInteractiveCanvasFinder)
|
|
193
|
+
}
|
|
194
|
+
await page.evaluate(installInteractiveCanvasFinder)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function readSootsimPageMetrics(page: Page): Promise<SootsimPageMetrics> {
|
|
198
|
+
await ensureCanvasFinder(page)
|
|
199
|
+
const deadline = Date.now() + 5000
|
|
200
|
+
while (Date.now() < deadline) {
|
|
201
|
+
const metrics = await page.evaluate(async () => {
|
|
202
|
+
const canvas =
|
|
203
|
+
window.__sootsimFindInteractiveCanvas?.() ?? document.querySelector('canvas')
|
|
204
|
+
if (!canvas) return null
|
|
205
|
+
const rect = canvas.getBoundingClientRect()
|
|
206
|
+
if (rect.width <= 0 || rect.height <= 0) return null
|
|
207
|
+
const surface =
|
|
208
|
+
(await window.__sootsimTest?.getSurfaceMetricsSnapshot?.()) as SurfaceMetricsResult
|
|
209
|
+
const screenWidth = surface?.window?.width
|
|
210
|
+
const screenHeight = surface?.window?.height
|
|
211
|
+
if (
|
|
212
|
+
typeof screenWidth !== 'number' ||
|
|
213
|
+
typeof screenHeight !== 'number' ||
|
|
214
|
+
screenWidth <= 0 ||
|
|
215
|
+
screenHeight <= 0
|
|
216
|
+
) {
|
|
217
|
+
return null
|
|
218
|
+
}
|
|
219
|
+
const homeIndicatorHeight = Number(surface?.device?.homeIndicatorHeight) || 0
|
|
220
|
+
const safeAreaBottom = Number(surface?.safeAreaInsets?.bottom) || 0
|
|
221
|
+
const bottomInset = Math.max(0, homeIndicatorHeight, safeAreaBottom)
|
|
222
|
+
const visibleRectBottom =
|
|
223
|
+
Number(surface?.visibleRect?.y) + Number(surface?.visibleRect?.height)
|
|
224
|
+
const interactiveHeight =
|
|
225
|
+
Number.isFinite(visibleRectBottom) && visibleRectBottom > 0
|
|
226
|
+
? Math.min(screenHeight, visibleRectBottom)
|
|
227
|
+
: Math.max(0, screenHeight - bottomInset)
|
|
228
|
+
return {
|
|
229
|
+
canvas: {
|
|
230
|
+
left: rect.left,
|
|
231
|
+
top: rect.top,
|
|
232
|
+
width: rect.width,
|
|
233
|
+
height: rect.height,
|
|
234
|
+
},
|
|
235
|
+
window: {
|
|
236
|
+
width: screenWidth,
|
|
237
|
+
height: screenHeight,
|
|
238
|
+
},
|
|
239
|
+
interactiveWindow: {
|
|
240
|
+
width: screenWidth,
|
|
241
|
+
height: interactiveHeight,
|
|
242
|
+
},
|
|
243
|
+
}
|
|
244
|
+
})
|
|
245
|
+
if (metrics) return metrics
|
|
246
|
+
await page.waitForTimeout(50)
|
|
247
|
+
}
|
|
248
|
+
throw new Error('sootsim surface metrics are not available')
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export async function readSootsimViewport(
|
|
252
|
+
page: Page,
|
|
253
|
+
): Promise<{ width: number; height: number }> {
|
|
254
|
+
return (await readSootsimPageMetrics(page)).window
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export async function readSootsimInteractiveViewport(
|
|
258
|
+
page: Page,
|
|
259
|
+
): Promise<{ width: number; height: number }> {
|
|
260
|
+
return (await readSootsimPageMetrics(page)).interactiveWindow
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export async function sootsimToPage(
|
|
264
|
+
page: Page,
|
|
265
|
+
sootX: number,
|
|
266
|
+
sootY: number,
|
|
267
|
+
): Promise<{ x: number; y: number }> {
|
|
268
|
+
const metrics = await readSootsimPageMetrics(page)
|
|
269
|
+
const scaleX = metrics.canvas.width / metrics.window.width
|
|
270
|
+
const scaleY = metrics.canvas.height / metrics.window.height
|
|
271
|
+
return {
|
|
272
|
+
x: metrics.canvas.left + sootX * scaleX,
|
|
273
|
+
y: metrics.canvas.top + sootY * scaleY,
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export async function dragScrollNode(
|
|
278
|
+
page: Page,
|
|
279
|
+
node: {
|
|
280
|
+
absolutePosition: { x: number; y: number }
|
|
281
|
+
layout: { width: number; height: number }
|
|
282
|
+
},
|
|
283
|
+
pixels: number,
|
|
284
|
+
direction: 'up' | 'down' | 'left' | 'right',
|
|
285
|
+
): Promise<void> {
|
|
286
|
+
const startSootX = node.absolutePosition.x + node.layout.width / 2
|
|
287
|
+
const startSootY = node.absolutePosition.y + node.layout.height / 2
|
|
288
|
+
let endSootX = startSootX
|
|
289
|
+
let endSootY = startSootY
|
|
290
|
+
|
|
291
|
+
switch (direction) {
|
|
292
|
+
case 'down':
|
|
293
|
+
endSootY -= pixels
|
|
294
|
+
break
|
|
295
|
+
case 'up':
|
|
296
|
+
endSootY += pixels
|
|
297
|
+
break
|
|
298
|
+
case 'left':
|
|
299
|
+
endSootX += pixels
|
|
300
|
+
break
|
|
301
|
+
case 'right':
|
|
302
|
+
endSootX -= pixels
|
|
303
|
+
break
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const startPage = await sootsimToPage(page, startSootX, startSootY)
|
|
307
|
+
const endPage = await sootsimToPage(page, endSootX, endSootY)
|
|
308
|
+
const steps = 12
|
|
309
|
+
|
|
310
|
+
await page.mouse.move(startPage.x, startPage.y)
|
|
311
|
+
await page.mouse.down()
|
|
312
|
+
await page.waitForTimeout(20)
|
|
313
|
+
|
|
314
|
+
const dx = (endPage.x - startPage.x) / steps
|
|
315
|
+
const dy = (endPage.y - startPage.y) / steps
|
|
316
|
+
for (let i = 1; i <= steps; i++) {
|
|
317
|
+
await page.mouse.move(startPage.x + dx * i, startPage.y + dy * i)
|
|
318
|
+
await page.waitForTimeout(16)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
await page.mouse.up()
|
|
322
|
+
await page.waitForTimeout(180)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Fire synthetic two-finger pointer events directly on the sootsim canvas. We
|
|
326
|
+
// use PointerEvents (not page.mouse) because page.mouse is single-cursor;
|
|
327
|
+
// sootsim's touch tracker keys on pointerId, so we just need to dispatch
|
|
328
|
+
// events with two distinct ids and pointerType 'touch'. Used for pinch /
|
|
329
|
+
// rotation drivers — RNGH's PinchGesture/RotationGesture both require ≥2
|
|
330
|
+
// concurrent active touches on iOS UIPinchGestureRecognizer parity.
|
|
331
|
+
export async function dispatchTwoFingerGesture(
|
|
332
|
+
page: Page,
|
|
333
|
+
startA: { x: number; y: number },
|
|
334
|
+
startB: { x: number; y: number },
|
|
335
|
+
endA: { x: number; y: number },
|
|
336
|
+
endB: { x: number; y: number },
|
|
337
|
+
options: { steps?: number; stepDelayMs?: number } = {},
|
|
338
|
+
): Promise<void> {
|
|
339
|
+
const steps = options.steps ?? 10
|
|
340
|
+
const stepDelay = options.stepDelayMs ?? 18
|
|
341
|
+
await ensureCanvasFinder(page)
|
|
342
|
+
const beforeWorkletStats = await readWorkletCallbackStats(page)
|
|
343
|
+
|
|
344
|
+
await page.evaluate(
|
|
345
|
+
({ startA, startB }) => {
|
|
346
|
+
const canvas: HTMLCanvasElement | null =
|
|
347
|
+
window.__sootsimFindInteractiveCanvas?.() ?? document.querySelector('canvas')
|
|
348
|
+
if (!canvas) throw new Error('sootsim interactive canvas not found')
|
|
349
|
+
const fire = (point: { x: number; y: number }, id: number, primary: boolean) => {
|
|
350
|
+
canvas.dispatchEvent(
|
|
351
|
+
new PointerEvent('pointerdown', {
|
|
352
|
+
bubbles: true,
|
|
353
|
+
cancelable: true,
|
|
354
|
+
clientX: point.x,
|
|
355
|
+
clientY: point.y,
|
|
356
|
+
isPrimary: primary,
|
|
357
|
+
pointerId: id,
|
|
358
|
+
pointerType: 'touch',
|
|
359
|
+
}),
|
|
360
|
+
)
|
|
361
|
+
}
|
|
362
|
+
fire(startA, 1001, true)
|
|
363
|
+
fire(startB, 1002, false)
|
|
364
|
+
},
|
|
365
|
+
{ startA, startB },
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
for (let i = 1; i <= steps; i++) {
|
|
369
|
+
const t = i / steps
|
|
370
|
+
const ax = startA.x + (endA.x - startA.x) * t
|
|
371
|
+
const ay = startA.y + (endA.y - startA.y) * t
|
|
372
|
+
const bx = startB.x + (endB.x - startB.x) * t
|
|
373
|
+
const by = startB.y + (endB.y - startB.y) * t
|
|
374
|
+
await page.evaluate(
|
|
375
|
+
({ ax, ay, bx, by }) => {
|
|
376
|
+
const canvas: HTMLCanvasElement | null =
|
|
377
|
+
window.__sootsimFindInteractiveCanvas?.() ?? document.querySelector('canvas')
|
|
378
|
+
if (!canvas) return
|
|
379
|
+
canvas.dispatchEvent(
|
|
380
|
+
new PointerEvent('pointermove', {
|
|
381
|
+
bubbles: true,
|
|
382
|
+
cancelable: true,
|
|
383
|
+
clientX: ax,
|
|
384
|
+
clientY: ay,
|
|
385
|
+
isPrimary: true,
|
|
386
|
+
pointerId: 1001,
|
|
387
|
+
pointerType: 'touch',
|
|
388
|
+
}),
|
|
389
|
+
)
|
|
390
|
+
canvas.dispatchEvent(
|
|
391
|
+
new PointerEvent('pointermove', {
|
|
392
|
+
bubbles: true,
|
|
393
|
+
cancelable: true,
|
|
394
|
+
clientX: bx,
|
|
395
|
+
clientY: by,
|
|
396
|
+
isPrimary: false,
|
|
397
|
+
pointerId: 1002,
|
|
398
|
+
pointerType: 'touch',
|
|
399
|
+
}),
|
|
400
|
+
)
|
|
401
|
+
},
|
|
402
|
+
{ ax, ay, bx, by },
|
|
403
|
+
)
|
|
404
|
+
await page.waitForTimeout(stepDelay)
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
await page.evaluate(
|
|
408
|
+
({ endA, endB }) => {
|
|
409
|
+
const canvas: HTMLCanvasElement | null =
|
|
410
|
+
window.__sootsimFindInteractiveCanvas?.() ?? document.querySelector('canvas')
|
|
411
|
+
if (!canvas) return
|
|
412
|
+
canvas.dispatchEvent(
|
|
413
|
+
new PointerEvent('pointerup', {
|
|
414
|
+
bubbles: true,
|
|
415
|
+
cancelable: true,
|
|
416
|
+
clientX: endA.x,
|
|
417
|
+
clientY: endA.y,
|
|
418
|
+
isPrimary: true,
|
|
419
|
+
pointerId: 1001,
|
|
420
|
+
pointerType: 'touch',
|
|
421
|
+
}),
|
|
422
|
+
)
|
|
423
|
+
canvas.dispatchEvent(
|
|
424
|
+
new PointerEvent('pointerup', {
|
|
425
|
+
bubbles: true,
|
|
426
|
+
cancelable: true,
|
|
427
|
+
clientX: endB.x,
|
|
428
|
+
clientY: endB.y,
|
|
429
|
+
isPrimary: false,
|
|
430
|
+
pointerId: 1002,
|
|
431
|
+
pointerType: 'touch',
|
|
432
|
+
}),
|
|
433
|
+
)
|
|
434
|
+
},
|
|
435
|
+
{ endA, endB },
|
|
436
|
+
)
|
|
437
|
+
await waitForWorkletCallbackDrain(page, beforeWorkletStats)
|
|
438
|
+
// runOnJS delivery increments before the callback's React setState commits.
|
|
439
|
+
// Detox gestures settle after native UI has observed the gesture result, so
|
|
440
|
+
// leave one bounded commit window after the worklet bridge drains.
|
|
441
|
+
await page.waitForTimeout(250)
|
|
442
|
+
}
|