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
package/README.md
CHANGED
|
@@ -184,7 +184,6 @@ the frame for quick art-direction passes.
|
|
|
184
184
|
the published CLI registry and the generated website docs both come from
|
|
185
185
|
`packages/sootsim-skills/`.
|
|
186
186
|
|
|
187
|
-
- `packages/sootsim/cli/registry.ts` re-exports the shared CLI metadata
|
|
188
187
|
- `packages/sootsim/skills/soot.md` is generated from that registry
|
|
189
188
|
- `src/features/site/docs/sootsim/cli/*` is generated output, not hand-edited source
|
|
190
189
|
|
package/detox/colors.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// re-export color utilities from kitchen-sink for convenience
|
|
2
|
+
// these are the same implementations used in detox tests
|
|
3
|
+
|
|
4
|
+
import * as fs from 'fs'
|
|
5
|
+
import { PNG } from 'pngjs'
|
|
6
|
+
|
|
7
|
+
export type RGB = { r: number; g: number; b: number }
|
|
8
|
+
|
|
9
|
+
export function getDominantColor(screenshotPath: string): RGB {
|
|
10
|
+
const data = fs.readFileSync(screenshotPath)
|
|
11
|
+
const png = PNG.sync.read(data)
|
|
12
|
+
|
|
13
|
+
const startX = Math.floor(png.width * 0.25)
|
|
14
|
+
const endX = Math.floor(png.width * 0.75)
|
|
15
|
+
const startY = Math.floor(png.height * 0.25)
|
|
16
|
+
const endY = Math.floor(png.height * 0.75)
|
|
17
|
+
|
|
18
|
+
let totalR = 0,
|
|
19
|
+
totalG = 0,
|
|
20
|
+
totalB = 0,
|
|
21
|
+
count = 0
|
|
22
|
+
|
|
23
|
+
for (let y = startY; y < endY; y++) {
|
|
24
|
+
for (let x = startX; x < endX; x++) {
|
|
25
|
+
const idx = (png.width * y + x) * 4
|
|
26
|
+
totalR += png.data[idx]
|
|
27
|
+
totalG += png.data[idx + 1]
|
|
28
|
+
totalB += png.data[idx + 2]
|
|
29
|
+
count++
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
r: Math.round(totalR / count),
|
|
35
|
+
g: Math.round(totalG / count),
|
|
36
|
+
b: Math.round(totalB / count),
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function isBlueish(color: RGB): boolean {
|
|
41
|
+
return color.b > 100 && color.b > color.r && color.b > color.g
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function isReddish(color: RGB): boolean {
|
|
45
|
+
return color.r > 100 && color.r > color.b && color.r > color.g
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function isGreenish(color: RGB): boolean {
|
|
49
|
+
return color.g > 100 && color.g > color.r && color.g > color.b
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function formatRGB(color: RGB): string {
|
|
53
|
+
return `RGB(${color.r}, ${color.g}, ${color.b})`
|
|
54
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// parse detox configuration from various locations
|
|
2
|
+
// maps detox device config to sootsim settings
|
|
3
|
+
|
|
4
|
+
import * as fs from 'fs'
|
|
5
|
+
import * as path from 'path'
|
|
6
|
+
|
|
7
|
+
export interface DetoxConfig {
|
|
8
|
+
testRunner?: {
|
|
9
|
+
args?: Record<string, any>
|
|
10
|
+
jest?: { setupTimeout?: number; teardownTimeout?: number }
|
|
11
|
+
}
|
|
12
|
+
apps?: Record<string, { type: string; binaryPath?: string; build?: string }>
|
|
13
|
+
devices?: Record<string, { type: string; device?: { type?: string } }>
|
|
14
|
+
configurations?: Record<string, { device?: string; app?: string }>
|
|
15
|
+
testMatch?: string[]
|
|
16
|
+
testDir?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface SootSimDetoxConfig {
|
|
20
|
+
testFiles: string[]
|
|
21
|
+
testTimeout: number
|
|
22
|
+
sootsimUrl: string
|
|
23
|
+
deviceModel: string
|
|
24
|
+
maxWorkers: number
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// search order: .detoxrc.js, .detoxrc.json, detox.config.js, detox.config.json, package.json#detox
|
|
28
|
+
export function loadDetoxConfig(projectDir: string): DetoxConfig | null {
|
|
29
|
+
const candidates = [
|
|
30
|
+
'.detoxrc.js',
|
|
31
|
+
'.detoxrc.json',
|
|
32
|
+
'detox.config.js',
|
|
33
|
+
'detox.config.json',
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
for (const file of candidates) {
|
|
37
|
+
const filePath = path.join(projectDir, file)
|
|
38
|
+
if (fs.existsSync(filePath)) {
|
|
39
|
+
try {
|
|
40
|
+
if (file.endsWith('.json')) {
|
|
41
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'))
|
|
42
|
+
}
|
|
43
|
+
// for JS files, we can't easily require them without running them
|
|
44
|
+
// return a basic config indicating file was found
|
|
45
|
+
return { testDir: 'e2e' }
|
|
46
|
+
} catch {
|
|
47
|
+
continue
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// check package.json
|
|
53
|
+
const pkgPath = path.join(projectDir, 'package.json')
|
|
54
|
+
if (fs.existsSync(pkgPath)) {
|
|
55
|
+
try {
|
|
56
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
|
|
57
|
+
if (pkg.detox) return pkg.detox
|
|
58
|
+
} catch {}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return null
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// find test files matching detox patterns
|
|
65
|
+
export function findDetoxTestFiles(
|
|
66
|
+
projectDir: string,
|
|
67
|
+
config: DetoxConfig | null,
|
|
68
|
+
): string[] {
|
|
69
|
+
const testDirs = ['e2e', 'test/e2e', 'detox', '__tests__/e2e']
|
|
70
|
+
const patterns = [
|
|
71
|
+
'.test.ts',
|
|
72
|
+
'.test.js',
|
|
73
|
+
'.test.tsx',
|
|
74
|
+
'.test.jsx',
|
|
75
|
+
'.spec.ts',
|
|
76
|
+
'.spec.js',
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
// if config specifies a test dir, prioritize it
|
|
80
|
+
if (config?.testDir) {
|
|
81
|
+
testDirs.unshift(config.testDir)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const files: string[] = []
|
|
85
|
+
|
|
86
|
+
for (const dir of testDirs) {
|
|
87
|
+
const fullDir = path.join(projectDir, dir)
|
|
88
|
+
if (!fs.existsSync(fullDir)) continue
|
|
89
|
+
|
|
90
|
+
const entries = fs.readdirSync(fullDir, { recursive: true })
|
|
91
|
+
for (const entry of entries) {
|
|
92
|
+
const entryStr = String(entry)
|
|
93
|
+
if (patterns.some((p) => entryStr.endsWith(p))) {
|
|
94
|
+
files.push(path.join(fullDir, entryStr))
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return files
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// map detox device type to sootsim device model
|
|
103
|
+
export function mapDetoxDevice(config: DetoxConfig | null): string {
|
|
104
|
+
if (!config?.devices) return 'iphone-15-pro'
|
|
105
|
+
|
|
106
|
+
const devices = Object.values(config.devices)
|
|
107
|
+
for (const dev of devices) {
|
|
108
|
+
const type = dev.device?.type?.toLowerCase() || ''
|
|
109
|
+
if (type.includes('se')) return 'iphone-se'
|
|
110
|
+
if (type.includes('16 pro max') || type.includes('16-pro-max'))
|
|
111
|
+
return 'iphone-16-pro-max'
|
|
112
|
+
if (type.includes('16 pro') || type.includes('16-pro')) return 'iphone-16-pro'
|
|
113
|
+
if (type.includes('16')) return 'iphone-16'
|
|
114
|
+
if (type.includes('15 pro max') || type.includes('15-pro-max'))
|
|
115
|
+
return 'iphone-15-pro-max'
|
|
116
|
+
if (type.includes('15 pro') || type.includes('15-pro')) return 'iphone-15-pro'
|
|
117
|
+
if (type.includes('15')) return 'iphone-15'
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return 'iphone-15-pro'
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// generate a sootsim-compatible config from detox config
|
|
124
|
+
export function toSootSimConfig(
|
|
125
|
+
projectDir: string,
|
|
126
|
+
config: DetoxConfig | null,
|
|
127
|
+
): SootSimDetoxConfig {
|
|
128
|
+
return {
|
|
129
|
+
testFiles: findDetoxTestFiles(projectDir, config),
|
|
130
|
+
testTimeout: config?.testRunner?.jest?.setupTimeout || 120000,
|
|
131
|
+
sootsimUrl: process.env.SOOTSIM_URL || 'http://localhost:5173',
|
|
132
|
+
deviceModel: mapDetoxDevice(config),
|
|
133
|
+
maxWorkers: 1,
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
// detox-compatible expectations for sootsim
|
|
2
|
+
// expect(element).toExist(), .toBeVisible(), .toHaveText(), etc.
|
|
3
|
+
// waitFor(element).toExist().withTimeout(ms)
|
|
4
|
+
|
|
5
|
+
import { dragScrollNode, readSootsimInteractiveViewport } from './gestures'
|
|
6
|
+
import type { SootElement } from './index'
|
|
7
|
+
import type { Matcher } from './matchers'
|
|
8
|
+
import type { Page } from 'playwright'
|
|
9
|
+
|
|
10
|
+
type FindFn = (matcher: Matcher) => Promise<any>
|
|
11
|
+
type GetPageFn = () => Page
|
|
12
|
+
const DEFAULT_VISIBILITY_THRESHOLD = 0.75
|
|
13
|
+
|
|
14
|
+
async function scrollMatcherElement(
|
|
15
|
+
page: Page,
|
|
16
|
+
findNode: FindFn,
|
|
17
|
+
matcher: Matcher,
|
|
18
|
+
pixels: number,
|
|
19
|
+
direction: 'up' | 'down' | 'left' | 'right',
|
|
20
|
+
): Promise<void> {
|
|
21
|
+
const node = await findNode(matcher)
|
|
22
|
+
if (!node) {
|
|
23
|
+
throw new Error(`scroll container not found: ${JSON.stringify(matcher)}`)
|
|
24
|
+
}
|
|
25
|
+
await dragScrollNode(page, node, pixels, direction)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type Viewport = { width: number; height: number }
|
|
29
|
+
|
|
30
|
+
function isNodeVisibleInViewport(node: any, viewport: Viewport): boolean {
|
|
31
|
+
if (!node || node.layout.width <= 0 || node.layout.height <= 0) {
|
|
32
|
+
return false
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const frame = node.visibleFrame ?? {
|
|
36
|
+
x: node.absolutePosition?.x ?? node.layout?.x ?? 0,
|
|
37
|
+
y: node.absolutePosition?.y ?? node.layout?.y ?? 0,
|
|
38
|
+
width: node.layout.width,
|
|
39
|
+
height: node.layout.height,
|
|
40
|
+
}
|
|
41
|
+
const right = frame.x + frame.width
|
|
42
|
+
const bottom = frame.y + frame.height
|
|
43
|
+
|
|
44
|
+
const visibleWidth = Math.max(0, Math.min(right, viewport.width) - Math.max(frame.x, 0))
|
|
45
|
+
const visibleHeight = Math.max(
|
|
46
|
+
0,
|
|
47
|
+
Math.min(bottom, viewport.height) - Math.max(frame.y, 0),
|
|
48
|
+
)
|
|
49
|
+
const visibleArea = visibleWidth * visibleHeight
|
|
50
|
+
const totalArea = node.layout.width * node.layout.height
|
|
51
|
+
|
|
52
|
+
return totalArea > 0 && visibleArea / totalArea >= DEFAULT_VISIBILITY_THRESHOLD
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function isNodeVisible(page: Page, node: any): Promise<boolean> {
|
|
56
|
+
if (!node || node.layout.width <= 0 || node.layout.height <= 0) {
|
|
57
|
+
return false
|
|
58
|
+
}
|
|
59
|
+
return isNodeVisibleInViewport(node, await readSootsimInteractiveViewport(page))
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function describeNodeVisibility(node: any, viewport?: Viewport): string {
|
|
63
|
+
if (!node) return 'not found'
|
|
64
|
+
const absX = node.absolutePosition?.x ?? node.layout?.x ?? 0
|
|
65
|
+
const absY = node.absolutePosition?.y ?? node.layout?.y ?? 0
|
|
66
|
+
return JSON.stringify({
|
|
67
|
+
layout: node.layout,
|
|
68
|
+
absolutePosition: node.absolutePosition ?? null,
|
|
69
|
+
frame: {
|
|
70
|
+
x: absX,
|
|
71
|
+
y: absY,
|
|
72
|
+
right: absX + (node.layout?.width ?? 0),
|
|
73
|
+
bottom: absY + (node.layout?.height ?? 0),
|
|
74
|
+
},
|
|
75
|
+
visibleFrame: node.visibleFrame ?? null,
|
|
76
|
+
viewport: viewport ?? null,
|
|
77
|
+
style: node.style ?? null,
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// expect(element) returns an object with assertion methods
|
|
82
|
+
export function createExpect(findNode: FindFn, getPage: GetPageFn) {
|
|
83
|
+
return function sootExpect(el: SootElement): SootExpectation {
|
|
84
|
+
return new SootExpectation(el._matcher, findNode, getPage, false)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
class SootExpectation {
|
|
89
|
+
private matcher: Matcher
|
|
90
|
+
private findNode: FindFn
|
|
91
|
+
private getPage: GetPageFn
|
|
92
|
+
private negated: boolean
|
|
93
|
+
|
|
94
|
+
constructor(matcher: Matcher, findNode: FindFn, getPage: GetPageFn, negated: boolean) {
|
|
95
|
+
this.matcher = matcher
|
|
96
|
+
this.findNode = findNode
|
|
97
|
+
this.getPage = getPage
|
|
98
|
+
this.negated = negated
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
get not(): SootExpectation {
|
|
102
|
+
return new SootExpectation(this.matcher, this.findNode, this.getPage, !this.negated)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async toExist(): Promise<void> {
|
|
106
|
+
const node = await this.findNode(this.matcher)
|
|
107
|
+
const exists = node !== null && node !== undefined
|
|
108
|
+
if (this.negated) {
|
|
109
|
+
if (exists) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
`expected element NOT to exist but it does: ${JSON.stringify(this.matcher)}`,
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
} else {
|
|
115
|
+
if (!exists) {
|
|
116
|
+
throw new Error(
|
|
117
|
+
`expected element to exist but it was not found: ${JSON.stringify(this.matcher)}`,
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async toBeVisible(): Promise<void> {
|
|
124
|
+
const node = await this.findNode(this.matcher)
|
|
125
|
+
const page = this.getPage()
|
|
126
|
+
const viewport = node ? await readSootsimInteractiveViewport(page) : undefined
|
|
127
|
+
const visible = viewport ? isNodeVisibleInViewport(node, viewport) : false
|
|
128
|
+
if (this.negated) {
|
|
129
|
+
if (visible) {
|
|
130
|
+
throw new Error(
|
|
131
|
+
`expected element NOT to be visible: ${JSON.stringify(this.matcher)}`,
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
} else {
|
|
135
|
+
if (!visible) {
|
|
136
|
+
throw new Error(
|
|
137
|
+
`expected element to be visible but it is ${describeNodeVisibility(node, viewport)}: ${JSON.stringify(this.matcher)}`,
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async toBeNotVisible(): Promise<void> {
|
|
144
|
+
const node = await this.findNode(this.matcher)
|
|
145
|
+
const visible = await isNodeVisible(this.getPage(), node)
|
|
146
|
+
if (visible) {
|
|
147
|
+
throw new Error(
|
|
148
|
+
`expected element NOT to be visible: ${JSON.stringify(this.matcher)}`,
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async toHaveText(expectedText: string): Promise<void> {
|
|
154
|
+
const node = await this.findNode(this.matcher)
|
|
155
|
+
if (!node) {
|
|
156
|
+
throw new Error(`element not found for toHaveText: ${JSON.stringify(this.matcher)}`)
|
|
157
|
+
}
|
|
158
|
+
const actualText = node.text || ''
|
|
159
|
+
const matches = actualText === expectedText || actualText.includes(expectedText)
|
|
160
|
+
|
|
161
|
+
if (this.negated) {
|
|
162
|
+
if (matches) {
|
|
163
|
+
throw new Error(
|
|
164
|
+
`expected text NOT to be "${expectedText}" but got "${actualText}": ${JSON.stringify(this.matcher)}`,
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
} else {
|
|
168
|
+
if (!matches) {
|
|
169
|
+
throw new Error(
|
|
170
|
+
`expected text "${expectedText}" but got "${actualText}": ${JSON.stringify(this.matcher)}`,
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async toHaveId(expectedId: string): Promise<void> {
|
|
177
|
+
const node = await this.findNode(this.matcher)
|
|
178
|
+
if (!node) {
|
|
179
|
+
throw new Error(`element not found for toHaveId: ${JSON.stringify(this.matcher)}`)
|
|
180
|
+
}
|
|
181
|
+
const hasId = node.testID === expectedId || node.id === expectedId
|
|
182
|
+
if (this.negated) {
|
|
183
|
+
if (hasId) throw new Error(`expected element NOT to have id "${expectedId}"`)
|
|
184
|
+
} else {
|
|
185
|
+
if (!hasId)
|
|
186
|
+
throw new Error(
|
|
187
|
+
`expected element to have id "${expectedId}" but got "${node.testID || node.id}"`,
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async toHaveLabel(expectedLabel: string): Promise<void> {
|
|
193
|
+
const node = await this.findNode(this.matcher)
|
|
194
|
+
if (!node) {
|
|
195
|
+
throw new Error(
|
|
196
|
+
`element not found for toHaveLabel: ${JSON.stringify(this.matcher)}`,
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
// check explicit accessibilityLabel, then derived text content
|
|
200
|
+
const actual = node.accessibilityLabel || node.text || ''
|
|
201
|
+
const matches = actual === expectedLabel
|
|
202
|
+
if (this.negated) {
|
|
203
|
+
if (matches)
|
|
204
|
+
throw new Error(`expected label NOT to be "${expectedLabel}" but it was`)
|
|
205
|
+
} else {
|
|
206
|
+
if (!matches)
|
|
207
|
+
throw new Error(
|
|
208
|
+
`expected label "${expectedLabel}" but got "${actual}": ${JSON.stringify(this.matcher)}`,
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async toHaveAccessibilityRole(expectedRole: string): Promise<void> {
|
|
214
|
+
const node = await this.findNode(this.matcher)
|
|
215
|
+
if (!node) {
|
|
216
|
+
throw new Error(
|
|
217
|
+
`element not found for toHaveAccessibilityRole: ${JSON.stringify(this.matcher)}`,
|
|
218
|
+
)
|
|
219
|
+
}
|
|
220
|
+
const actual = node.accessibilityRole || ''
|
|
221
|
+
const matches = actual === expectedRole
|
|
222
|
+
if (this.negated) {
|
|
223
|
+
if (matches) throw new Error(`expected role NOT to be "${expectedRole}" but it was`)
|
|
224
|
+
} else {
|
|
225
|
+
if (!matches)
|
|
226
|
+
throw new Error(
|
|
227
|
+
`expected role "${expectedRole}" but got "${actual}": ${JSON.stringify(this.matcher)}`,
|
|
228
|
+
)
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async toBeEnabled(): Promise<void> {
|
|
233
|
+
const node = await this.findNode(this.matcher)
|
|
234
|
+
if (!node) {
|
|
235
|
+
throw new Error(
|
|
236
|
+
`element not found for toBeEnabled: ${JSON.stringify(this.matcher)}`,
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
const disabled = node.accessibilityState?.disabled ?? false
|
|
240
|
+
if (this.negated) {
|
|
241
|
+
if (!disabled) throw new Error(`expected element to be disabled but it is enabled`)
|
|
242
|
+
} else {
|
|
243
|
+
if (disabled) throw new Error(`expected element to be enabled but it is disabled`)
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async toBeFocused(): Promise<void> {
|
|
248
|
+
// in sootsim we don't track focus state fully, so just check existence
|
|
249
|
+
await this.toExist()
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async toHaveValue(value: string): Promise<void> {
|
|
253
|
+
const node = await this.findNode(this.matcher)
|
|
254
|
+
if (!node) {
|
|
255
|
+
throw new Error(
|
|
256
|
+
`element not found for toHaveValue: ${JSON.stringify(this.matcher)}`,
|
|
257
|
+
)
|
|
258
|
+
}
|
|
259
|
+
// value could be in text or props
|
|
260
|
+
const actual = node.text || ''
|
|
261
|
+
if (this.negated) {
|
|
262
|
+
if (actual.includes(value)) {
|
|
263
|
+
throw new Error(
|
|
264
|
+
`expected element NOT to have value "${value}" but got "${actual}"`,
|
|
265
|
+
)
|
|
266
|
+
}
|
|
267
|
+
} else {
|
|
268
|
+
if (!actual.includes(value)) {
|
|
269
|
+
throw new Error(`expected element to have value "${value}" but got "${actual}"`)
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async toHaveSliderPosition(
|
|
275
|
+
normalizedPosition: number,
|
|
276
|
+
tolerance?: number,
|
|
277
|
+
): Promise<void> {
|
|
278
|
+
// stub for slider tests -- sootsim doesn't have native sliders yet
|
|
279
|
+
await this.toExist()
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async toHaveToggleValue(value: boolean): Promise<void> {
|
|
283
|
+
await this.toExist()
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// waitFor(element) returns a chainable object that polls until the assertion passes
|
|
288
|
+
export function createWaitFor(findNode: FindFn, getPage: GetPageFn) {
|
|
289
|
+
return function sootWaitFor(el: SootElement): SootWaitForChain {
|
|
290
|
+
return new SootWaitForChain(el._matcher, findNode, getPage)
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
class SootWaitForChain {
|
|
295
|
+
private matcher: Matcher
|
|
296
|
+
private findNode: FindFn
|
|
297
|
+
private getPage: GetPageFn
|
|
298
|
+
private assertionFn: (() => Promise<void>) | null = null
|
|
299
|
+
private _negated = false
|
|
300
|
+
|
|
301
|
+
constructor(matcher: Matcher, findNode: FindFn, getPage: GetPageFn) {
|
|
302
|
+
this.matcher = matcher
|
|
303
|
+
this.findNode = findNode
|
|
304
|
+
this.getPage = getPage
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
get not(): SootWaitForChain {
|
|
308
|
+
this._negated = true
|
|
309
|
+
return this
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
toExist(): SootWaitForAction {
|
|
313
|
+
this.assertionFn = async () => {
|
|
314
|
+
const node = await this.findNode(this.matcher)
|
|
315
|
+
const exists = node !== null && node !== undefined
|
|
316
|
+
if (this._negated ? exists : !exists) {
|
|
317
|
+
throw new Error(`waitFor toExist failed: ${JSON.stringify(this.matcher)}`)
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return new SootWaitForAction(this.assertionFn, this.findNode, this.getPage)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
toBeVisible(): SootWaitForAction {
|
|
324
|
+
this.assertionFn = async () => {
|
|
325
|
+
const node = await this.findNode(this.matcher)
|
|
326
|
+
const page = this.getPage()
|
|
327
|
+
const viewport = node ? await readSootsimInteractiveViewport(page) : undefined
|
|
328
|
+
const visible = viewport ? isNodeVisibleInViewport(node, viewport) : false
|
|
329
|
+
if (this._negated ? visible : !visible) {
|
|
330
|
+
throw new Error(
|
|
331
|
+
`waitFor toBeVisible failed (${describeNodeVisibility(node, viewport)}): ${JSON.stringify(this.matcher)}`,
|
|
332
|
+
)
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return new SootWaitForAction(this.assertionFn, this.findNode, this.getPage)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
toBeNotVisible(): SootWaitForAction {
|
|
339
|
+
this.assertionFn = async () => {
|
|
340
|
+
const node = await this.findNode(this.matcher)
|
|
341
|
+
const visible = await isNodeVisible(this.getPage(), node)
|
|
342
|
+
if (visible) {
|
|
343
|
+
throw new Error(`waitFor toBeNotVisible failed: ${JSON.stringify(this.matcher)}`)
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return new SootWaitForAction(this.assertionFn, this.findNode, this.getPage)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
toHaveText(expectedText: string): SootWaitForAction {
|
|
350
|
+
this.assertionFn = async () => {
|
|
351
|
+
const node = await this.findNode(this.matcher)
|
|
352
|
+
if (!node)
|
|
353
|
+
throw new Error(
|
|
354
|
+
`waitFor toHaveText: element not found: ${JSON.stringify(this.matcher)}`,
|
|
355
|
+
)
|
|
356
|
+
const actual = node.text || ''
|
|
357
|
+
const matches = actual === expectedText || actual.includes(expectedText)
|
|
358
|
+
if (this._negated ? matches : !matches) {
|
|
359
|
+
throw new Error(
|
|
360
|
+
`waitFor toHaveText: expected "${expectedText}" but got "${actual}" ${JSON.stringify(this.matcher)}`,
|
|
361
|
+
)
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return new SootWaitForAction(this.assertionFn, this.findNode, this.getPage)
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
toHaveValue(value: string): SootWaitForAction {
|
|
368
|
+
this.assertionFn = async () => {
|
|
369
|
+
const node = await this.findNode(this.matcher)
|
|
370
|
+
if (!node) throw new Error(`waitFor toHaveValue: element not found`)
|
|
371
|
+
const actual = node.text || ''
|
|
372
|
+
if (!actual.includes(value)) {
|
|
373
|
+
throw new Error(`waitFor toHaveValue: expected "${value}" in "${actual}"`)
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return new SootWaitForAction(this.assertionFn, this.findNode, this.getPage)
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
class SootWaitForAction {
|
|
381
|
+
private assertionFn: () => Promise<void>
|
|
382
|
+
private findNode: FindFn
|
|
383
|
+
private getPage: GetPageFn
|
|
384
|
+
|
|
385
|
+
constructor(assertionFn: () => Promise<void>, findNode: FindFn, getPage: GetPageFn) {
|
|
386
|
+
this.assertionFn = assertionFn
|
|
387
|
+
this.findNode = findNode
|
|
388
|
+
this.getPage = getPage
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async withTimeout(ms: number): Promise<void> {
|
|
392
|
+
const start = Date.now()
|
|
393
|
+
const pollInterval = 100
|
|
394
|
+
let lastError: Error | null = null
|
|
395
|
+
|
|
396
|
+
while (Date.now() - start < ms) {
|
|
397
|
+
try {
|
|
398
|
+
await this.assertionFn()
|
|
399
|
+
return // assertion passed
|
|
400
|
+
} catch (e) {
|
|
401
|
+
lastError = e as Error
|
|
402
|
+
await new Promise((r) => setTimeout(r, pollInterval))
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
throw lastError || new Error(`waitFor timed out after ${ms}ms`)
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// if no withTimeout is called, just run the assertion once
|
|
410
|
+
async then(resolve: (v: void) => void, reject: (e: any) => void): Promise<void> {
|
|
411
|
+
try {
|
|
412
|
+
// default 5 second timeout
|
|
413
|
+
await this.withTimeout(5000)
|
|
414
|
+
resolve(undefined)
|
|
415
|
+
} catch (e) {
|
|
416
|
+
reject(e)
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
whileElement(matcher: Matcher): SootWaitForScrollAction {
|
|
421
|
+
return new SootWaitForScrollAction(
|
|
422
|
+
this.assertionFn,
|
|
423
|
+
this.findNode,
|
|
424
|
+
this.getPage,
|
|
425
|
+
matcher,
|
|
426
|
+
)
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
class SootWaitForScrollAction {
|
|
431
|
+
private assertionFn: () => Promise<void>
|
|
432
|
+
private findNode: FindFn
|
|
433
|
+
private getPage: GetPageFn
|
|
434
|
+
private matcher: Matcher
|
|
435
|
+
|
|
436
|
+
constructor(
|
|
437
|
+
assertionFn: () => Promise<void>,
|
|
438
|
+
findNode: FindFn,
|
|
439
|
+
getPage: GetPageFn,
|
|
440
|
+
matcher: Matcher,
|
|
441
|
+
) {
|
|
442
|
+
this.assertionFn = assertionFn
|
|
443
|
+
this.findNode = findNode
|
|
444
|
+
this.getPage = getPage
|
|
445
|
+
this.matcher = matcher
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
async scroll(
|
|
449
|
+
pixels: number,
|
|
450
|
+
direction: 'up' | 'down' | 'left' | 'right',
|
|
451
|
+
_startPositionX?: number,
|
|
452
|
+
_startPositionY?: number,
|
|
453
|
+
): Promise<void> {
|
|
454
|
+
const page = this.getPage()
|
|
455
|
+
const start = Date.now()
|
|
456
|
+
const timeoutMs = 5000
|
|
457
|
+
let lastError: Error | null = null
|
|
458
|
+
|
|
459
|
+
while (Date.now() - start < timeoutMs) {
|
|
460
|
+
try {
|
|
461
|
+
await this.assertionFn()
|
|
462
|
+
return
|
|
463
|
+
} catch (error) {
|
|
464
|
+
lastError = error as Error
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
await scrollMatcherElement(page, this.findNode, this.matcher, pixels, direction)
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
throw (
|
|
471
|
+
lastError ||
|
|
472
|
+
new Error(
|
|
473
|
+
`waitFor whileElement(...).scroll() timed out: ${JSON.stringify(this.matcher)}`,
|
|
474
|
+
)
|
|
475
|
+
)
|
|
476
|
+
}
|
|
477
|
+
}
|