@rpcbase/test 0.304.0 → 0.305.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rpcbase/test",
3
- "version": "0.304.0",
3
+ "version": "0.305.0",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src",
package/src/cli.js CHANGED
@@ -19,6 +19,7 @@ const moduleDir = path.dirname(fileURLToPath(import.meta.url))
19
19
 
20
20
  const VITEST_COVERAGE_CANDIDATES = ["src/coverage.json"]
21
21
 
22
+ const COMBINED_COVERAGE_ENV_VAR = "RB_TEST_COMBINED_COVERAGE"
22
23
 
23
24
  const isAider = process.env.IS_AIDER === "yes"
24
25
 
@@ -29,25 +30,27 @@ if (process.env.IS_AIDER !== undefined && process.env.IS_AIDER !== "yes") {
29
30
  async function runTests() {
30
31
  const userArgs = process.argv.slice(2)
31
32
 
33
+ const playwrightCoverage = await loadPlaywrightCoverageConfig()
32
34
  const vitestCoverage = await loadVitestCoverageConfig()
35
+ const combinedCoverage = resolveCombinedCoverage(playwrightCoverage, vitestCoverage)
33
36
 
34
- if (vitestCoverage?.enabled) {
35
- await cleanCoverageArtifacts(vitestCoverage.config)
37
+ if (combinedCoverage?.enabled) {
38
+ await cleanCoverageArtifacts(combinedCoverage.config)
36
39
  }
37
40
 
38
41
  let testError = null
39
42
 
40
43
  try {
41
- await runVitest(vitestCoverage)
44
+ await runVitest(vitestCoverage, combinedCoverage?.config ?? null)
42
45
  console.log("\nRunning Playwright Tests...")
43
46
  await runPlaywright(userArgs)
44
47
  } catch (error) {
45
48
  testError = error
46
49
  }
47
50
 
48
- if (vitestCoverage?.enabled) {
51
+ if (combinedCoverage?.enabled) {
49
52
  try {
50
- await finalizeCoverage(vitestCoverage.config)
53
+ await finalizeCoverage(combinedCoverage.config)
51
54
  } catch (error) {
52
55
  if (!testError) {
53
56
  testError = error
@@ -64,7 +67,7 @@ runTests()
64
67
  .then(() => process.exit(0))
65
68
  .catch(() => process.exit(1))
66
69
 
67
- async function runVitest(coverage) {
70
+ async function runVitest(coverage, combinedConfig) {
68
71
  const vitestArgs = ["run", "--passWithNoTests"]
69
72
  const vitestConfig = resolveVitestConfig()
70
73
 
@@ -75,7 +78,7 @@ async function runVitest(coverage) {
75
78
  const env = withRegisterShim(process.env)
76
79
 
77
80
  if (coverage?.enabled) {
78
- env.NODE_V8_COVERAGE = coverage.nodeCoverageDir
81
+ env.NODE_V8_COVERAGE = resolveNodeCoverageDir(combinedConfig ?? coverage.config)
79
82
  }
80
83
 
81
84
  await spawnWithLogs({
@@ -88,7 +91,10 @@ async function runVitest(coverage) {
88
91
  })
89
92
 
90
93
  if (coverage?.enabled) {
91
- await convertNodeCoverage(coverage)
94
+ await convertNodeCoverage({
95
+ config: combinedConfig ?? coverage.config,
96
+ nodeCoverageDir: resolveNodeCoverageDir(combinedConfig ?? coverage.config),
97
+ })
92
98
  }
93
99
  }
94
100
 
@@ -119,6 +125,7 @@ function runPlaywright(userArgs) {
119
125
  ensureJsxRuntimeShim(process.cwd())
120
126
  const launcher = resolvePlaywrightLauncher()
121
127
  const env = withRegisterShim(process.env)
128
+ env[COMBINED_COVERAGE_ENV_VAR] = "1"
122
129
 
123
130
  return spawnWithLogs({
124
131
  name: "Playwright",
@@ -246,17 +253,46 @@ async function loadVitestCoverageConfig() {
246
253
 
247
254
  return {
248
255
  config,
249
- nodeCoverageDir: path.join(config.testResultsRoot, "node-coverage"),
250
256
  enabled: config.coverageEnabled,
251
257
  }
252
258
  }
253
259
 
260
+ async function loadPlaywrightCoverageConfig() {
261
+ const options = await loadCoverageOptions({ optional: true })
262
+ if (!options) {
263
+ return null
264
+ }
265
+
266
+ const config = createCoverageConfig(options)
267
+
268
+ return {
269
+ config,
270
+ enabled: config.coverageEnabled,
271
+ }
272
+ }
273
+
274
+ function resolveCombinedCoverage(playwrightCoverage, vitestCoverage) {
275
+ if (playwrightCoverage?.enabled) {
276
+ return playwrightCoverage
277
+ }
278
+
279
+ if (vitestCoverage?.enabled) {
280
+ return vitestCoverage
281
+ }
282
+
283
+ return null
284
+ }
285
+
254
286
  async function cleanCoverageArtifacts(config) {
255
287
  await removeCoverageFiles(config)
256
288
  await fsPromises.rm(config.coverageReportDir, { recursive: true, force: true })
257
289
  await fsPromises.rm(path.join(config.testResultsRoot, "node-coverage"), { recursive: true, force: true })
258
290
  }
259
291
 
292
+ function resolveNodeCoverageDir(config) {
293
+ return path.join(config.testResultsRoot, "node-coverage", "vitest")
294
+ }
295
+
260
296
  async function convertNodeCoverage(coverage) {
261
297
  const { config, nodeCoverageDir } = coverage
262
298
 
@@ -5,6 +5,10 @@ import { removeCoverageFiles } from "./files.js"
5
5
 
6
6
  export function createCoverageGlobalSetup(config) {
7
7
  return async function globalSetup() {
8
+ if (process.env.RB_TEST_COMBINED_COVERAGE === "1") {
9
+ return
10
+ }
11
+
8
12
  if (!config.coverageEnabled) {
9
13
  return
10
14
  }
@@ -1,5 +1,6 @@
1
1
  import fs from "node:fs/promises"
2
2
  import path from "node:path"
3
+ import { fileURLToPath } from "node:url"
3
4
 
4
5
  import * as libCoverage from "istanbul-lib-coverage"
5
6
  import { createContext } from "istanbul-lib-report"
@@ -37,7 +38,7 @@ export async function generateCoverageReport(config) {
37
38
  }
38
39
 
39
40
  for (const script of payload.scripts) {
40
- await mergeScriptCoverage(coverageMap, script)
41
+ await mergeScriptCoverage(coverageMap, script, config)
41
42
  }
42
43
  }
43
44
 
@@ -82,20 +83,37 @@ export async function generateCoverageReport(config) {
82
83
  }
83
84
  }
84
85
 
85
- async function mergeScriptCoverage(coverageMap, script) {
86
+ async function mergeScriptCoverage(coverageMap, script, config) {
86
87
  const scriptPath = script.absolutePath
87
88
  if (!scriptPath) {
88
89
  return
89
90
  }
90
91
 
92
+ if (isNodeModulesPath(scriptPath)) {
93
+ return
94
+ }
95
+
96
+ if (isViteVirtualModulePath(scriptPath)) {
97
+ return
98
+ }
99
+
91
100
  const source = script.source && script.source.length > 0
92
101
  ? script.source
93
102
  : await fs.readFile(scriptPath, "utf8")
94
103
 
95
- const converter = v8ToIstanbul(scriptPath, 0, { source })
104
+ const sourceMap = await loadSourceMapForScript(scriptPath, source)
105
+ const converter = v8ToIstanbul(
106
+ scriptPath,
107
+ 0,
108
+ sourceMap ? { source, sourceMap: { sourcemap: sourceMap } } : { source },
109
+ )
96
110
  await converter.load()
97
111
  converter.applyCoverage(script.functions)
98
- coverageMap.merge(converter.toIstanbul())
112
+
113
+ const filtered = filterCoverageMap(converter.toIstanbul(), config)
114
+ if (Object.keys(filtered).length > 0) {
115
+ coverageMap.merge(filtered)
116
+ }
99
117
  }
100
118
 
101
119
  async function readCoverageFile(file) {
@@ -190,3 +208,222 @@ function createGlobMatcher(pattern) {
190
208
  function toPosix(input) {
191
209
  return input.split(path.sep).join("/")
192
210
  }
211
+
212
+ function stripQuery(url) {
213
+ const queryIndex = url.indexOf("?")
214
+ const hashIndex = url.indexOf("#")
215
+
216
+ const endIndex = Math.min(
217
+ queryIndex === -1 ? Number.POSITIVE_INFINITY : queryIndex,
218
+ hashIndex === -1 ? Number.POSITIVE_INFINITY : hashIndex,
219
+ )
220
+
221
+ if (!Number.isFinite(endIndex)) {
222
+ return url
223
+ }
224
+
225
+ return url.slice(0, endIndex)
226
+ }
227
+
228
+ function extractSourceMappingUrl(source) {
229
+ const regex = /\/\/[#@]\s*sourceMappingURL=([^\s]+)/g
230
+
231
+ let last = null
232
+ let match = null
233
+
234
+ while ((match = regex.exec(source)) !== null) {
235
+ last = match[1]
236
+ }
237
+
238
+ return typeof last === "string" && last.length > 0 ? last : null
239
+ }
240
+
241
+ function resolveSourceMapPath(scriptPath, mappingUrl) {
242
+ const cleaned = stripQuery(mappingUrl)
243
+
244
+ if (cleaned.startsWith("file://")) {
245
+ return fileURLToPath(cleaned)
246
+ }
247
+
248
+ if (path.isAbsolute(cleaned)) {
249
+ return cleaned
250
+ }
251
+
252
+ return path.resolve(path.dirname(scriptPath), cleaned)
253
+ }
254
+
255
+ function filterCoverageMap(map, config) {
256
+ if (!map || typeof map !== "object") {
257
+ return {}
258
+ }
259
+
260
+ const filtered = {}
261
+
262
+ for (const [filePath, fileCoverage] of Object.entries(map)) {
263
+ if (!isLibraryFile(filePath, config)) {
264
+ continue
265
+ }
266
+ filtered[filePath] = fileCoverage
267
+ }
268
+
269
+ return filtered
270
+ }
271
+
272
+ function isLibraryFile(filePath, config) {
273
+ const absolutePath = resolveCoveragePath(filePath, config.workspaceRoot)
274
+ if (!absolutePath) {
275
+ return false
276
+ }
277
+
278
+ if (isNodeModulesPath(absolutePath)) {
279
+ return false
280
+ }
281
+
282
+ if (isViteVirtualModulePath(absolutePath)) {
283
+ return false
284
+ }
285
+
286
+ return isInsideLib(absolutePath, config.libRoots)
287
+ }
288
+
289
+ function resolveCoveragePath(filePath, workspaceRoot) {
290
+ const raw = String(filePath ?? "").trim()
291
+ if (!raw) {
292
+ return null
293
+ }
294
+
295
+ const cleaned = stripQuery(raw)
296
+
297
+ if (cleaned.startsWith("file://")) {
298
+ try {
299
+ return path.normalize(fileURLToPath(cleaned))
300
+ } catch {
301
+ return null
302
+ }
303
+ }
304
+
305
+ if (path.isAbsolute(cleaned)) {
306
+ return path.normalize(cleaned)
307
+ }
308
+
309
+ if (cleaned.includes("://")) {
310
+ return null
311
+ }
312
+
313
+ return path.normalize(path.resolve(workspaceRoot, cleaned))
314
+ }
315
+
316
+ function isNodeModulesPath(filePath) {
317
+ return path
318
+ .normalize(String(filePath ?? ""))
319
+ .split(path.sep)
320
+ .includes("node_modules")
321
+ }
322
+
323
+ function isViteVirtualModulePath(filePath) {
324
+ const normalized = path.normalize(String(filePath ?? ""))
325
+ const baseName = path.basename(normalized)
326
+ return baseName === "__vite-browser-external"
327
+ || baseName.startsWith("__vite-browser-external:")
328
+ || baseName.startsWith("__vite-")
329
+ }
330
+
331
+ function isInsideLib(absolutePath, libRoots) {
332
+ return libRoots.some((libRoot) => {
333
+ const relative = path.relative(libRoot, absolutePath)
334
+ return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative))
335
+ })
336
+ }
337
+
338
+ function parseSourceMapPayload(raw) {
339
+ if (!raw || typeof raw !== "object") {
340
+ return null
341
+ }
342
+
343
+ const sources = raw.sources
344
+ if (!Array.isArray(sources) || sources.length === 0) {
345
+ return null
346
+ }
347
+
348
+ return raw
349
+ }
350
+
351
+ function normalizeSourceMap(sourceMap, scriptPath) {
352
+ const root = typeof sourceMap.sourceRoot === "string"
353
+ ? sourceMap.sourceRoot.replace("file://", "")
354
+ : ""
355
+
356
+ const dir = path.dirname(scriptPath)
357
+ const fixedSources = sourceMap.sources.map((source) => {
358
+ const raw = String(source ?? "")
359
+ const cleaned = stripQuery(raw)
360
+
361
+ if (cleaned.startsWith("file://")) {
362
+ try {
363
+ return path.normalize(fileURLToPath(cleaned))
364
+ } catch {
365
+ return cleaned
366
+ }
367
+ }
368
+
369
+ const withoutWebpack = cleaned.replace(/^webpack:\/\//, "")
370
+ const candidate = path.join(root, withoutWebpack)
371
+
372
+ if (path.isAbsolute(candidate)) {
373
+ return path.normalize(candidate)
374
+ }
375
+
376
+ const normalizedCandidate = candidate.split("/").join(path.sep)
377
+
378
+ if (dir.endsWith(`${path.sep}dist`) && !normalizedCandidate.startsWith(`..${path.sep}`)) {
379
+ if (normalizedCandidate.startsWith(`src${path.sep}`) || normalizedCandidate.startsWith(`lib${path.sep}`)) {
380
+ return path.normalize(path.resolve(dir, "..", normalizedCandidate))
381
+ }
382
+ }
383
+
384
+ return path.normalize(path.resolve(dir, normalizedCandidate))
385
+ })
386
+
387
+ return {
388
+ ...sourceMap,
389
+ sources: fixedSources,
390
+ }
391
+ }
392
+
393
+ async function loadSourceMapForScript(scriptPath, source) {
394
+ const mappingUrl = extractSourceMappingUrl(source)
395
+ if (!mappingUrl) {
396
+ return null
397
+ }
398
+
399
+ const cleaned = stripQuery(mappingUrl)
400
+
401
+ if (cleaned.startsWith("data:")) {
402
+ const commaIndex = cleaned.indexOf(",")
403
+ if (commaIndex === -1) {
404
+ return null
405
+ }
406
+
407
+ const meta = cleaned.slice(0, commaIndex)
408
+ const payload = cleaned.slice(commaIndex + 1)
409
+ const raw = meta.includes(";base64")
410
+ ? Buffer.from(payload, "base64").toString("utf8")
411
+ : decodeURIComponent(payload)
412
+
413
+ try {
414
+ const parsed = parseSourceMapPayload(JSON.parse(raw))
415
+ return parsed ? normalizeSourceMap(parsed, scriptPath) : null
416
+ } catch {
417
+ return null
418
+ }
419
+ }
420
+
421
+ try {
422
+ const mapPath = resolveSourceMapPath(scriptPath, cleaned)
423
+ const raw = await fs.readFile(mapPath, "utf8")
424
+ const parsed = parseSourceMapPayload(JSON.parse(raw))
425
+ return parsed ? normalizeSourceMap(parsed, scriptPath) : null
426
+ } catch {
427
+ return null
428
+ }
429
+ }
@@ -13,6 +13,10 @@ class CoverageReporter {
13
13
  }
14
14
 
15
15
  async onBegin() {
16
+ if (process.env.RB_TEST_COMBINED_COVERAGE === "1") {
17
+ return
18
+ }
19
+
16
20
  if (!this.config.coverageEnabled) {
17
21
  return
18
22
  }
@@ -22,6 +26,10 @@ class CoverageReporter {
22
26
  }
23
27
 
24
28
  async onEnd(result) {
29
+ if (process.env.RB_TEST_COMBINED_COVERAGE === "1") {
30
+ return
31
+ }
32
+
25
33
  if (!this.config.coverageEnabled) {
26
34
  return
27
35
  }
@@ -4,6 +4,11 @@ import path from "node:path"
4
4
 
5
5
  const VITE_FS_PREFIX = "/@fs/"
6
6
 
7
+ function sanitizePathSegment(input) {
8
+ const value = String(input ?? "").trim()
9
+ return value.replace(/[^a-zA-Z0-9._-]+/g, "_") || "unknown"
10
+ }
11
+
7
12
  export async function createCoverageTracker(page, config) {
8
13
  const session = await page.context().newCDPSession(page)
9
14
  const scriptMeta = new Map()
@@ -40,7 +45,7 @@ export async function createCoverageTracker(page, config) {
40
45
  return
41
46
  }
42
47
 
43
- const outputFile = testInfo.outputPath(config.coverageFileName)
48
+ const outputFile = resolveCoverageOutputFile(config, testInfo)
44
49
  await fs.mkdir(path.dirname(outputFile), { recursive: true })
45
50
  await fs.writeFile(outputFile, JSON.stringify(payload, null, 2), "utf8")
46
51
  } finally {
@@ -50,6 +55,13 @@ export async function createCoverageTracker(page, config) {
50
55
  }
51
56
  }
52
57
 
58
+ function resolveCoverageOutputFile(config, testInfo) {
59
+ const projectName = sanitizePathSegment(testInfo.project?.name)
60
+ const testId = sanitizePathSegment(testInfo.testId)
61
+ const outputDir = path.join(config.testResultsRoot, "playwright", projectName, testId)
62
+ return path.join(outputDir, config.coverageFileName)
63
+ }
64
+
53
65
  async function collectCoveragePayload(session, scriptMeta, sourceCache, testInfo, config) {
54
66
  const { result } = await session.send("Profiler.takePreciseCoverage")
55
67
  await session.send("Profiler.stopPreciseCoverage")
@@ -128,7 +140,7 @@ function normalizeScriptUrl(rawUrl, config) {
128
140
  if (decoded.startsWith(VITE_FS_PREFIX)) {
129
141
  absolutePath = path.normalize(decoded.slice(VITE_FS_PREFIX.length))
130
142
  } else if (decoded.startsWith("/")) {
131
- absolutePath = path.resolve(config.workspaceRoot, `.${decoded}`)
143
+ absolutePath = path.resolve(process.cwd(), `.${decoded}`)
132
144
  } else {
133
145
  return null
134
146
  }
package/src/index.js CHANGED
@@ -17,7 +17,7 @@ export function defineConfig(userConfig = {}) {
17
17
  const normalized = { ...userConfig }
18
18
  const reporters = ensureReporterArray(normalized.reporter)
19
19
 
20
- if (coverageHarness?.config.coverageEnabled) {
20
+ if (coverageHarness?.config.coverageEnabled && process.env.RB_TEST_COMBINED_COVERAGE !== "1") {
21
21
  const coverageReporter = coverageHarness.reporterEntry()
22
22
  if (!reporters.some(([name]) => name === coverageReporter[0])) {
23
23
  reporters.push(coverageReporter)