@rpcbase/test 0.308.0 → 0.310.0

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 (81) hide show
  1. package/dist/clearDatabase.d.ts +2 -0
  2. package/dist/clearDatabase.d.ts.map +1 -0
  3. package/dist/clearDatabase.js +12 -0
  4. package/dist/clearDatabase.js.map +1 -0
  5. package/dist/cli.d.ts +2 -0
  6. package/dist/cli.d.ts.map +1 -0
  7. package/dist/cli.js +785 -0
  8. package/dist/cli.js.map +1 -0
  9. package/dist/coverage/collect.d.ts +5 -0
  10. package/dist/coverage/collect.d.ts.map +1 -0
  11. package/dist/coverage/collect.js +113 -0
  12. package/dist/coverage/collect.js.map +1 -0
  13. package/dist/coverage/config-loader.d.ts +11 -0
  14. package/dist/coverage/config-loader.d.ts.map +1 -0
  15. package/dist/coverage/config-loader.js +292 -0
  16. package/dist/coverage/config-loader.js.map +1 -0
  17. package/dist/coverage/config.d.ts +3 -0
  18. package/dist/coverage/config.d.ts.map +1 -0
  19. package/dist/coverage/config.js +110 -0
  20. package/dist/coverage/config.js.map +1 -0
  21. package/dist/coverage/files.d.ts +4 -0
  22. package/dist/coverage/files.d.ts.map +1 -0
  23. package/dist/coverage/files.js +52 -0
  24. package/dist/coverage/files.js.map +1 -0
  25. package/dist/coverage/fixtures.d.ts +5 -0
  26. package/dist/coverage/fixtures.d.ts.map +1 -0
  27. package/dist/coverage/fixtures.js +42 -0
  28. package/dist/coverage/fixtures.js.map +1 -0
  29. package/dist/coverage/global-setup.d.ts +3 -0
  30. package/dist/coverage/global-setup.d.ts.map +1 -0
  31. package/dist/coverage/global-setup.js +18 -0
  32. package/dist/coverage/global-setup.js.map +1 -0
  33. package/dist/coverage/index.d.ts +10 -0
  34. package/dist/coverage/index.d.ts.map +1 -0
  35. package/dist/coverage/index.js +62 -0
  36. package/dist/coverage/index.js.map +1 -0
  37. package/dist/coverage/report.d.ts +7 -0
  38. package/dist/coverage/report.d.ts.map +1 -0
  39. package/{src → dist}/coverage/report.js +262 -362
  40. package/dist/coverage/report.js.map +1 -0
  41. package/dist/coverage/reporter.d.ts +16 -0
  42. package/dist/coverage/reporter.d.ts.map +1 -0
  43. package/dist/coverage/reporter.js +57 -0
  44. package/dist/coverage/reporter.js.map +1 -0
  45. package/dist/coverage/types.d.ts +47 -0
  46. package/dist/coverage/types.d.ts.map +1 -0
  47. package/dist/coverage/v8-tracker.d.ts +6 -0
  48. package/dist/coverage/v8-tracker.d.ts.map +1 -0
  49. package/dist/coverage/v8-tracker.js +166 -0
  50. package/dist/coverage/v8-tracker.js.map +1 -0
  51. package/dist/defineConfig.d.ts +3 -0
  52. package/dist/defineConfig.d.ts.map +1 -0
  53. package/dist/index.d.ts +11 -0
  54. package/dist/index.d.ts.map +1 -0
  55. package/dist/index.js +61 -0
  56. package/dist/index.js.map +1 -0
  57. package/dist/serverCoverage.d.ts +8 -0
  58. package/dist/serverCoverage.d.ts.map +1 -0
  59. package/dist/serverCoverage.js +42 -0
  60. package/dist/serverCoverage.js.map +1 -0
  61. package/dist/vitest.config.d.ts +3 -0
  62. package/dist/vitest.config.d.ts.map +1 -0
  63. package/dist/vitest.config.js +83 -0
  64. package/dist/vitest.config.js.map +1 -0
  65. package/package.json +25 -14
  66. package/index.d.ts +0 -64
  67. package/src/clearDatabase.js +0 -19
  68. package/src/cli.js +0 -948
  69. package/src/coverage/collect.js +0 -134
  70. package/src/coverage/config-loader.js +0 -202
  71. package/src/coverage/config.js +0 -134
  72. package/src/coverage/files.js +0 -55
  73. package/src/coverage/fixtures.js +0 -37
  74. package/src/coverage/global-setup.js +0 -19
  75. package/src/coverage/index.js +0 -30
  76. package/src/coverage/reporter.js +0 -65
  77. package/src/coverage/v8-tracker.js +0 -205
  78. package/src/defineConfig.js +0 -129
  79. package/src/index.js +0 -49
  80. package/src/vitest.config.mjs +0 -107
  81. /package/{src → dist}/register-tty.cjs +0 -0
package/src/cli.js DELETED
@@ -1,948 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { spawn, spawnSync } from "child_process"
4
- import fs from "fs"
5
- import fsPromises from "fs/promises"
6
- import path from "path"
7
- import { createRequire } from "module"
8
- import { fileURLToPath } from "url"
9
-
10
- import fg from "fast-glob"
11
-
12
- import { createCoverageConfig } from "./coverage/config.js"
13
- import { createCollectCoverageMatcher, isInsideAnyRoot, resolveCollectCoverageRoots } from "./coverage/collect.js"
14
- import { loadCoverageOptions } from "./coverage/config-loader.js"
15
- import { removeCoverageFiles } from "./coverage/files.js"
16
- import { collectCoveredFiles, CoverageThresholdError, generateCoverageReport } from "./coverage/report.js"
17
-
18
-
19
- const require = createRequire(import.meta.url)
20
- const moduleDir = path.dirname(fileURLToPath(import.meta.url))
21
-
22
-
23
- const VITEST_COVERAGE_CANDIDATES = ["src/coverage.json"]
24
-
25
- const COMBINED_COVERAGE_ENV_VAR = "RB_TEST_COMBINED_COVERAGE"
26
-
27
- const isAider = process.env.IS_AIDER === "yes"
28
-
29
- if (process.env.IS_AIDER !== undefined && process.env.IS_AIDER !== "yes") {
30
- console.warn("Warning: IS_AIDER is set to a value other than 'yes'.")
31
- }
32
-
33
- async function runTests() {
34
- const userArgs = process.argv.slice(2)
35
- const buildSpecsMap = userArgs.includes("--build-specs-map")
36
- const auto = userArgs.includes("--auto")
37
- const showMapping = userArgs.includes("--show-mapping")
38
- const filteredArgs = userArgs.filter((arg) => arg !== "--build-specs-map" && arg !== "--auto" && arg !== "--show-mapping")
39
-
40
- if (buildSpecsMap && auto) {
41
- throw new Error("[rb-test] --auto cannot be combined with --build-specs-map")
42
- }
43
-
44
- if (showMapping && !auto) {
45
- throw new Error("[rb-test] --show-mapping requires --auto")
46
- }
47
-
48
- const playwrightCoverage = await loadPlaywrightCoverageConfig()
49
- const vitestCoverage = await loadVitestCoverageConfig()
50
- const combinedCoverage = resolveCombinedCoverage(playwrightCoverage, vitestCoverage)
51
-
52
- if (buildSpecsMap) {
53
- await buildSpecsMapFromCoverage({
54
- userArgs: filteredArgs,
55
- playwrightCoverage,
56
- vitestCoverage,
57
- combinedCoverage,
58
- })
59
- return
60
- }
61
-
62
- const shouldGenerateCoverageReport = combinedCoverage?.enabled && !auto
63
-
64
- if (shouldGenerateCoverageReport) {
65
- await cleanCoverageArtifacts(combinedCoverage.config)
66
- }
67
-
68
- let testError = null
69
-
70
- try {
71
- await runVitest(vitestCoverage, combinedCoverage?.config ?? null, { disableCoverage: auto })
72
- console.log("\nRunning Playwright Tests...")
73
- const playwrightArgs = auto
74
- ? await resolveAutoPlaywrightArgs({
75
- userArgs: filteredArgs,
76
- playwrightCoverage,
77
- vitestCoverage,
78
- showMapping,
79
- })
80
- : userArgs
81
- if (playwrightArgs) {
82
- await runPlaywright(playwrightArgs, { disableCoverage: auto })
83
- }
84
- } catch (error) {
85
- testError = error
86
- }
87
-
88
- if (shouldGenerateCoverageReport) {
89
- try {
90
- await finalizeCoverage(combinedCoverage.config)
91
- } catch (error) {
92
- if (!testError) {
93
- testError = error
94
- }
95
- }
96
- }
97
-
98
- if (testError) {
99
- throw testError
100
- }
101
- }
102
-
103
- runTests()
104
- .then(() => process.exit(0))
105
- .catch((error) => {
106
- if (!(error instanceof CoverageThresholdError)) {
107
- console.error(error?.stack ?? String(error))
108
- }
109
- process.exit(1)
110
- })
111
-
112
- async function runVitest(coverage, combinedConfig, { disableCoverage = false } = {}) {
113
- const vitestArgs = ["run", "--passWithNoTests"]
114
- const vitestConfig = resolveVitestConfig()
115
-
116
- if (vitestConfig) {
117
- vitestArgs.push("--config", vitestConfig)
118
- }
119
- const launcher = resolveVitestLauncher()
120
- const env = withRegisterShim(process.env)
121
-
122
- if (disableCoverage) {
123
- env.RB_DISABLE_COVERAGE = "1"
124
- }
125
-
126
- if (coverage?.enabled && !disableCoverage) {
127
- env.NODE_V8_COVERAGE = resolveNodeCoverageDir(combinedConfig ?? coverage.config)
128
- }
129
-
130
- await spawnWithLogs({
131
- name: "Vitest",
132
- launcher,
133
- args: vitestArgs,
134
- env,
135
- successMessage: "Vitest suite passed!",
136
- failureMessage: "Vitest failed",
137
- })
138
-
139
- if (coverage?.enabled && !disableCoverage) {
140
- await convertNodeCoverage({
141
- config: combinedConfig ?? coverage.config,
142
- nodeCoverageDir: resolveNodeCoverageDir(combinedConfig ?? coverage.config),
143
- })
144
- }
145
- }
146
-
147
- async function buildSpecsMapFromCoverage({ userArgs, combinedCoverage }) {
148
- if (!combinedCoverage?.enabled) {
149
- throw new Error("[specs-map] Coverage must be enabled to build the specs map.")
150
- }
151
-
152
- const config = combinedCoverage.config
153
- const workspaceRoot = findWorkspaceRoot(process.cwd())
154
-
155
- const specSourceFiles = await findSpecSourceFiles(config.rootDir)
156
- if (specSourceFiles.length === 0) {
157
- throw new Error("[specs-map] No spec files found under spec/**/*.spec.ts")
158
- }
159
-
160
- const filesMapDir = path.join(config.testResultsRoot, "files-map")
161
- await fsPromises.rm(filesMapDir, { recursive: true, force: true })
162
- await fsPromises.mkdir(filesMapDir, { recursive: true })
163
-
164
- for (const specSourceFile of specSourceFiles) {
165
- const specProjectPath = path.relative(config.rootDir, specSourceFile)
166
- const specWorkspacePath = toPosixPath(path.relative(workspaceRoot, specSourceFile))
167
- const testFile = resolvePlaywrightSpecFile(specProjectPath)
168
-
169
- console.log(`\n[specs-map] Running ${specWorkspacePath}`)
170
-
171
- await removeCoverageFiles(config)
172
-
173
- let error = null
174
- let failed = false
175
- try {
176
- await runPlaywright([...userArgs, testFile])
177
- } catch (runError) {
178
- error = runError
179
- failed = true
180
- console.error(`[specs-map] Failed: ${specWorkspacePath}`)
181
- console.error(runError?.stack ?? String(runError))
182
- }
183
-
184
- const coveredFiles = await collectCoveredFiles(config)
185
- const impactedFiles = coveredFiles
186
- .map((filePath) => toPosixPath(path.relative(workspaceRoot, filePath)))
187
- .filter((relativePath) => relativePath && !relativePath.startsWith("../") && relativePath !== "..")
188
- .sort()
189
-
190
- const outputFile = path.join(filesMapDir, `${specProjectPath}.json`)
191
- await fsPromises.mkdir(path.dirname(outputFile), { recursive: true })
192
- await fsPromises.writeFile(
193
- outputFile,
194
- JSON.stringify(
195
- {
196
- spec: specWorkspacePath,
197
- files: impactedFiles,
198
- failed,
199
- },
200
- null,
201
- 2,
202
- ),
203
- "utf8",
204
- )
205
-
206
- if (failed) {
207
- throw error
208
- }
209
- }
210
- }
211
-
212
- async function resolveAutoPlaywrightArgs({ userArgs, playwrightCoverage, vitestCoverage, showMapping = false }) {
213
- const config = playwrightCoverage?.config ?? vitestCoverage?.config ?? null
214
- if (!config) {
215
- console.warn("[auto] Coverage config not found; running full Playwright suite.")
216
- return userArgs
217
- }
218
-
219
- const filesMapDir = path.join(config.testResultsRoot, "files-map")
220
- const mapFiles = await findFilesMapJson(filesMapDir)
221
- if (mapFiles.length === 0) {
222
- console.warn("[auto] Specs map not found; running full Playwright suite.")
223
- return userArgs
224
- }
225
-
226
- const workspaceRoot = findWorkspaceRoot(process.cwd())
227
- const gitChanges = getGitChanges(workspaceRoot)
228
- const renameMap = new Map(gitChanges.filter((change) => change.kind === "rename").map((change) => [change.oldPath, change.newPath]))
229
-
230
- const specRootAbs = path.join(config.rootDir, "spec")
231
- const matchesCollectCoverageFrom = createCollectCoverageMatcher(config.collectCoverageFrom, config.rootDir)
232
-
233
- const directSpecChanges = new Set()
234
- const sourceChanges = []
235
-
236
- for (const change of gitChanges) {
237
- if (change.kind === "rename") {
238
- const oldAbs = path.join(workspaceRoot, change.oldPath)
239
- const newAbs = path.join(workspaceRoot, change.newPath)
240
-
241
- if (isSpecSourceFile(newAbs, specRootAbs) && fs.existsSync(newAbs)) {
242
- directSpecChanges.add(change.newPath)
243
- }
244
-
245
- const oldMatches = matchesCollectCoverageFrom(oldAbs)
246
- const newMatches = matchesCollectCoverageFrom(newAbs)
247
- if (oldMatches || newMatches) {
248
- sourceChanges.push(change)
249
- }
250
- continue
251
- }
252
-
253
- const abs = path.join(workspaceRoot, change.path)
254
-
255
- if (isSpecSourceFile(abs, specRootAbs) && fs.existsSync(abs)) {
256
- directSpecChanges.add(change.path)
257
- }
258
-
259
- if (matchesCollectCoverageFrom(abs)) {
260
- sourceChanges.push(change)
261
- }
262
- }
263
-
264
- if (directSpecChanges.size === 0 && sourceChanges.length === 0) {
265
- console.log("[auto] No relevant git changes.")
266
- return null
267
- }
268
-
269
- const parsedMaps = []
270
- for (const file of mapFiles) {
271
- const json = await readJson(file)
272
- if (!json) {
273
- continue
274
- }
275
-
276
- if (json.failed === true) {
277
- console.warn("[auto] Specs map contains failed entries; running full Playwright suite.")
278
- return userArgs
279
- }
280
-
281
- const spec = typeof json?.spec === "string" ? json.spec : null
282
- if (!spec) {
283
- continue
284
- }
285
-
286
- const files = Array.isArray(json?.files) ? json.files.filter((entry) => typeof entry === "string") : []
287
- parsedMaps.push({ spec, files })
288
- }
289
-
290
- if (parsedMaps.length === 0) {
291
- console.warn("[auto] Specs map is empty; running full Playwright suite.")
292
- return userArgs
293
- }
294
-
295
- const specsByImpactedFile = new Map()
296
-
297
- for (const entry of parsedMaps) {
298
- const resolvedSpec = resolveRenamedPath(entry.spec, renameMap)
299
- for (const file of entry.files) {
300
- const list = specsByImpactedFile.get(file) ?? []
301
- list.push(resolvedSpec)
302
- specsByImpactedFile.set(file, list)
303
- }
304
- }
305
-
306
- const unmappedSourceChanges = sourceChanges.filter((change) => {
307
- if (change.kind === "path") {
308
- return !specsByImpactedFile.has(change.path)
309
- }
310
- return !specsByImpactedFile.has(change.oldPath) && !specsByImpactedFile.has(change.newPath)
311
- })
312
-
313
- if (unmappedSourceChanges.length > 0) {
314
- console.warn("[auto] Unmapped source changes detected:")
315
- for (const change of unmappedSourceChanges) {
316
- if (change.kind === "path") {
317
- console.warn(` - ${change.path}`)
318
- } else {
319
- console.warn(` - ${change.oldPath} -> ${change.newPath}`)
320
- }
321
- }
322
- }
323
-
324
- const selectedSpecs = new Set(directSpecChanges)
325
- const triggersBySpec = new Map()
326
-
327
- for (const spec of directSpecChanges) {
328
- if (showMapping) {
329
- triggersBySpec.set(spec, new Set([spec]))
330
- }
331
- }
332
-
333
- for (const change of sourceChanges) {
334
- if (change.kind === "path") {
335
- const specs = specsByImpactedFile.get(change.path) ?? []
336
- specs.forEach((spec) => selectedSpecs.add(spec))
337
- if (showMapping) {
338
- for (const spec of specs) {
339
- const current = triggersBySpec.get(spec) ?? new Set()
340
- current.add(change.path)
341
- triggersBySpec.set(spec, current)
342
- }
343
- }
344
- continue
345
- }
346
-
347
- const oldSpecs = specsByImpactedFile.get(change.oldPath) ?? []
348
- oldSpecs.forEach((spec) => selectedSpecs.add(spec))
349
- const newSpecs = specsByImpactedFile.get(change.newPath) ?? []
350
- newSpecs.forEach((spec) => selectedSpecs.add(spec))
351
- if (showMapping) {
352
- const key = `${change.oldPath} -> ${change.newPath}`
353
- const allSpecs = new Set([...oldSpecs, ...newSpecs])
354
- for (const spec of allSpecs) {
355
- const current = triggersBySpec.get(spec) ?? new Set()
356
- current.add(key)
357
- triggersBySpec.set(spec, current)
358
- }
359
- }
360
- }
361
-
362
- const missingSpecs = []
363
- const specsToRun = Array.from(selectedSpecs)
364
- .filter((spec) => {
365
- const abs = path.join(workspaceRoot, spec)
366
- if (fs.existsSync(abs)) {
367
- return true
368
- }
369
- missingSpecs.push(spec)
370
- return false
371
- })
372
- .sort()
373
- if (missingSpecs.length > 0) {
374
- console.warn(`[auto] Ignoring ${missingSpecs.length} missing spec file(s):`)
375
- for (const spec of missingSpecs.sort()) {
376
- console.warn(` - ${spec}`)
377
- }
378
- }
379
-
380
- if (specsToRun.length === 0) {
381
- console.log("[auto] No impacted specs.")
382
- return null
383
- }
384
-
385
- if (showMapping) {
386
- console.log("[auto] Mapping:")
387
- for (const spec of specsToRun) {
388
- const triggers = Array.from(triggersBySpec.get(spec) ?? []).sort()
389
- if (triggers.length === 0) {
390
- continue
391
- }
392
- console.log(` - ${spec}`)
393
- for (const trigger of triggers) {
394
- console.log(` <- ${trigger}`)
395
- }
396
- }
397
- }
398
-
399
- const playwrightFiles = specsToRun
400
- .map((specWorkspacePath) => path.join(workspaceRoot, specWorkspacePath))
401
- .filter((specAbs) => isSubpath(specAbs, config.rootDir))
402
- .map((specAbs) => {
403
- const specProjectPath = path.relative(config.rootDir, specAbs)
404
- return resolvePlaywrightSpecFile(specProjectPath)
405
- })
406
-
407
- const totalSpecFiles = (await findSpecSourceFiles(config.rootDir)).length
408
- console.log(`[auto] Running ${playwrightFiles.length}/${totalSpecFiles} spec file(s).`)
409
- return [...userArgs, ...playwrightFiles]
410
- }
411
-
412
- async function findFilesMapJson(filesMapDir) {
413
- const patterns = ["spec/**/*.spec.ts.json", "spec/**/*.spec.tsx.json"]
414
- const matches = await fg(patterns, { cwd: filesMapDir, absolute: true, onlyFiles: true }).catch(() => [])
415
- return matches.sort()
416
- }
417
-
418
- function getGitChanges(workspaceRoot) {
419
- const result = spawnSync("git", ["status", "--porcelain=1", "-z"], {
420
- cwd: workspaceRoot,
421
- encoding: "utf8",
422
- })
423
-
424
- if (result.status !== 0) {
425
- throw new Error(`[auto] Failed to read git status: ${result.stderr || "unknown error"}`)
426
- }
427
-
428
- const tokens = String(result.stdout ?? "").split("\0").filter(Boolean)
429
- const changes = []
430
-
431
- for (let i = 0; i < tokens.length; i += 1) {
432
- const record = tokens[i]
433
- if (record.length < 4) {
434
- continue
435
- }
436
-
437
- const status = record.slice(0, 2)
438
- const pathPart = toPosixPath(record.slice(3))
439
-
440
- if (isRenameOrCopyStatus(status)) {
441
- const next = tokens[i + 1]
442
- if (typeof next !== "string") {
443
- continue
444
- }
445
- changes.push({
446
- kind: "rename",
447
- oldPath: pathPart,
448
- newPath: toPosixPath(next),
449
- })
450
- i += 1
451
- continue
452
- }
453
-
454
- changes.push({
455
- kind: "path",
456
- path: pathPart,
457
- })
458
- }
459
-
460
- return changes
461
- }
462
-
463
- function isRenameOrCopyStatus(status) {
464
- return status.includes("R") || status.includes("C")
465
- }
466
-
467
- function resolveRenamedPath(original, renameMap) {
468
- let current = original
469
- const visited = new Set()
470
-
471
- while (renameMap.has(current) && !visited.has(current)) {
472
- visited.add(current)
473
- current = renameMap.get(current)
474
- }
475
-
476
- return current
477
- }
478
-
479
- function isSubpath(candidate, root) {
480
- const relative = path.relative(root, candidate)
481
- return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative))
482
- }
483
-
484
- function isSpecSourceFile(absolutePath, specRootAbsolute) {
485
- if (!isSubpath(absolutePath, specRootAbsolute)) {
486
- return false
487
- }
488
-
489
- return absolutePath.endsWith(".spec.ts")
490
- || absolutePath.endsWith(".spec.tsx")
491
- }
492
-
493
- async function findSpecSourceFiles(projectRoot) {
494
- const patterns = ["spec/**/*.spec.ts", "spec/**/*.spec.tsx"]
495
- const matches = await fg(patterns, { cwd: projectRoot, absolute: true, onlyFiles: true })
496
- return matches.sort()
497
- }
498
-
499
- function resolvePlaywrightSpecFile(specProjectPath) {
500
- const buildSpecRoot = path.join(process.cwd(), "build", "spec")
501
- const isBuildSpecProject = fs.existsSync(buildSpecRoot)
502
-
503
- if (!isBuildSpecProject) {
504
- return specProjectPath
505
- }
506
-
507
- const builtCandidate = normalizeBuiltSpecPath(path.join("build", specProjectPath))
508
- const builtAbsolute = path.resolve(process.cwd(), builtCandidate)
509
- if (!fs.existsSync(builtAbsolute)) {
510
- throw new Error(`[specs-map] Missing built spec file: ${builtCandidate}`)
511
- }
512
-
513
- return builtCandidate
514
- }
515
-
516
- function normalizeBuiltSpecPath(filePath) {
517
- if (filePath.endsWith(".ts") || filePath.endsWith(".tsx")) {
518
- return `${filePath.replace(/\.tsx?$/, "")}.js`
519
- }
520
- return filePath
521
- }
522
-
523
- function toPosixPath(input) {
524
- return String(input ?? "").split(path.sep).join("/")
525
- }
526
-
527
- function findWorkspaceRoot(projectRoot) {
528
- let dir = path.resolve(projectRoot)
529
-
530
- while (true) {
531
- const pkgPath = path.join(dir, "package.json")
532
- try {
533
- if (fs.existsSync(pkgPath)) {
534
- const parsed = JSON.parse(fs.readFileSync(pkgPath, "utf8"))
535
- if (parsed && typeof parsed === "object" && parsed.workspaces) {
536
- return dir
537
- }
538
- }
539
- } catch {
540
- //
541
- }
542
-
543
- const parent = path.dirname(dir)
544
- if (parent === dir) {
545
- return path.resolve(projectRoot)
546
- }
547
- dir = parent
548
- }
549
- }
550
-
551
- function runPlaywright(userArgs, { disableCoverage = false } = {}) {
552
- // Determine config file path
553
- const configPath = fs.existsSync(
554
- path.join(process.cwd(), "playwright.config.ts"),
555
- )
556
- ? path.join(process.cwd(), "playwright.config.ts")
557
- : path.join(moduleDir, "playwright.config.ts")
558
-
559
- const hasCustomConfig = userArgs.some((arg) => {
560
- if (arg === "--config" || arg === "-c") {
561
- return true
562
- }
563
-
564
- return arg.startsWith("--config=")
565
- })
566
-
567
- const playwrightArgs = ["test"]
568
-
569
- if (!hasCustomConfig) {
570
- playwrightArgs.push("--config", configPath)
571
- }
572
-
573
- playwrightArgs.push(...userArgs)
574
-
575
- ensureJsxRuntimeShim(process.cwd())
576
- const launcher = resolvePlaywrightLauncher()
577
- const env = withRegisterShim(process.env)
578
- env[COMBINED_COVERAGE_ENV_VAR] = "1"
579
- if (disableCoverage) {
580
- env.RB_DISABLE_COVERAGE = "1"
581
- }
582
-
583
- return spawnWithLogs({
584
- name: "Playwright",
585
- launcher,
586
- args: playwrightArgs,
587
- env,
588
- successMessage: "Playwright suite passed!",
589
- failureMessage: "Playwright failed",
590
- })
591
- }
592
-
593
- function resolvePlaywrightLauncher() {
594
- const cliPath = resolveCliPath()
595
- if (cliPath) {
596
- return {
597
- command: process.execPath,
598
- args: [cliPath],
599
- }
600
- }
601
-
602
- const localBin = path.resolve(process.cwd(), "node_modules/.bin/playwright")
603
- if (fs.existsSync(localBin)) {
604
- return {
605
- command: localBin,
606
- args: [],
607
- }
608
- }
609
-
610
- return {
611
- command: "playwright",
612
- args: [],
613
- }
614
- }
615
-
616
- function resolveCliPath() {
617
- const searchRoots = [process.cwd(), moduleDir]
618
-
619
- for (const base of searchRoots) {
620
- try {
621
- const pkgPath = require.resolve("@playwright/test/package.json", { paths: [base] })
622
- const cliPath = path.join(path.dirname(pkgPath), "cli.js")
623
- if (fs.existsSync(cliPath)) {
624
- return cliPath
625
- }
626
- } catch (_error) {
627
- // continue searching
628
- }
629
- }
630
-
631
- return null
632
- }
633
-
634
- function resolveVitestLauncher() {
635
- const searchRoots = [process.cwd(), moduleDir]
636
-
637
- for (const base of searchRoots) {
638
- try {
639
- const pkgPath = require.resolve("vitest/package.json", { paths: [base] })
640
- const pkgDir = path.dirname(pkgPath)
641
- const pkgJson = JSON.parse(fs.readFileSync(pkgPath, "utf8"))
642
- const binPath = typeof pkgJson.bin === "string" ? pkgJson.bin : pkgJson.bin?.vitest
643
- if (binPath) {
644
- return {
645
- command: process.execPath,
646
- args: [path.join(pkgDir, binPath)],
647
- }
648
- }
649
- } catch (_error) {
650
- // continue searching
651
- }
652
- }
653
-
654
- const localBin = path.resolve(process.cwd(), "node_modules/.bin/vitest")
655
- if (fs.existsSync(localBin)) {
656
- return {
657
- command: localBin,
658
- args: [],
659
- }
660
- }
661
-
662
- return {
663
- command: "vitest",
664
- args: [],
665
- }
666
- }
667
-
668
- function resolveVitestConfig() {
669
- const userConfig = findVitestConfig(process.cwd())
670
- if (userConfig) {
671
- return userConfig
672
- }
673
-
674
- const bundledConfig = path.join(moduleDir, "vitest.config.mjs")
675
- return fs.existsSync(bundledConfig) ? bundledConfig : null
676
- }
677
-
678
- function findVitestConfig(baseDir) {
679
- const candidates = [
680
- "vitest.config.ts",
681
- "vitest.config.js",
682
- "vitest.config.mjs",
683
- ]
684
-
685
- for (const file of candidates) {
686
- const fullPath = path.join(baseDir, file)
687
- if (fs.existsSync(fullPath)) {
688
- return fullPath
689
- }
690
- }
691
-
692
- return null
693
- }
694
-
695
- async function loadVitestCoverageConfig() {
696
- const options = await loadCoverageOptions({
697
- optional: true,
698
- candidates: VITEST_COVERAGE_CANDIDATES,
699
- defaultTestResultsDir: "test-results-vitest",
700
- })
701
- if (!options) {
702
- return null
703
- }
704
-
705
- const config = createCoverageConfig(options)
706
-
707
- return {
708
- config,
709
- enabled: config.coverageEnabled,
710
- }
711
- }
712
-
713
- async function loadPlaywrightCoverageConfig() {
714
- const options = await loadCoverageOptions({ optional: true })
715
- if (!options) {
716
- return null
717
- }
718
-
719
- const config = createCoverageConfig(options)
720
-
721
- return {
722
- config,
723
- enabled: config.coverageEnabled,
724
- }
725
- }
726
-
727
- function resolveCombinedCoverage(playwrightCoverage, vitestCoverage) {
728
- if (playwrightCoverage?.enabled) {
729
- return playwrightCoverage
730
- }
731
-
732
- if (vitestCoverage?.enabled) {
733
- return vitestCoverage
734
- }
735
-
736
- return null
737
- }
738
-
739
- async function cleanCoverageArtifacts(config) {
740
- await removeCoverageFiles(config)
741
- await fsPromises.rm(config.coverageReportDir, { recursive: true, force: true })
742
- await fsPromises.rm(path.join(config.testResultsRoot, "node-coverage"), { recursive: true, force: true })
743
- }
744
-
745
- function resolveNodeCoverageDir(config) {
746
- return path.join(config.testResultsRoot, "node-coverage", "vitest")
747
- }
748
-
749
- async function convertNodeCoverage(coverage) {
750
- const { config, nodeCoverageDir } = coverage
751
-
752
- const entries = await fsPromises.readdir(nodeCoverageDir).catch(() => [])
753
- const scripts = []
754
- const scriptRoots = resolveCollectCoverageRoots(config.collectCoverageFrom, config.rootDir)
755
-
756
- for (const entry of entries) {
757
- if (!entry.endsWith(".json")) {
758
- continue
759
- }
760
-
761
- const fullPath = path.join(nodeCoverageDir, entry)
762
- const payload = await readJson(fullPath)
763
- const results = Array.isArray(payload?.result) ? payload.result : []
764
-
765
- for (const script of results) {
766
- const normalized = normalizeNodeScriptUrl(script.url, config.rootDir)
767
- if (!normalized) {
768
- continue
769
- }
770
-
771
- if (isNodeModulesPath(normalized.absolutePath)) {
772
- continue
773
- }
774
-
775
- if (!isInsideAnyRoot(normalized.absolutePath, scriptRoots)) {
776
- continue
777
- }
778
-
779
- const source = await fsPromises.readFile(normalized.absolutePath, "utf8").catch(() => "")
780
-
781
- scripts.push({
782
- absolutePath: normalized.absolutePath,
783
- relativePath: normalized.relativePath,
784
- source,
785
- functions: script.functions ?? [],
786
- url: script.url,
787
- })
788
- }
789
- }
790
-
791
- if (scripts.length === 0) {
792
- return
793
- }
794
-
795
- const outDir = path.join(config.testResultsRoot, "vitest")
796
- await fsPromises.mkdir(outDir, { recursive: true })
797
- const outputFile = path.join(outDir, config.coverageFileName)
798
- await fsPromises.writeFile(outputFile, JSON.stringify({ testId: "vitest", scripts }, null, 2), "utf8")
799
- }
800
-
801
- async function finalizeCoverage(config) {
802
- try {
803
- await generateCoverageReport(config)
804
- } catch (error) {
805
- if (error instanceof CoverageThresholdError) {
806
- console.error(error.message)
807
- }
808
- throw error
809
- }
810
- }
811
-
812
- async function readJson(filePath) {
813
- try {
814
- const raw = await fsPromises.readFile(filePath, "utf8")
815
- return JSON.parse(raw)
816
- } catch {
817
- return null
818
- }
819
- }
820
-
821
- function normalizeNodeScriptUrl(rawUrl, rootDir) {
822
- if (!rawUrl || rawUrl.startsWith("node:")) {
823
- return null
824
- }
825
-
826
- let absolutePath = null
827
-
828
- try {
829
- if (rawUrl.startsWith("file://")) {
830
- absolutePath = fileURLToPath(rawUrl)
831
- }
832
- } catch (_err) {
833
- // ignore invalid URLs
834
- }
835
-
836
- if (!absolutePath && path.isAbsolute(rawUrl)) {
837
- absolutePath = rawUrl
838
- }
839
-
840
- if (!absolutePath) {
841
- return null
842
- }
843
-
844
- const normalized = path.normalize(absolutePath)
845
-
846
- return {
847
- absolutePath: normalized,
848
- relativePath: path.relative(rootDir, normalized),
849
- }
850
- }
851
-
852
- function isNodeModulesPath(filePath) {
853
- return path
854
- .normalize(String(filePath ?? ""))
855
- .split(path.sep)
856
- .includes("node_modules")
857
- }
858
-
859
- function spawnWithLogs({ name, launcher, args, env, successMessage, failureMessage }) {
860
- return new Promise((resolve, reject) => {
861
- const stdoutBuffer = []
862
- const stderrBuffer = []
863
-
864
- const child = spawn(
865
- launcher.command,
866
- [...(launcher.args || []), ...args],
867
- {
868
- shell: false,
869
- env,
870
- },
871
- )
872
-
873
- child.stdout.on("data", (data) => {
874
- if (!isAider) {
875
- process.stdout.write(data)
876
- }
877
- stdoutBuffer.push(data.toString())
878
- })
879
-
880
- child.stderr.on("data", (data) => {
881
- if (!isAider) {
882
- process.stderr.write(data)
883
- }
884
- stderrBuffer.push(data.toString())
885
- })
886
-
887
- child.on("close", (code) => {
888
- if (code === 0) {
889
- if (successMessage) {
890
- console.log(successMessage)
891
- }
892
- resolve()
893
- } else {
894
- console.error(failureMessage || `${name} failed:`)
895
-
896
- if (isAider) {
897
- if (stdoutBuffer.length > 0) {
898
- console.error(stdoutBuffer.join(""))
899
- }
900
-
901
- if (stderrBuffer.length > 0) {
902
- console.error(stderrBuffer.join(""))
903
- }
904
- }
905
-
906
- reject(new Error(`${name} failed with exit code: ${code}`))
907
- }
908
- })
909
-
910
- child.on("error", (error) => {
911
- console.error(`Error spawning ${name}:`, error)
912
- reject(error)
913
- })
914
- })
915
- }
916
-
917
- function withRegisterShim(baseEnv) {
918
- const nodeOptions = appendNodeRequire(baseEnv.NODE_OPTIONS, path.join(moduleDir, "register-tty.cjs"))
919
- return {
920
- ...baseEnv,
921
- NODE_OPTIONS: nodeOptions,
922
- }
923
- }
924
-
925
- function ensureJsxRuntimeShim(projectRoot) {
926
- const shimDir = path.join(projectRoot, "node_modules", "playwright")
927
- fs.mkdirSync(shimDir, { recursive: true })
928
- const shims = [
929
- { file: "jsx-runtime.js", target: "react/jsx-runtime" },
930
- { file: "jsx-dev-runtime.js", target: "react/jsx-dev-runtime" },
931
- ]
932
-
933
- for (const { file, target } of shims) {
934
- const filePath = path.join(shimDir, file)
935
- if (!fs.existsSync(filePath)) {
936
- const content = `export * from "${target}";\nexport { default } from "${target}";\n`
937
- fs.writeFileSync(filePath, content, "utf8")
938
- }
939
- }
940
- }
941
-
942
- function appendNodeRequire(existing, modulePath) {
943
- const flag = `--require=${modulePath}`
944
- if (!existing || existing.length === 0) {
945
- return flag
946
- }
947
- return `${existing} ${flag}`
948
- }