rnxsim 0.1.499 → 0.1.501

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.
Files changed (71) hide show
  1. package/README.md +1 -1
  2. package/cli/bridge-flow-runner.ts +6 -2
  3. package/cli/cloud-client.ts +90 -7
  4. package/cli/cloud-dispatch.ts +27 -27
  5. package/cli/cloud-session.ts +61 -6
  6. package/cli/commands/control.ts +33 -31
  7. package/cli/commands/detox.ts +157 -159
  8. package/cli/commands/inspect/core.ts +2 -0
  9. package/cli/commands/platform.ts +170 -48
  10. package/cli/commands/preview.ts +1 -1
  11. package/cli/commands/test.ts +403 -95
  12. package/cli/main.ts +11 -0
  13. package/cli/outbound-endpoints.ts +10 -0
  14. package/cli/ws-bridge.ts +18 -19
  15. package/detox/jest-preset.cjs +1 -6
  16. package/dist-lib/agent-daemon-client.cjs +1 -1
  17. package/dist-lib/agent-events.cjs +1 -1
  18. package/dist-lib/agent-identity.cjs +1 -1
  19. package/dist-lib/agent-sessions.cjs +1 -1
  20. package/dist-lib/attached-projects.cjs +1 -1
  21. package/dist-lib/auth/shared-session.cjs +1 -1
  22. package/dist-lib/backend-origin.cjs +1 -1
  23. package/dist-lib/beta.cjs +1 -1
  24. package/dist-lib/beta.mjs +1 -1
  25. package/dist-lib/bridge-constants.cjs +1 -1
  26. package/dist-lib/bridge-contract-input.cjs +1 -1
  27. package/dist-lib/bridge-contract-input.mjs +1 -1
  28. package/dist-lib/bridge-contract.cjs +1 -1
  29. package/dist-lib/bridge-contract.mjs +1 -1
  30. package/dist-lib/capture-contract.cjs +1 -1
  31. package/dist-lib/capture-contract.mjs +1 -1
  32. package/dist-lib/cli-constants.cjs +1 -1
  33. package/dist-lib/cloud-contract.cjs +7 -1
  34. package/dist-lib/cloud-contract.mjs +5 -1
  35. package/dist-lib/cloud.cjs +1 -1
  36. package/dist-lib/cloud.mjs +1 -1
  37. package/dist-lib/config.cjs +1 -1
  38. package/dist-lib/detox/index.cjs +1 -1
  39. package/dist-lib/detox/jest-preset.cjs +1 -6
  40. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  41. package/dist-lib/home-paths.cjs +1 -1
  42. package/dist-lib/host/bridge-host.cjs +54 -29
  43. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  44. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  45. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  46. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  47. package/dist-lib/host/websocket-proxy.cjs +1 -1
  48. package/dist-lib/index.cjs +52 -22
  49. package/dist-lib/jump-to-source-babel.cjs +1 -1
  50. package/dist-lib/jump-to-source-native.cjs +1 -1
  51. package/dist-lib/menu.cjs +1 -1
  52. package/dist-lib/menu.mjs +1 -1
  53. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  54. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  55. package/dist-lib/metro-production-bundle.cjs +1 -1
  56. package/dist-lib/metro-production-bundle.mjs +1 -1
  57. package/dist-lib/metro.cjs +1 -1
  58. package/dist-lib/profiles.cjs +1 -1
  59. package/dist-lib/public-brand.cjs +1 -1
  60. package/dist-lib/react-native-host-modules.cjs +1 -1
  61. package/dist-lib/react-native-host-modules.mjs +1 -1
  62. package/dist-lib/render-mode.cjs +1 -1
  63. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  64. package/dist-lib/sdk.cjs +7 -8
  65. package/dist-lib/sdk.mjs +7 -8
  66. package/dist-lib/skills.cjs +128 -63
  67. package/dist-lib/vite.cjs +1 -1
  68. package/package.json +1 -1
  69. package/skills/rnx-test/SKILL.md +2 -2
  70. package/src/cloud-contract.ts +2 -0
  71. package/src/sim-client.ts +8 -10
@@ -1,14 +1,7 @@
1
1
  // rnx detox — run existing detox suites against rnx
2
2
  //
3
- // the command is a thin orchestration layer over `npx jest`:
4
- // 1. resolves a test dir (cwd e2e|test/e2e|detox, or the flag)
5
- // 2. makes sure an rnx shell is reachable (auto-launches one if not)
6
- // 3. spawns jest with our preset baked in so `import ... from 'detox'`
7
- // resolves to the rnx driver
8
- //
9
- // it deliberately shells out to the user's own jest so their jest version,
10
- // transforms, and setupFiles keep working. the only thing we force is the
11
- // moduleNameMapper for `^detox$`.
3
+ // use the customer's jest config unchanged. the rnx preset maps the detox
4
+ // driver and owns browser cleanup; jest owns setup, selection and deadlines.
12
5
 
13
6
  import { spawn } from 'child_process'
14
7
  import {
@@ -20,27 +13,17 @@ import {
20
13
  unlinkSync,
21
14
  } from 'fs'
22
15
  import { createRequire } from 'module'
16
+ import { fileURLToPath } from 'node:url'
23
17
  import { tmpdir } from 'os'
24
18
  import { dirname, resolve, join, relative } from 'path'
25
19
  import { rnxPublicBrand } from '../../src/public-brand'
26
20
  import { resolveCliAuth } from '../auth'
27
21
  import { registerRun, shouldRegisterRun } from '../run-registry'
28
22
  import { rnxExit } from '../run-rnx'
23
+ import { buildShellUrl } from './control'
29
24
  import { discoverSootsimUrl } from './flow'
30
25
  import { resolveDefaultUploadOrigin } from './upload'
31
26
 
32
- // resolve the detox preset directory via the package's own subpath export.
33
- function resolveDetoxDir(): string {
34
- try {
35
- const entry = import.meta.resolve(`${rnxPublicBrand.packageName}/detox`)
36
- // import.meta.resolve returns a file:// URL — convert to a path
37
- return dirname(entry.startsWith('file://') ? entry.slice(7) : entry)
38
- } catch {
39
- // fallback for source-layout dev (no compiled package.json exports)
40
- return resolve(import.meta.dirname!, '..', '..', 'detox')
41
- }
42
- }
43
-
44
27
  interface RunDetoxOpts {
45
28
  port?: number
46
29
  verbose?: boolean
@@ -73,62 +56,73 @@ export async function runDetox(args: string[], opts: RunDetoxOpts = {}): Promise
73
56
  }
74
57
 
75
58
  const getFlag = (name: string) => {
59
+ const inline = args.find((arg) => arg.startsWith(`${name}=`))
60
+ if (inline) return inline.slice(name.length + 1)
76
61
  const i = args.indexOf(name)
77
62
  return i >= 0 ? args[i + 1] : undefined
78
63
  }
79
64
  const hasFlag = (name: string) => args.includes(name)
80
65
 
81
- const watch = hasFlag('--watch')
66
+ const watch = hasFlag('--watch') || hasFlag('--watchAll')
82
67
  const headed = hasFlag('--headed')
83
68
  const noLaunch = hasFlag('--no-launch')
84
69
  const configFlag = getFlag('--config')
85
- const port = opts.port || Number(process.env.SOOTSIM_PORT) || 5173
86
-
87
- // positional args: test file paths passed directly to jest
88
- const positional = args.filter((a, i) => {
89
- if (a.startsWith('--') || a === '-t' || a === '-h') return false
90
- const prev = args[i - 1]
91
- if (
92
- prev === '--config' ||
93
- prev === '-t' ||
94
- prev === '--testNamePattern' ||
95
- prev === '--grep' ||
96
- prev === '--outputFile' ||
97
- prev === '--maxWorkers' ||
98
- prev === '--shard'
70
+ const port =
71
+ Number(getFlag('--port')) || opts.port || Number(process.env.SOOTSIM_PORT) || 5173
72
+ if (getFlag('--sim')) {
73
+ throw new Error(
74
+ 'Detox owns its browser context; pin its shell with RNX_URL and --device instead of --sim',
99
75
  )
100
- return false
101
- return true
102
- })
103
- const passthroughJestFlags = args.flatMap((arg, index) => {
104
- if (
105
- arg === '--runInBand' ||
106
- arg.startsWith('--maxWorkers=') ||
107
- arg.startsWith('--shard=')
108
- ) {
109
- return [arg]
110
- }
111
- if ((arg === '--maxWorkers' || arg === '--shard') && args[index + 1]) {
112
- return [arg, args[index + 1]]
76
+ }
77
+
78
+ // consume only rnx options. jest must receive its complete argv, including
79
+ // selection, timeouts, reporters, coverage, and options added by future jest versions.
80
+ const jestArgs: string[] = ['jest']
81
+ for (let index = 0; index < args.length; index++) {
82
+ const arg = args[index]
83
+ if (arg === '--headed' || arg === '--no-launch') continue
84
+ const name = arg.split('=', 1)[0]
85
+ if (['--config', '--app', '--device', '--port'].includes(name)) {
86
+ if (!arg.includes('=')) {
87
+ if (!args[index + 1] || args[index + 1].startsWith('-')) {
88
+ throw new Error(`${name} requires a value`)
89
+ }
90
+ index++
91
+ }
92
+ continue
113
93
  }
114
- return []
115
- })
94
+ jestArgs.push(arg === '--grep' ? '--testNamePattern' : arg)
95
+ }
96
+
97
+ let localConfig =
98
+ configFlag ||
99
+ firstExisting([
100
+ 'rnx-detox.config.cjs',
101
+ 'jest.config.ts',
102
+ 'jest.config.mts',
103
+ 'jest.config.mjs',
104
+ 'jest.config.cjs',
105
+ 'jest.config.js',
106
+ 'jest.config.cts',
107
+ 'jest.config.json',
108
+ ])
109
+ if (!localConfig && existsSync('package.json')) {
110
+ const pkg = JSON.parse(readFileSync('package.json', 'utf8'))
111
+ if (pkg.jest) localConfig = 'package.json'
112
+ }
116
113
 
117
- // test dir discovery. an explicit suite owns its root; otherwise external
118
- // suites set RNX_TEST_DIR and conventional locations remain the fallback.
114
+ // test dir discovery. external test suites set RNX_TEST_DIR; otherwise
115
+ // we auto-detect the conventional locations.
119
116
  const detoxDirs = ['e2e', 'test/e2e', 'detox']
120
- const explicitTarget =
121
- positional[0] && existsSync(resolve(process.cwd(), positional[0]))
122
- ? resolve(process.cwd(), positional[0])
123
- : null
124
- let testDir: string | null = explicitTarget
125
- ? statSync(explicitTarget).isDirectory()
126
- ? explicitTarget
127
- : dirname(explicitTarget)
128
- : process.env.RNX_TEST_DIR
129
- ? resolve(process.cwd(), process.env.RNX_TEST_DIR)
130
- : null
131
- if (!testDir) {
117
+ let testDir: string | null = process.env.RNX_TEST_DIR
118
+ ? resolve(process.cwd(), process.env.RNX_TEST_DIR)
119
+ : null
120
+ if (!testDir && !localConfig && args[0] && !args[0].startsWith('-')) {
121
+ const target = resolve(args[0])
122
+ if (existsSync(target))
123
+ testDir = statSync(target).isDirectory() ? target : dirname(target)
124
+ }
125
+ if (!testDir && !localConfig) {
132
126
  for (const dir of detoxDirs) {
133
127
  const full = resolve(process.cwd(), dir)
134
128
  if (existsSync(full)) {
@@ -137,13 +131,14 @@ export async function runDetox(args: string[], opts: RunDetoxOpts = {}): Promise
137
131
  }
138
132
  }
139
133
  }
140
- if (!testDir) {
134
+ if (!testDir && !localConfig) {
141
135
  console.error(
142
136
  ` error: no detox tests found. expected one of: ${detoxDirs.join(', ')}\n` +
143
137
  ` run 'rnx detox init' to scaffold a starter suite.`,
144
138
  )
145
139
  rnxExit(1)
146
140
  }
141
+ testDir ??= process.cwd()
147
142
 
148
143
  // auto-launch: make sure an rnx shell is reachable before running jest.
149
144
  // Reuse the Maestro discovery path: scan local dev servers.
@@ -152,52 +147,40 @@ export async function runDetox(args: string[], opts: RunDetoxOpts = {}): Promise
152
147
  await ensureShellRunning(port)
153
148
  }
154
149
 
155
- // build the jest command. customer config remains authoritative for setup,
156
- // transforms, roots, and timeouts; the generated wrapper adds only RNX's
157
- // Detox module mapping and cleanup hook.
158
- const jestArgs: string[] = ['jest']
159
- const localConfig =
160
- configFlag ||
161
- firstExisting(['rnx-detox.config.cjs', 'jest.config.cjs', 'jest.config.js'])
162
-
150
+ // build the jest command. prefer the user's config if they passed --config
151
+ // or if a rnx-detox.config.cjs exists in cwd. otherwise write a temporary
152
+ // config that loads our preset.
163
153
  let tempConfig: string | null = null
164
- const presetPath = resolve(resolveDetoxDir(), 'jest-preset.cjs')
165
154
  if (localConfig) {
166
- const localConfigPath = resolve(process.cwd(), localConfig)
167
- // Keep the wrapper beside the customer config so a relative rootDir keeps
168
- // resolving from the same directory Jest would have used directly.
169
- tempConfig = join(dirname(localConfigPath), `rnx-detox-${process.pid}.config.cjs`)
170
- writeFileSync(
171
- tempConfig,
172
- `const rnx = require(${JSON.stringify(presetPath)})\n` +
173
- `const customer = require(${JSON.stringify(localConfigPath)})\n` +
174
- `module.exports = {\n` +
175
- ` ...rnx,\n` +
176
- ` ...customer,\n` +
177
- ` moduleNameMapper: { ...rnx.moduleNameMapper, ...customer.moduleNameMapper, '^detox$': rnx.moduleNameMapper['^detox$'] },\n` +
178
- ` setupFilesAfterEnv: [...new Set([...(customer.setupFilesAfterEnv || []), ...(rnx.setupFilesAfterEnv || [])])],\n` +
179
- `}\n`,
155
+ jestArgs.push(
156
+ '--config',
157
+ localConfig.startsWith('{') ? localConfig : resolve(process.cwd(), localConfig),
180
158
  )
181
- jestArgs.push('--config', tempConfig)
182
159
  } else {
183
160
  // no local config — write a temp config that pulls in our preset.
184
161
  // jest's --preset flag has quirky module resolution that breaks with
185
162
  // subpath exports, so we generate a real config file instead.
186
163
  // scope roots + testMatch to the discovered test dir so jest doesn't
187
164
  // scan the entire repo (which causes haste collisions in monorepos).
165
+ const presetPath = fileURLToPath(
166
+ import.meta.resolve(`${rnxPublicBrand.packageName}/detox/jest-preset`),
167
+ )
188
168
  // an external suite may optionally declare its own ignore patterns via e2e/jest-ignore.cjs
189
169
  const suiteIgnoreFile = testDir ? join(testDir, 'e2e', 'jest-ignore.cjs') : null
190
170
  let suiteIgnorePatterns: string[] = []
191
171
  if (suiteIgnoreFile && existsSync(suiteIgnoreFile)) {
192
- try {
193
- const req = createRequire(import.meta.url)
194
- const exported = req(suiteIgnoreFile)
195
- suiteIgnorePatterns = Array.isArray(exported)
196
- ? exported
197
- : Array.isArray(exported.testPathIgnorePatterns)
198
- ? exported.testPathIgnorePatterns
199
- : []
200
- } catch {}
172
+ const req = createRequire(import.meta.url)
173
+ const exported = req(suiteIgnoreFile)
174
+ const patterns = Array.isArray(exported)
175
+ ? exported
176
+ : exported.testPathIgnorePatterns
177
+ if (
178
+ !Array.isArray(patterns) ||
179
+ !patterns.every((pattern) => typeof pattern === 'string')
180
+ ) {
181
+ throw new Error(`${suiteIgnoreFile} must export string ignore patterns`)
182
+ }
183
+ suiteIgnorePatterns = patterns
201
184
  }
202
185
  tempConfig = join(tmpdir(), `rnx-detox-${process.pid}.config.cjs`)
203
186
  const cfg = {
@@ -215,36 +198,33 @@ export async function runDetox(args: string[], opts: RunDetoxOpts = {}): Promise
215
198
  jestArgs.push('--config', tempConfig)
216
199
  }
217
200
 
218
- if (watch) jestArgs.push('--watch')
219
-
220
- const testNamePattern =
221
- getFlag('-t') || getFlag('--testNamePattern') || getFlag('--grep')
222
- if (testNamePattern) jestArgs.push('-t', testNamePattern)
223
-
224
201
  // hosted run registration (org dashboard pass/fail row). when it applies,
225
202
  // capture jest's structured results via --json --outputFile so the row can
226
203
  // carry test counts + the first failure; console output stays untouched.
227
204
  const auth = resolveCliAuth()
228
205
  const wantsRegistration = !watch && shouldRegisterRun({ uploadedShare: false, auth })
229
- let resultsFile: string | null = process.env.SOOTSIM_JEST_OUTPUT_FILE || null
230
- if (process.env.SOOTSIM_JEST_JSON === '1') jestArgs.push('--json')
231
- if (process.env.SOOTSIM_JEST_OUTPUT_FILE) {
206
+ let resultsFile: string | null =
207
+ getFlag('--outputFile') || process.env.SOOTSIM_JEST_OUTPUT_FILE || null
208
+ if ((process.env.SOOTSIM_JEST_JSON === '1' || wantsRegistration) && !hasFlag('--json'))
209
+ jestArgs.push('--json')
210
+ if (!getFlag('--outputFile') && process.env.SOOTSIM_JEST_OUTPUT_FILE) {
232
211
  jestArgs.push('--outputFile', process.env.SOOTSIM_JEST_OUTPUT_FILE)
233
- } else if (wantsRegistration) {
212
+ } else if (!resultsFile && wantsRegistration) {
234
213
  resultsFile = join(tmpdir(), `rnx-detox-results-${process.pid}.json`)
235
- jestArgs.push('--json', '--outputFile', resultsFile)
214
+ jestArgs.push('--outputFile', resultsFile)
236
215
  }
237
216
 
238
- jestArgs.push(...passthroughJestFlags)
239
-
240
- // any positional test files get passed straight through
241
- jestArgs.push(...positional)
242
-
243
217
  console.log(` rnx detox`)
244
218
  console.log(` test dir: ${testDir}`)
245
219
  console.log(` port: ${port}${headed ? ' (headed)' : ''}`)
246
220
  if (localConfig) console.log(` config: ${localConfig}`)
247
221
 
222
+ const shellBaseUrl = process.env.RNX_URL || `http://localhost:${port}`
223
+ const app = getFlag('--app')
224
+ const shellUrl = new URL(app ? await buildShellUrl(app, shellBaseUrl) : shellBaseUrl)
225
+ const device = getFlag('--device')
226
+ if (device) shellUrl.searchParams.set('device', device)
227
+
248
228
  // proof capture runs under the repo's pinned node runtime. its v8 collector
249
229
  // crashes in ClearStaleLeftTrimmedPointerVisitor when Sparkplug baseline
250
230
  // compilation (Builtins_BaselineOutOfLinePrologue) pushes untagged register
@@ -273,7 +253,7 @@ export async function runDetox(args: string[], opts: RunDetoxOpts = {}): Promise
273
253
  RNX_TEST_DIR: testDir,
274
254
  SOOTSIM_PORT: String(port),
275
255
  SOOTSIM_HEADED: headed ? '1' : '',
276
- RNX_URL: process.env.RNX_URL || `http://localhost:${port}`,
256
+ RNX_URL: shellUrl.toString(),
277
257
  },
278
258
  })
279
259
 
@@ -292,26 +272,30 @@ export async function runDetox(args: string[], opts: RunDetoxOpts = {}): Promise
292
272
  process.on('SIGINT', onInt)
293
273
 
294
274
  const startedAt = Date.now()
295
- const code = await new Promise<number>((resolveCode, reject) => {
296
- child.once('error', reject)
297
- child.once('exit', (exitCode, signal) => {
298
- if (signal) {
299
- console.error(` error: jest terminated by ${signal}`)
300
- resolveCode(1)
301
- return
302
- }
303
- resolveCode(exitCode ?? 1)
275
+ let code: number
276
+ try {
277
+ code = await new Promise<number>((resolveCode, reject) => {
278
+ child.once('error', reject)
279
+ child.once('exit', (exitCode, signal) => {
280
+ if (signal) {
281
+ console.error(` error: jest terminated by ${signal}`)
282
+ resolveCode(1)
283
+ return
284
+ }
285
+ resolveCode(exitCode ?? 1)
286
+ })
304
287
  })
305
- })
306
- process.off('SIGTERM', onTerm)
307
- process.off('SIGINT', onInt)
308
- if (tempConfig)
309
- try {
310
- unlinkSync(tempConfig)
311
- } catch {}
288
+ } finally {
289
+ process.off('SIGTERM', onTerm)
290
+ process.off('SIGINT', onInt)
291
+ if (tempConfig) unlinkSync(tempConfig)
292
+ }
312
293
 
313
294
  if (wantsRegistration) {
314
- const results = resultsFile ? readJestResults(resultsFile) : null
295
+ const results =
296
+ resultsFile && existsSync(resultsFile) && statSync(resultsFile).mtimeMs >= startedAt
297
+ ? readJestResults(resultsFile)
298
+ : null
315
299
  const origin = await resolveDefaultUploadOrigin()
316
300
  const run = await registerRun({
317
301
  origin,
@@ -330,11 +314,20 @@ export async function runDetox(args: string[], opts: RunDetoxOpts = {}): Promise
330
314
  auth,
331
315
  })
332
316
  if (run) console.log(` run: ${run.id}`)
317
+ else rnxExit(code || 1)
318
+ }
319
+ if (
320
+ resultsFile &&
321
+ existsSync(resultsFile) &&
322
+ !getFlag('--outputFile') &&
323
+ !process.env.SOOTSIM_JEST_OUTPUT_FILE
324
+ ) {
325
+ unlinkSync(resultsFile)
333
326
  }
334
327
  rnxExit(code)
335
328
  }
336
329
 
337
- type JestRunSummary = {
330
+ export type JestRunSummary = {
338
331
  numTotalTests: number
339
332
  numPassedTests: number
340
333
  firstFailure: string | null
@@ -344,47 +337,52 @@ type JestRunSummary = {
344
337
  // parse jest's --json output file down to the counts + first failure the
345
338
  // hosted run row carries. tolerant: any parse problem returns null and the
346
339
  // run registers with exit-code status only.
347
- function readJestResults(file: string): JestRunSummary | null {
340
+ export function readJestResults(file: string): JestRunSummary | null {
348
341
  try {
349
- const parsed = JSON.parse(readFileSync(file, 'utf8')) as {
350
- numTotalTests?: number
351
- numPassedTests?: number
352
- testResults?: Array<{
353
- testResults?: Array<{
354
- status?: string
355
- fullName?: string
356
- failureMessages?: string[]
357
- }>
358
- }>
359
- }
342
+ const parsed: unknown = JSON.parse(readFileSync(file, 'utf8'))
343
+ if (
344
+ !parsed ||
345
+ typeof parsed !== 'object' ||
346
+ !('numTotalTests' in parsed) ||
347
+ typeof parsed.numTotalTests !== 'number' ||
348
+ !('numPassedTests' in parsed) ||
349
+ typeof parsed.numPassedTests !== 'number' ||
350
+ !('testResults' in parsed) ||
351
+ !Array.isArray(parsed.testResults)
352
+ )
353
+ return null
360
354
  let firstFailure: string | null = null
361
355
  let firstFailedIndex: number | null = null
362
356
  let index = 0
363
- for (const suite of parsed.testResults ?? []) {
364
- for (const test of suite.testResults ?? []) {
357
+ for (const suite of parsed.testResults) {
358
+ if (!suite || typeof suite !== 'object' || !Array.isArray(suite.assertionResults))
359
+ return null
360
+ for (const test of suite.assertionResults) {
361
+ if (!test || typeof test !== 'object') return null
365
362
  if (firstFailedIndex == null && test.status === 'failed') {
366
363
  firstFailedIndex = index
367
- const message = test.failureMessages?.[0]?.split('\n')[0]
368
- firstFailure = [test.fullName, message].filter(Boolean).join(': ') || null
364
+ const messages: unknown[] = Array.isArray(test.failureMessages)
365
+ ? test.failureMessages
366
+ : []
367
+ const message = messages
368
+ .filter((value): value is string => typeof value === 'string')
369
+ .join('\n')
370
+ firstFailure =
371
+ [typeof test.fullName === 'string' ? test.fullName : '', message]
372
+ .filter(Boolean)
373
+ .join(': ') || null
369
374
  }
370
375
  index += 1
371
376
  }
372
377
  }
373
- if (typeof parsed.numTotalTests !== 'number') return null
374
378
  return {
375
379
  numTotalTests: parsed.numTotalTests,
376
- numPassedTests: parsed.numPassedTests ?? 0,
380
+ numPassedTests: parsed.numPassedTests,
377
381
  firstFailure,
378
382
  firstFailedIndex,
379
383
  }
380
384
  } catch {
381
385
  return null
382
- } finally {
383
- if (!process.env.SOOTSIM_JEST_OUTPUT_FILE) {
384
- try {
385
- unlinkSync(file)
386
- } catch {}
387
- }
388
386
  }
389
387
  }
390
388
 
@@ -419,7 +417,7 @@ function initDetoxProject(): void {
419
417
 
420
418
  /** @type {import('@jest/types').Config.InitialOptions} */
421
419
  module.exports = {
422
- preset: 'rnxsim/detox/jest-preset',
420
+ preset: 'rnxsim/detox',
423
421
  rootDir: __dirname,
424
422
  testMatch: ['<rootDir>/e2e/**/*.test.ts', '<rootDir>/e2e/**/*.test.js'],
425
423
  }
@@ -1025,6 +1025,7 @@ export interface WaitReadyStatus {
1025
1025
  export interface WaitReadyOptions {
1026
1026
  progressIntervalMs?: number
1027
1027
  onProgress?: (status: WaitReadyStatus) => void
1028
+ simId?: string
1028
1029
  }
1029
1030
 
1030
1031
  export function waitReadyReason(
@@ -1091,6 +1092,7 @@ export async function inspectWaitReady(
1091
1092
  result = await bridge.send(
1092
1093
  {
1093
1094
  type: 'waitFor',
1095
+ ...(options.simId ? { simId: options.simId } : {}),
1094
1096
  waitForOptions: { condition: { type: 'ready' }, timeoutMs },
1095
1097
  },
1096
1098
  { timeoutMs: timeoutMs + 1_000 },