peachsim 0.1.527
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 +22 -0
- package/README.md +640 -0
- package/cli/app-config.ts +82 -0
- package/cli/app-fonts.ts +388 -0
- package/cli/app-project.ts +59 -0
- package/cli/app-splash.ts +171 -0
- package/cli/app-state-reset.ts +26 -0
- package/cli/auth.ts +184 -0
- package/cli/bin.ts +59 -0
- package/cli/bridge-diagnostics.ts +226 -0
- package/cli/bridge-flow-runner.ts +2806 -0
- package/cli/browser-backend.ts +44 -0
- package/cli/browser-evals.ts +96 -0
- package/cli/cli-update.ts +261 -0
- package/cli/cloud-client.ts +1530 -0
- package/cli/cloud-dispatch.ts +206 -0
- package/cli/cloud-session.ts +227 -0
- package/cli/command-contract.ts +221 -0
- package/cli/command-registry.ts +304 -0
- package/cli/commands/agent-wrapper.ts +1048 -0
- package/cli/commands/agent.ts +439 -0
- package/cli/commands/app-fonts.ts +99 -0
- package/cli/commands/assert.ts +542 -0
- package/cli/commands/auth.ts +71 -0
- package/cli/commands/box/bundle-plane.ts +71 -0
- package/cli/commands/box/checkout-plane.ts +256 -0
- package/cli/commands/box/plane-share.ts +208 -0
- package/cli/commands/box/rnx-command.ts +60 -0
- package/cli/commands/box/search-command.ts +157 -0
- package/cli/commands/box.ts +925 -0
- package/cli/commands/camera.ts +266 -0
- package/cli/commands/cleanup.ts +169 -0
- package/cli/commands/compat.ts +176 -0
- package/cli/commands/config.ts +34 -0
- package/cli/commands/control.ts +2154 -0
- package/cli/commands/cpu-profile.ts +941 -0
- package/cli/commands/daemon-mac-app.ts +165 -0
- package/cli/commands/daemon.ts +800 -0
- package/cli/commands/debug.ts +751 -0
- package/cli/commands/desktop.ts +39 -0
- package/cli/commands/detect.ts +201 -0
- package/cli/commands/detox.ts +481 -0
- package/cli/commands/device.ts +134 -0
- package/cli/commands/diagnose.ts +589 -0
- package/cli/commands/do-chain.ts +408 -0
- package/cli/commands/electron.ts +94 -0
- package/cli/commands/film.ts +379 -0
- package/cli/commands/flow.ts +1240 -0
- package/cli/commands/inspect/actions.ts +516 -0
- package/cli/commands/inspect/core.ts +2086 -0
- package/cli/commands/inspect/count.ts +17 -0
- package/cli/commands/inspect/describe.ts +119 -0
- package/cli/commands/inspect/env.ts +25 -0
- package/cli/commands/inspect/find.ts +169 -0
- package/cli/commands/inspect/get-layout.ts +53 -0
- package/cli/commands/inspect/keyboard.ts +54 -0
- package/cli/commands/inspect/list.ts +175 -0
- package/cli/commands/inspect/memory.ts +215 -0
- package/cli/commands/inspect/redaction.ts +39 -0
- package/cli/commands/inspect/resolve-target.ts +77 -0
- package/cli/commands/inspect/screens.ts +78 -0
- package/cli/commands/inspect/settle.ts +32 -0
- package/cli/commands/inspect/settling.ts +197 -0
- package/cli/commands/inspect/shared.ts +259 -0
- package/cli/commands/inspect/sleep.ts +15 -0
- package/cli/commands/inspect/tree.ts +23 -0
- package/cli/commands/inspect/url.ts +17 -0
- package/cli/commands/inspect/wait-event.ts +211 -0
- package/cli/commands/inspect/wait-idle.ts +25 -0
- package/cli/commands/inspect/wait-ready.ts +87 -0
- package/cli/commands/inspect/wait-selector.ts +59 -0
- package/cli/commands/inspect/wait.ts +32 -0
- package/cli/commands/inspect.ts +4738 -0
- package/cli/commands/install-cli.ts +275 -0
- package/cli/commands/install-desktop.ts +385 -0
- package/cli/commands/login.ts +357 -0
- package/cli/commands/logout.ts +24 -0
- package/cli/commands/maestro-generate.ts +375 -0
- package/cli/commands/maestro.ts +536 -0
- package/cli/commands/mode.ts +58 -0
- package/cli/commands/no-bridge-hint.ts +80 -0
- package/cli/commands/perf.ts +67 -0
- package/cli/commands/permissions.ts +203 -0
- package/cli/commands/platform.ts +410 -0
- package/cli/commands/preview.ts +103 -0
- package/cli/commands/profile.ts +106 -0
- package/cli/commands/react.ts +353 -0
- package/cli/commands/record.ts +1435 -0
- package/cli/commands/report-issue.ts +305 -0
- package/cli/commands/reset.ts +89 -0
- package/cli/commands/runtime.ts +348 -0
- package/cli/commands/screenshot-command.ts +106 -0
- package/cli/commands/screenshot-layers.ts +144 -0
- package/cli/commands/screenshot-mode.ts +38 -0
- package/cli/commands/screenshot.ts +492 -0
- package/cli/commands/screenshots-capture.ts +608 -0
- package/cli/commands/screenshots.ts +127 -0
- package/cli/commands/serve.ts +139 -0
- package/cli/commands/setup.ts +416 -0
- package/cli/commands/shell-boolean-mode.ts +82 -0
- package/cli/commands/skills.ts +468 -0
- package/cli/commands/slides.ts +357 -0
- package/cli/commands/state.ts +94 -0
- package/cli/commands/storage.ts +58 -0
- package/cli/commands/telemetry.ts +55 -0
- package/cli/commands/test.ts +535 -0
- package/cli/commands/three-mode.ts +771 -0
- package/cli/commands/timeline.ts +121 -0
- package/cli/commands/upgrade.ts +167 -0
- package/cli/commands/upload.ts +1203 -0
- package/cli/commands/version.ts +63 -0
- package/cli/commands/welcome.ts +17 -0
- package/cli/commands/what-happened.ts +328 -0
- package/cli/current-sim.ts +217 -0
- package/cli/desktop-companion.ts +315 -0
- package/cli/drivers/electron.ts +70 -0
- package/cli/drivers/index.ts +20 -0
- package/cli/drivers/playwright-provisioning.ts +184 -0
- package/cli/drivers/playwright-sim-host.ts +537 -0
- package/cli/drivers/playwright.ts +209 -0
- package/cli/drivers/registry.ts +65 -0
- package/cli/drivers/types.ts +102 -0
- package/cli/flow-export.ts +55 -0
- package/cli/flow-file.ts +142 -0
- package/cli/flow-live-status.ts +126 -0
- package/cli/flow-session.ts +140 -0
- package/cli/help-core.ts +143 -0
- package/cli/help.ts +44 -0
- package/cli/hidden-runtime-alias.ts +19 -0
- package/cli/hints.ts +214 -0
- package/cli/inspect-notice-state.ts +114 -0
- package/cli/internal-child.ts +176 -0
- package/cli/maestro-js.ts +323 -0
- package/cli/main.ts +704 -0
- package/cli/open-url.ts +8 -0
- package/cli/outbound-endpoints.ts +830 -0
- package/cli/parent-pid.ts +204 -0
- package/cli/parse-args.ts +189 -0
- package/cli/privacy.ts +53 -0
- package/cli/prompt.ts +56 -0
- package/cli/recording-access.ts +108 -0
- package/cli/registry.ts +2 -0
- package/cli/resolve-assets.ts +63 -0
- package/cli/rnx-in-process.ts +98 -0
- package/cli/run-registry.ts +226 -0
- package/cli/run-rnx-core.ts +148 -0
- package/cli/run-rnx.ts +137 -0
- package/cli/runtime-notes.ts +66 -0
- package/cli/runtime-summary.ts +25 -0
- package/cli/secure-file.ts +31 -0
- package/cli/self-invocation.ts +58 -0
- package/cli/send-to-box.ts +109 -0
- package/cli/setup-repository.ts +191 -0
- package/cli/shell-init.ts +98 -0
- package/cli/standalone.ts +4 -0
- package/cli/telemetry.ts +84 -0
- package/cli/ui.ts +68 -0
- package/cli/ws-bridge.ts +1020 -0
- package/detox/attempt-events.cjs +23 -0
- package/detox/colors.ts +54 -0
- package/detox/config-loader.ts +135 -0
- package/detox/element-types.ts +48 -0
- package/detox/expectations.ts +534 -0
- package/detox/gestures.ts +501 -0
- package/detox/index.ts +2694 -0
- package/detox/jest-preset.cjs +29 -0
- package/detox/jest-setup-after-env.cjs +22 -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-lifecycle.ts +290 -0
- package/detox/navigation.ts +43 -0
- package/detox/proof-frame.cjs +114 -0
- package/detox/record-block-execution.cjs +78 -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-lib/agent-daemon-client.cjs +502 -0
- package/dist-lib/agent-events.cjs +48 -0
- package/dist-lib/agent-identity.cjs +53 -0
- package/dist-lib/agent-sessions.cjs +759 -0
- package/dist-lib/attached-projects.cjs +526 -0
- package/dist-lib/auth/shared-session.cjs +208 -0
- package/dist-lib/backend-origin.cjs +88 -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/bridge-contract-input.cjs +451 -0
- package/dist-lib/bridge-contract-input.mjs +414 -0
- package/dist-lib/bridge-contract.cjs +130 -0
- package/dist-lib/bridge-contract.mjs +94 -0
- package/dist-lib/capture-contract.cjs +220 -0
- package/dist-lib/capture-contract.mjs +190 -0
- package/dist-lib/cli-constants.cjs +32 -0
- package/dist-lib/cloud-contract.cjs +194 -0
- package/dist-lib/cloud-contract.mjs +145 -0
- package/dist-lib/cloud.cjs +670 -0
- package/dist-lib/cloud.mjs +639 -0
- package/dist-lib/config.cjs +96 -0
- package/dist-lib/detox/index.cjs +3789 -0
- package/dist-lib/detox/jest-preset.cjs +29 -0
- package/dist-lib/dev-bundle-resolution.cjs +290 -0
- package/dist-lib/home-paths.cjs +638 -0
- package/dist-lib/host/bridge-host.cjs +10903 -0
- package/dist-lib/host/fetch-proxy-handler.cjs +421 -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/replacement-module-handler.cjs +469 -0
- package/dist-lib/host/websocket-proxy.cjs +298 -0
- package/dist-lib/index.cjs +4268 -0
- package/dist-lib/jump-to-source-babel.cjs +93 -0
- package/dist-lib/jump-to-source-native.cjs +112 -0
- package/dist-lib/menu.cjs +646 -0
- package/dist-lib/menu.mjs +604 -0
- package/dist-lib/metro-fingerprint-registry.cjs +186 -0
- package/dist-lib/metro-fingerprint-registry.mjs +156 -0
- package/dist-lib/metro-production-bundle.cjs +329 -0
- package/dist-lib/metro-production-bundle.mjs +295 -0
- package/dist-lib/metro.cjs +904 -0
- package/dist-lib/profiles.cjs +246 -0
- package/dist-lib/public-brand.cjs +39 -0
- package/dist-lib/react-native-host-modules.cjs +51 -0
- package/dist-lib/react-native-host-modules.mjs +25 -0
- package/dist-lib/render-mode.cjs +54 -0
- package/dist-lib/scripts/dev-server-scanner.cjs +1232 -0
- package/dist-lib/sdk.cjs +2433 -0
- package/dist-lib/sdk.mjs +2336 -0
- package/dist-lib/skills.cjs +39132 -0
- package/dist-lib/swift-submit.cjs +92 -0
- package/dist-lib/swift-submit.mjs +65 -0
- package/dist-lib/swift.cjs +1646 -0
- package/dist-lib/vite.cjs +1591 -0
- package/package.json +290 -0
- package/scripts/dev-server-scanner.ts +1291 -0
- package/scripts/optional-demo-registry.ts +113 -0
- package/skills/contrast/SKILL.md +165 -0
- package/skills/rnx-debug/SKILL.md +433 -0
- package/skills/rnx-perf/SKILL.md +399 -0
- package/skills/rnx-setup/SKILL.md +172 -0
- package/skills/rnx-test/SKILL.md +321 -0
- package/skills/rnx-visual/SKILL.md +126 -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 +580 -0
- package/src/app-url.ts +39 -0
- package/src/attached-projects.ts +539 -0
- package/src/auth/shared-session.ts +235 -0
- package/src/backend-origin.ts +70 -0
- package/src/beta.ts +19 -0
- package/src/bridge-constants.ts +175 -0
- package/src/bridge-contract-input.ts +502 -0
- package/src/bridge-contract.ts +558 -0
- package/src/browser-cache.ts +16 -0
- package/src/capture-contract.ts +587 -0
- package/src/cli-constants.ts +1 -0
- package/src/cli-version.ts +40 -0
- package/src/cloud-contract.ts +274 -0
- package/src/cloud.ts +762 -0
- package/src/codex-client.ts +234 -0
- package/src/config.ts +132 -0
- package/src/connect.ts +101 -0
- package/src/dev-bundle-resolution.ts +346 -0
- package/src/dev-server-open.ts +198 -0
- package/src/disk-cleanup.ts +540 -0
- package/src/flow-timeout-scale.ts +15 -0
- package/src/home-paths.ts +806 -0
- package/src/host/agent-host.ts +666 -0
- package/src/host/bridge-host.ts +2788 -0
- package/src/host/fetch-proxy-handler.ts +449 -0
- package/src/host/fetch-proxy-overrides.ts +51 -0
- package/src/host/open-url.ts +89 -0
- package/src/host/plane-host.ts +203 -0
- package/src/host/replacement-module-handler.ts +158 -0
- package/src/host/websocket-proxy.ts +315 -0
- package/src/index.ts +71 -0
- package/src/jump-to-source-babel.ts +135 -0
- package/src/jump-to-source-native.ts +102 -0
- package/src/menu.ts +777 -0
- package/src/metro-fingerprint-registry.ts +302 -0
- package/src/metro-plugin.ts +341 -0
- package/src/metro-production-bundle.ts +471 -0
- package/src/native-dev-bundle-url.ts +118 -0
- package/src/native-seam-manifest.ts +341 -0
- package/src/playwright-host-log.ts +16 -0
- package/src/plist.ts +27 -0
- package/src/poll-until.ts +123 -0
- package/src/profiles.ts +179 -0
- package/src/public-brand.ts +9 -0
- package/src/react-native-host-modules.ts +26 -0
- package/src/render-mode.ts +27 -0
- package/src/rolldown-plugin-swift.ts +240 -0
- package/src/runtime-assets.ts +233 -0
- package/src/runtime-delivery.ts +54 -0
- package/src/screenshots/compose.ts +422 -0
- package/src/screenshots/frame-compose.ts +514 -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 +15 -0
- package/src/settings-bundle.ts +418 -0
- package/src/shell-bridge-identity.ts +15 -0
- package/src/sim-client.ts +697 -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/swift-submit.ts +185 -0
- package/src/swift.ts +7 -0
- package/src/team-machine-prompt.ts +73 -0
- package/src/vite-plugin-one.ts +264 -0
- package/src/vite-plugin-swift.ts +620 -0
- package/src/vite-plugin.ts +1946 -0
- package/src/worklets-babel.ts +250 -0
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
// rnx test — choose Maestro or Detox, support local bundle loopback and standalone cloud execution.
|
|
2
|
+
|
|
3
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
|
|
4
|
+
import { createServer, type Server } from 'node:http'
|
|
5
|
+
import { basename, dirname, extname, join, resolve } from 'node:path'
|
|
6
|
+
import {
|
|
7
|
+
RNX_CLOUD_TEST_FILES_MAX_BYTES,
|
|
8
|
+
RNX_CLOUD_TEST_FILES_MAX_COUNT,
|
|
9
|
+
} from '../../src/cloud-contract'
|
|
10
|
+
import { authHeaderValue, resolveCliAuth } from '../auth'
|
|
11
|
+
import { buildShellUrl, resolveShellBaseUrlForBridgePort } from './control'
|
|
12
|
+
import { hoistLeadingSimFlag } from './flow'
|
|
13
|
+
import { resolveDefaultUploadOrigin } from './upload'
|
|
14
|
+
|
|
15
|
+
export interface RunTestOptions {
|
|
16
|
+
port?: number
|
|
17
|
+
verbose?: boolean
|
|
18
|
+
pollIntervalMs?: number
|
|
19
|
+
timeoutMs?: number
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface TestAppTarget {
|
|
23
|
+
target: string
|
|
24
|
+
close: () => Promise<void>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const VALUE_FLAGS = new Set([
|
|
28
|
+
'--app',
|
|
29
|
+
'--bundle',
|
|
30
|
+
'--bundle-url',
|
|
31
|
+
'--config',
|
|
32
|
+
'--sim',
|
|
33
|
+
'--session',
|
|
34
|
+
'--tab',
|
|
35
|
+
'--port',
|
|
36
|
+
'--device',
|
|
37
|
+
'--driver',
|
|
38
|
+
'--base-url',
|
|
39
|
+
'--url',
|
|
40
|
+
'-t',
|
|
41
|
+
'--testNamePattern',
|
|
42
|
+
'--grep',
|
|
43
|
+
'--maxWorkers',
|
|
44
|
+
'--shard',
|
|
45
|
+
'--outputFile',
|
|
46
|
+
'--origin',
|
|
47
|
+
'--owner',
|
|
48
|
+
'--repo',
|
|
49
|
+
'--poll-interval',
|
|
50
|
+
'--timeout',
|
|
51
|
+
])
|
|
52
|
+
|
|
53
|
+
function valueAfter(args: string[], name: string): string | null {
|
|
54
|
+
const inline = args.find((arg) => arg.startsWith(`${name}=`))
|
|
55
|
+
if (inline) return inline.slice(name.length + 1) || null
|
|
56
|
+
const index = args.indexOf(name)
|
|
57
|
+
if (index < 0) return null
|
|
58
|
+
const value = args[index + 1]
|
|
59
|
+
return value && !value.startsWith('-') ? value : null
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function targetFromArgs(args: string[]): string | null {
|
|
63
|
+
for (let index = 0; index < args.length; index++) {
|
|
64
|
+
const arg = args[index]
|
|
65
|
+
if (VALUE_FLAGS.has(arg)) {
|
|
66
|
+
index += 1
|
|
67
|
+
continue
|
|
68
|
+
}
|
|
69
|
+
if (arg.startsWith('--') && arg.includes('=')) {
|
|
70
|
+
continue
|
|
71
|
+
}
|
|
72
|
+
if (!arg.startsWith('-')) return arg
|
|
73
|
+
}
|
|
74
|
+
return null
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function withoutApp(args: string[]): string[] {
|
|
78
|
+
const result: string[] = []
|
|
79
|
+
for (let i = 0; i < args.length; i++) {
|
|
80
|
+
const arg = args[i]
|
|
81
|
+
if (arg === '--app') {
|
|
82
|
+
i += 1
|
|
83
|
+
continue
|
|
84
|
+
}
|
|
85
|
+
if (arg.startsWith('--app=')) {
|
|
86
|
+
continue
|
|
87
|
+
}
|
|
88
|
+
result.push(arg)
|
|
89
|
+
}
|
|
90
|
+
return result
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function closeServer(server: Server): Promise<void> {
|
|
94
|
+
return new Promise((resolveClose, rejectClose) => {
|
|
95
|
+
server.close((error) => {
|
|
96
|
+
if (error) rejectClose(error)
|
|
97
|
+
else resolveClose()
|
|
98
|
+
})
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function servePrebuiltBundle(path: string): Promise<TestAppTarget> {
|
|
103
|
+
const file = resolve(process.cwd(), path)
|
|
104
|
+
const stat = statSync(file)
|
|
105
|
+
if (!stat.isFile() || extname(file) !== '.js') {
|
|
106
|
+
throw new Error('--app must name a regular Metro .js bundle or a packager target')
|
|
107
|
+
}
|
|
108
|
+
const bundle = readFileSync(file)
|
|
109
|
+
if (bundle.length === 0) throw new Error('--app bundle is empty')
|
|
110
|
+
|
|
111
|
+
const server = createServer((request, response) => {
|
|
112
|
+
if (new URL(request.url || '/', 'http://127.0.0.1').pathname !== '/bundle.js') {
|
|
113
|
+
response.writeHead(404).end()
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
response.writeHead(200, {
|
|
117
|
+
'access-control-allow-origin': '*',
|
|
118
|
+
'cache-control': 'no-store',
|
|
119
|
+
'content-type': 'application/javascript; charset=utf-8',
|
|
120
|
+
'content-length': String(bundle.length),
|
|
121
|
+
})
|
|
122
|
+
response.end(bundle)
|
|
123
|
+
})
|
|
124
|
+
await new Promise<void>((resolveListen, rejectListen) => {
|
|
125
|
+
server.once('error', rejectListen)
|
|
126
|
+
server.listen(0, '127.0.0.1', () => {
|
|
127
|
+
server.off('error', rejectListen)
|
|
128
|
+
resolveListen()
|
|
129
|
+
})
|
|
130
|
+
})
|
|
131
|
+
const address = server.address()
|
|
132
|
+
if (!address || typeof address === 'string') {
|
|
133
|
+
await closeServer(server)
|
|
134
|
+
throw new Error('could not start the local prebuilt-bundle server')
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
target: `http://127.0.0.1:${address.port}/bundle.js`,
|
|
138
|
+
close: () => closeServer(server),
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export async function resolveTestAppTarget(app: string): Promise<TestAppTarget> {
|
|
143
|
+
const localPath = resolve(process.cwd(), app)
|
|
144
|
+
if (existsSync(localPath)) return servePrebuiltBundle(localPath)
|
|
145
|
+
return { target: app, close: async () => {} }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function restoreRnxUrl(previous: string | undefined): void {
|
|
149
|
+
if (previous === undefined) {
|
|
150
|
+
delete process.env.RNX_URL
|
|
151
|
+
} else {
|
|
152
|
+
process.env.RNX_URL = previous
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// dispatch only: each interpreter retains its own selection and execution rules.
|
|
157
|
+
export function resolveTestCommand(args: string[], cwd = process.cwd()) {
|
|
158
|
+
const forwarded = hoistLeadingSimFlag(args)
|
|
159
|
+
const target = forwarded[0]?.startsWith('-') ? undefined : forwarded[0]
|
|
160
|
+
const config = forwarded.some(
|
|
161
|
+
(arg) => arg === '--config' || arg.startsWith('--config='),
|
|
162
|
+
)
|
|
163
|
+
if (config) return { command: 'detox', args: forwarded }
|
|
164
|
+
if (!target) {
|
|
165
|
+
throw new Error(
|
|
166
|
+
'pass a Maestro flow/directory, a Jest suite, or --config <jest-config>',
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
const absolute = resolve(cwd, target)
|
|
170
|
+
if (!existsSync(absolute)) throw new Error(`test target not found: ${target}`)
|
|
171
|
+
const directory = statSync(absolute).isDirectory()
|
|
172
|
+
if (
|
|
173
|
+
!directory &&
|
|
174
|
+
/^(?:jest|rnx-detox)\.config\.[cm]?[jt]s$|^jest\.config\.json$/.test(
|
|
175
|
+
basename(absolute),
|
|
176
|
+
)
|
|
177
|
+
) {
|
|
178
|
+
return { command: 'detox', args: ['--config', target, ...forwarded.slice(1)] }
|
|
179
|
+
}
|
|
180
|
+
const maestro = directory
|
|
181
|
+
? readdirSync(absolute).some((name) => /\.ya?ml$/.test(name)) ||
|
|
182
|
+
['.maestro', 'maestro'].includes(basename(absolute))
|
|
183
|
+
: /\.ya?ml$/.test(extname(absolute))
|
|
184
|
+
if (maestro) {
|
|
185
|
+
return {
|
|
186
|
+
command: 'maestro',
|
|
187
|
+
args: [
|
|
188
|
+
'test',
|
|
189
|
+
...forwarded.map((arg, index) => {
|
|
190
|
+
if (index === 0 && !directory && /^config\.ya?ml$/.test(basename(absolute)))
|
|
191
|
+
return dirname(absolute)
|
|
192
|
+
return arg === '--app' || arg.startsWith('--app=')
|
|
193
|
+
? arg.replace('--app', '--url')
|
|
194
|
+
: arg
|
|
195
|
+
}),
|
|
196
|
+
],
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (!directory && !/\.[cm]?[jt]sx?$/.test(extname(absolute))) {
|
|
200
|
+
throw new Error(`expected Maestro YAML or a Detox/Jest suite: ${target}`)
|
|
201
|
+
}
|
|
202
|
+
return { command: 'detox', args: forwarded }
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function collectTestFiles(
|
|
206
|
+
targetPath: string,
|
|
207
|
+
ignoredPath: string | null,
|
|
208
|
+
): Record<string, string> {
|
|
209
|
+
const files: Record<string, string> = {}
|
|
210
|
+
let sourceBytes = 0
|
|
211
|
+
const add = (path: string, relativePath: string) => {
|
|
212
|
+
if (Object.keys(files).length >= RNX_CLOUD_TEST_FILES_MAX_COUNT) {
|
|
213
|
+
throw new Error(
|
|
214
|
+
`cloud test suites may contain at most ${RNX_CLOUD_TEST_FILES_MAX_COUNT} files`,
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
sourceBytes += statSync(path).size
|
|
218
|
+
if (sourceBytes > RNX_CLOUD_TEST_FILES_MAX_BYTES) {
|
|
219
|
+
throw new Error(
|
|
220
|
+
`cloud test files may contain at most ${RNX_CLOUD_TEST_FILES_MAX_BYTES} bytes`,
|
|
221
|
+
)
|
|
222
|
+
}
|
|
223
|
+
files[relativePath] = readFileSync(path, 'utf8')
|
|
224
|
+
}
|
|
225
|
+
const stat = statSync(targetPath)
|
|
226
|
+
if (stat.isDirectory()) {
|
|
227
|
+
function walk(dir: string, base: string) {
|
|
228
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
229
|
+
if (entry.name.startsWith('.') && entry.name !== '.maestro') continue
|
|
230
|
+
if (['node_modules', 'dist', 'build', '.git'].includes(entry.name)) continue
|
|
231
|
+
const full = join(dir, entry.name)
|
|
232
|
+
if (ignoredPath && resolve(full) === ignoredPath) continue
|
|
233
|
+
const rel = base ? `${base}/${entry.name}` : entry.name
|
|
234
|
+
if (entry.isDirectory()) {
|
|
235
|
+
walk(full, rel)
|
|
236
|
+
} else if (entry.isFile()) {
|
|
237
|
+
add(full, rel)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
walk(targetPath, '')
|
|
242
|
+
} else if (stat.isFile()) {
|
|
243
|
+
add(targetPath, basename(targetPath))
|
|
244
|
+
const parent = dirname(targetPath)
|
|
245
|
+
for (const configName of [
|
|
246
|
+
'rnx-detox.config.cjs',
|
|
247
|
+
'jest.config.cjs',
|
|
248
|
+
'jest.config.js',
|
|
249
|
+
]) {
|
|
250
|
+
const configPath = join(parent, configName)
|
|
251
|
+
if (existsSync(configPath) && !files[configName]) {
|
|
252
|
+
add(configPath, configName)
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
const serializedBytes = Buffer.byteLength(JSON.stringify(files))
|
|
257
|
+
if (serializedBytes > RNX_CLOUD_TEST_FILES_MAX_BYTES) {
|
|
258
|
+
throw new Error(
|
|
259
|
+
`cloud test files serialize to ${serializedBytes} bytes; the limit is ${RNX_CLOUD_TEST_FILES_MAX_BYTES}`,
|
|
260
|
+
)
|
|
261
|
+
}
|
|
262
|
+
return files
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export async function runCloudTest(
|
|
266
|
+
args: string[],
|
|
267
|
+
opts: RunTestOptions = {},
|
|
268
|
+
): Promise<number> {
|
|
269
|
+
const forwarded = args.filter((a) => a !== '--cloud' && a !== '--hosted')
|
|
270
|
+
const originFlag = valueAfter(forwarded, '--origin')
|
|
271
|
+
const origin = await resolveDefaultUploadOrigin(originFlag ?? undefined)
|
|
272
|
+
const app =
|
|
273
|
+
valueAfter(forwarded, '--app') ||
|
|
274
|
+
valueAfter(forwarded, '--bundle') ||
|
|
275
|
+
valueAfter(forwarded, '--bundle-url') ||
|
|
276
|
+
valueAfter(forwarded, '--url') ||
|
|
277
|
+
forwarded.find((a) => a.startsWith('--app='))?.split('=')[1] ||
|
|
278
|
+
forwarded.find((a) => a.startsWith('--bundle='))?.split('=')[1] ||
|
|
279
|
+
forwarded.find((a) => a.startsWith('--url='))?.split('=')[1]
|
|
280
|
+
|
|
281
|
+
const owner = valueAfter(forwarded, '--owner')
|
|
282
|
+
const repo = valueAfter(forwarded, '--repo')
|
|
283
|
+
|
|
284
|
+
let target = targetFromArgs(forwarded)
|
|
285
|
+
const config = valueAfter(forwarded, '--config')
|
|
286
|
+
if (!target && config) {
|
|
287
|
+
target = config
|
|
288
|
+
}
|
|
289
|
+
if (!target) {
|
|
290
|
+
for (const cand of ['.maestro', 'maestro', 'flows', 'e2e']) {
|
|
291
|
+
if (existsSync(resolve(process.cwd(), cand))) {
|
|
292
|
+
target = cand
|
|
293
|
+
break
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (!target) {
|
|
298
|
+
console.error(' usage: rnx test <flow-or-suite> --app <bundle> --cloud')
|
|
299
|
+
return 1
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const targetPath = resolve(process.cwd(), target)
|
|
303
|
+
if (!existsSync(targetPath)) {
|
|
304
|
+
console.error(` error: test target not found: ${target}`)
|
|
305
|
+
return 1
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (!app) {
|
|
309
|
+
console.error(
|
|
310
|
+
' error: rnx test --cloud requires --app <bundle.js|https://bundle-url>',
|
|
311
|
+
)
|
|
312
|
+
return 1
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const auth = resolveCliAuth()
|
|
316
|
+
if (!auth) {
|
|
317
|
+
console.error(' error: rnx test --cloud requires `rnx login` or RNX_API_KEY')
|
|
318
|
+
return 1
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const pollIntervalFlag = valueAfter(forwarded, '--poll-interval')
|
|
322
|
+
const timeoutFlag = valueAfter(forwarded, '--timeout')
|
|
323
|
+
const pollIntervalMs =
|
|
324
|
+
pollIntervalFlag === null ? (opts.pollIntervalMs ?? 1000) : Number(pollIntervalFlag)
|
|
325
|
+
const timeoutMs =
|
|
326
|
+
timeoutFlag === null ? (opts.timeoutMs ?? 20 * 60 * 1000) : Number(timeoutFlag)
|
|
327
|
+
if (!Number.isFinite(pollIntervalMs) || pollIntervalMs < 1) {
|
|
328
|
+
console.error(' error: --poll-interval must be a positive number of milliseconds')
|
|
329
|
+
return 1
|
|
330
|
+
}
|
|
331
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs < 1) {
|
|
332
|
+
console.error(' error: --timeout must be a positive number of milliseconds')
|
|
333
|
+
return 1
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
let bundleUrl: string | undefined
|
|
337
|
+
let bundle: string | undefined
|
|
338
|
+
let bundleShareId: string | undefined
|
|
339
|
+
|
|
340
|
+
if (app.startsWith('http://') || app.startsWith('https://')) {
|
|
341
|
+
bundleUrl = app
|
|
342
|
+
} else if (app.startsWith('srb_') || app.includes('/api/preview/share/bundle')) {
|
|
343
|
+
bundleShareId = app.replace(/^.*id=/, '')
|
|
344
|
+
} else {
|
|
345
|
+
const localBundle = resolve(process.cwd(), app)
|
|
346
|
+
if (!existsSync(localBundle) || !statSync(localBundle).isFile()) {
|
|
347
|
+
console.error(` error: --app bundle file not found: ${app}`)
|
|
348
|
+
return 1
|
|
349
|
+
}
|
|
350
|
+
bundle = readFileSync(localBundle, 'utf8')
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const targetName = basename(targetPath)
|
|
354
|
+
const collectionPath =
|
|
355
|
+
statSync(targetPath).isFile() &&
|
|
356
|
+
(/^(?:jest|rnx-detox)\.config\.[cm]?[jt]s$|^jest\.config\.json$/.test(targetName) ||
|
|
357
|
+
/\.[cm]?[jt]sx?$/.test(extname(targetPath)))
|
|
358
|
+
? dirname(targetPath)
|
|
359
|
+
: targetPath
|
|
360
|
+
const localBundlePath =
|
|
361
|
+
app.startsWith('http://') ||
|
|
362
|
+
app.startsWith('https://') ||
|
|
363
|
+
app.startsWith('srb_') ||
|
|
364
|
+
app.includes('/api/preview/share/bundle')
|
|
365
|
+
? null
|
|
366
|
+
: resolve(process.cwd(), app)
|
|
367
|
+
const testFiles = collectTestFiles(collectionPath, localBundlePath)
|
|
368
|
+
if (Object.keys(testFiles).length === 0) {
|
|
369
|
+
console.error(` error: no test files found at ${target}`)
|
|
370
|
+
return 1
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const submitPayload: Record<string, unknown> = {
|
|
374
|
+
kind: 'test',
|
|
375
|
+
testFiles,
|
|
376
|
+
}
|
|
377
|
+
if (bundleUrl) submitPayload.bundleUrl = bundleUrl
|
|
378
|
+
if (bundle) submitPayload.bundle = bundle
|
|
379
|
+
if (bundleShareId) submitPayload.bundleShareId = bundleShareId
|
|
380
|
+
if (owner) submitPayload.owner = owner
|
|
381
|
+
if (repo) submitPayload.repo = repo
|
|
382
|
+
|
|
383
|
+
const endpoint = `${origin.replace(/\/$/, '')}/api/sootsim/runner/jobs`
|
|
384
|
+
let response: Response
|
|
385
|
+
try {
|
|
386
|
+
response = await fetch(endpoint, {
|
|
387
|
+
method: 'POST',
|
|
388
|
+
headers: {
|
|
389
|
+
'content-type': 'application/json',
|
|
390
|
+
authorization: authHeaderValue(auth),
|
|
391
|
+
},
|
|
392
|
+
body: JSON.stringify(submitPayload),
|
|
393
|
+
})
|
|
394
|
+
} catch (err) {
|
|
395
|
+
console.error(
|
|
396
|
+
` error: could not connect to ${endpoint}: ${err instanceof Error ? err.message : String(err)}`,
|
|
397
|
+
)
|
|
398
|
+
return 1
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (!response.ok) {
|
|
402
|
+
const body = await response.text()
|
|
403
|
+
let message = body
|
|
404
|
+
try {
|
|
405
|
+
const json = JSON.parse(body)
|
|
406
|
+
if (json.message) message = json.message
|
|
407
|
+
} catch {}
|
|
408
|
+
console.error(
|
|
409
|
+
` error: cloud test submission failed (${response.status}): ${message}`,
|
|
410
|
+
)
|
|
411
|
+
return 1
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const { id } = (await response.json()) as { id: string }
|
|
415
|
+
console.log(` cloud test job submitted: ${id}`)
|
|
416
|
+
|
|
417
|
+
const pollUrl = `${origin.replace(/\/$/, '')}/api/sootsim/runner/jobs?id=${encodeURIComponent(id)}`
|
|
418
|
+
const startedAt = Date.now()
|
|
419
|
+
|
|
420
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
421
|
+
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs))
|
|
422
|
+
let pollRes: Response
|
|
423
|
+
try {
|
|
424
|
+
pollRes = await fetch(pollUrl, {
|
|
425
|
+
headers: { authorization: authHeaderValue(auth) },
|
|
426
|
+
})
|
|
427
|
+
} catch (err) {
|
|
428
|
+
console.warn(
|
|
429
|
+
` warn: error polling job: ${err instanceof Error ? err.message : String(err)}`,
|
|
430
|
+
)
|
|
431
|
+
continue
|
|
432
|
+
}
|
|
433
|
+
if (!pollRes.ok) {
|
|
434
|
+
console.warn(` warn: polling error: HTTP ${pollRes.status}`)
|
|
435
|
+
continue
|
|
436
|
+
}
|
|
437
|
+
const job = (await pollRes.json()) as {
|
|
438
|
+
id: string
|
|
439
|
+
status: string
|
|
440
|
+
result?: {
|
|
441
|
+
previewId?: string | null
|
|
442
|
+
previewUrl?: string | null
|
|
443
|
+
runId?: string | null
|
|
444
|
+
exitCode?: number | null
|
|
445
|
+
failureReason?: string | null
|
|
446
|
+
} | null
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (job.status === 'done' || job.status === 'failed' || job.status === 'expired') {
|
|
450
|
+
const result = job.result
|
|
451
|
+
// a job the recorder failed after a passing suite (missing recording or
|
|
452
|
+
// registration) is still a failed run.
|
|
453
|
+
const exitCode =
|
|
454
|
+
job.status === 'done' ? (result?.exitCode ?? 0) : result?.exitCode || 1
|
|
455
|
+
console.log(` status: ${job.status}`)
|
|
456
|
+
console.log(` exit code: ${exitCode}`)
|
|
457
|
+
if (result?.previewUrl) {
|
|
458
|
+
console.log(` preview: ${result.previewUrl}`)
|
|
459
|
+
} else if (result?.previewId) {
|
|
460
|
+
console.log(
|
|
461
|
+
` preview: ${origin.replace(/\/$/, '')}/preview/${result.previewId}`,
|
|
462
|
+
)
|
|
463
|
+
}
|
|
464
|
+
if (result?.runId) {
|
|
465
|
+
console.log(` run: ${result.runId}`)
|
|
466
|
+
}
|
|
467
|
+
if (result?.failureReason) {
|
|
468
|
+
console.error(` failure: ${result.failureReason}`)
|
|
469
|
+
}
|
|
470
|
+
return exitCode
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
console.error(` error: cloud test timed out after ${Math.round(timeoutMs / 1000)}s`)
|
|
475
|
+
return 1
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
export async function runTest(
|
|
479
|
+
args: string[],
|
|
480
|
+
opts: RunTestOptions = {},
|
|
481
|
+
): Promise<number> {
|
|
482
|
+
if (args.includes('--cloud') || args.includes('--hosted')) {
|
|
483
|
+
return runCloudTest(args, opts)
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const selected = resolveTestCommand(args)
|
|
487
|
+
const app = valueAfter(args, '--app')
|
|
488
|
+
const appTarget = app
|
|
489
|
+
? await resolveTestAppTarget(app)
|
|
490
|
+
: { target: '', close: async () => {} }
|
|
491
|
+
|
|
492
|
+
try {
|
|
493
|
+
if (selected.command === 'maestro') {
|
|
494
|
+
const { flowTimeoutScale } = await import('../../src/flow-timeout-scale')
|
|
495
|
+
if (flowTimeoutScale() !== 1) {
|
|
496
|
+
throw new Error(
|
|
497
|
+
'rnx test requires original flow timeouts; unset SOOTSIM_FLOW_TIMEOUT_SCALE',
|
|
498
|
+
)
|
|
499
|
+
}
|
|
500
|
+
const { runMaestro } = await import('./maestro')
|
|
501
|
+
const maestroArgs = appTarget.target
|
|
502
|
+
? selected.args.map((arg, idx) => {
|
|
503
|
+
const prev = selected.args[idx - 1]
|
|
504
|
+
if (prev === '--url') return appTarget.target
|
|
505
|
+
if (arg.startsWith('--url=')) return `--url=${appTarget.target}`
|
|
506
|
+
return arg
|
|
507
|
+
})
|
|
508
|
+
: selected.args
|
|
509
|
+
const code = await runMaestro(maestroArgs, opts)
|
|
510
|
+
return typeof code === 'number' ? code : 0
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const { runDetox } = await import('./detox')
|
|
514
|
+
const detoxArgs = withoutApp(selected.args)
|
|
515
|
+
const previousRnxUrl = process.env.RNX_URL
|
|
516
|
+
if (appTarget.target) {
|
|
517
|
+
process.env.RNX_URL = await buildShellUrl(
|
|
518
|
+
appTarget.target,
|
|
519
|
+
resolveShellBaseUrlForBridgePort(
|
|
520
|
+
opts.port ?? (Number(process.env.SOOTSIM_PORT) || 5173),
|
|
521
|
+
),
|
|
522
|
+
)
|
|
523
|
+
}
|
|
524
|
+
try {
|
|
525
|
+
await runDetox(detoxArgs, opts)
|
|
526
|
+
return 0
|
|
527
|
+
} finally {
|
|
528
|
+
if (appTarget.target) {
|
|
529
|
+
restoreRnxUrl(previousRnxUrl)
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
} finally {
|
|
533
|
+
await appTarget.close()
|
|
534
|
+
}
|
|
535
|
+
}
|