vitest 4.0.7 → 4.0.9

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 (65) hide show
  1. package/LICENSE.md +1 -1
  2. package/dist/browser.d.ts +2 -2
  3. package/dist/browser.js +2 -2
  4. package/dist/chunks/{base.D3GxgUMI.js → base.CiIV2DDC.js} +71 -36
  5. package/dist/chunks/{benchmark.DHKMYAts.js → benchmark.B3N2zMcH.js} +9 -4
  6. package/dist/chunks/{browser.d.-LKfRopd.d.ts → browser.d.DnU_kh8a.d.ts} +1 -1
  7. package/dist/chunks/{cac.G9DAn-c7.js → cac.B_NTJoIH.js} +115 -42
  8. package/dist/chunks/{cli-api.Csks4as1.js → cli-api.D48wY175.js} +1863 -845
  9. package/dist/chunks/console.Cf-YriPC.js +146 -0
  10. package/dist/chunks/{coverage.C2LA1DSL.js → coverage.BUlIqJrL.js} +284 -114
  11. package/dist/chunks/{creator.cqqifzG7.js → creator.BzqvXeRE.js} +75 -31
  12. package/dist/chunks/{date.-jtEtIeV.js → date.Bq6ZW5rf.js} +17 -6
  13. package/dist/chunks/{git.BFNcloKD.js → git.Bm2pzPAa.js} +3 -3
  14. package/dist/chunks/{global.d.DxtanrNO.d.ts → global.d.BQDgW9Pr.d.ts} +1 -1
  15. package/dist/chunks/{globals.BGT_RUsD.js → globals.DBrtKPdh.js} +5 -5
  16. package/dist/chunks/index.0kCJoeWi.js +220 -0
  17. package/dist/chunks/{index.CWIFvlX5.js → index.BfmpdV5p.js} +165 -54
  18. package/dist/chunks/{index.RwjEGCQ0.js → index.CGezRSGU.js} +2 -2
  19. package/dist/chunks/{index.DEPqWSIZ.js → index.CPA8jGhR.js} +33 -16
  20. package/dist/chunks/{index.CVpyv-Zg.js → index.kotH7DY7.js} +832 -373
  21. package/dist/chunks/{index.jMQYiEWE.js → index.op2Re5rn.js} +22 -12
  22. package/dist/chunks/{index.Dc3xnDvT.js → index.z7NPOg2E.js} +4 -4
  23. package/dist/chunks/{init-forks.IU-xQ2_X.js → init-forks.aqTzCSR2.js} +14 -4
  24. package/dist/chunks/{init-threads.C_NWvZkU.js → init-threads.C7T0-YMD.js} +1 -1
  25. package/dist/chunks/{init.fmH9J833.js → init.BQhNfT0h.js} +53 -30
  26. package/dist/chunks/{inspector.DLZxSeU3.js → inspector.CvyFGlXm.js} +25 -10
  27. package/dist/chunks/{moduleRunner.d.DEkTotCv.d.ts → moduleRunner.d.BxT-OjLR.d.ts} +1 -1
  28. package/dist/chunks/{node.BwAWWjHZ.js → node.Ce0vMQM7.js} +1 -1
  29. package/dist/chunks/{plugin.d.Cpes8Bt6.d.ts → plugin.d.DevON6kQ.d.ts} +1 -1
  30. package/dist/chunks/{reporters.d.CSNcMDxF.d.ts → reporters.d.BQ0wpUaj.d.ts} +6 -5
  31. package/dist/chunks/{rpc.D38ahn14.js → rpc.BytlcPfC.js} +20 -7
  32. package/dist/chunks/{setup-common.DR1sucx6.js → setup-common.Dw1XgX0v.js} +20 -8
  33. package/dist/chunks/{startModuleRunner.Cn7hCL7D.js → startModuleRunner.DLjmA_wU.js} +209 -86
  34. package/dist/chunks/{test.B6aJd6T3.js → test.w5HLbjmU.js} +48 -22
  35. package/dist/chunks/{utils.CG9h5ccR.js → utils.DvEY5TfP.js} +14 -5
  36. package/dist/chunks/{vi.BZvkKVkM.js → vi.CyIUVSoU.js} +267 -117
  37. package/dist/chunks/{vm.BL7_zzOr.js → vm.DXN8eCh2.js} +181 -75
  38. package/dist/chunks/{worker.d.D25zYZ7N.d.ts → worker.d.ZGohxCEd.d.ts} +74 -7
  39. package/dist/cli.js +2 -2
  40. package/dist/config.d.ts +5 -5
  41. package/dist/coverage.d.ts +3 -3
  42. package/dist/coverage.js +1 -1
  43. package/dist/environments.js +1 -1
  44. package/dist/index.d.ts +5 -5
  45. package/dist/index.js +5 -5
  46. package/dist/module-evaluator.d.ts +2 -2
  47. package/dist/module-evaluator.js +88 -38
  48. package/dist/module-runner.js +2 -2
  49. package/dist/node.d.ts +7 -7
  50. package/dist/node.js +17 -13
  51. package/dist/reporters.d.ts +3 -3
  52. package/dist/reporters.js +2 -2
  53. package/dist/runners.js +7 -7
  54. package/dist/snapshot.js +2 -2
  55. package/dist/suite.js +2 -2
  56. package/dist/worker.d.ts +1 -1
  57. package/dist/worker.js +15 -15
  58. package/dist/workers/forks.js +16 -16
  59. package/dist/workers/runVmTests.js +41 -22
  60. package/dist/workers/threads.js +16 -16
  61. package/dist/workers/vmForks.js +11 -11
  62. package/dist/workers/vmThreads.js +11 -11
  63. package/package.json +20 -20
  64. package/dist/chunks/console.CTJL2nuH.js +0 -115
  65. package/dist/chunks/index.Bgo3tNWt.js +0 -176
@@ -4,7 +4,7 @@ import { resolve as resolve$1, dirname, isAbsolute, relative, basename, join, no
4
4
  import { performance as performance$1 } from 'node:perf_hooks';
5
5
  import { getTests, getTestName, hasFailed, getSuites, generateHash, calculateSuiteHash, someTasksAreOnly, interpretTaskModes, getTasks, getFullName } from '@vitest/runner/utils';
6
6
  import { slash, toArray, isPrimitive } from '@vitest/utils/helpers';
7
- import { parseStacktrace, parseErrorStacktrace, defaultStackIgnorePatterns } from '@vitest/utils/source-map';
7
+ import { parseStacktrace, defaultStackIgnorePatterns, parseErrorStacktrace } from '@vitest/utils/source-map';
8
8
  import c from 'tinyrainbow';
9
9
  import { i as isTTY } from './env.D4Lgay0q.js';
10
10
  import { stripVTControlCharacters } from 'node:util';
@@ -126,10 +126,14 @@ const stringify = (value, replacer, space) => {
126
126
  };
127
127
 
128
128
  function getOutputFile(config, reporter) {
129
- if (config?.outputFile) return typeof config.outputFile === "string" ? config.outputFile : config.outputFile[reporter];
129
+ if (!config?.outputFile) return;
130
+ if (typeof config.outputFile === "string") return config.outputFile;
131
+ return config.outputFile[reporter];
130
132
  }
131
133
  function createDefinesScript(define) {
132
- return !define || serializeDefine(define) === "{}" ? "" : `
134
+ if (!define) return "";
135
+ if (serializeDefine(define) === "{}") return "";
136
+ return `
133
137
  const defines = ${serializeDefine(define)}
134
138
  Object.keys(defines).forEach((key) => {
135
139
  const segments = key.split('.')
@@ -161,13 +165,17 @@ function serializeDefine(define) {
161
165
  let res = `{`;
162
166
  const keys = Object.keys(userDefine).sort();
163
167
  for (let i = 0; i < keys.length; i++) {
164
- const key = keys[i], val = userDefine[key];
165
- if (res += `${JSON.stringify(key)}: ${handleDefineValue(val)}`, i !== keys.length - 1) res += `, `;
168
+ const key = keys[i];
169
+ const val = userDefine[key];
170
+ res += `${JSON.stringify(key)}: ${handleDefineValue(val)}`;
171
+ if (i !== keys.length - 1) res += `, `;
166
172
  }
167
173
  return `${res}}`;
168
174
  }
169
175
  function handleDefineValue(value) {
170
- return typeof value === "undefined" ? "undefined" : typeof value === "string" ? value : JSON.stringify(value);
176
+ if (typeof value === "undefined") return "undefined";
177
+ if (typeof value === "string") return value;
178
+ return JSON.stringify(value);
171
179
  }
172
180
 
173
181
  class BlobReporter {
@@ -180,13 +188,18 @@ class BlobReporter {
180
188
  }
181
189
  onInit(ctx) {
182
190
  if (ctx.config.watch) throw new Error("Blob reporter is not supported in watch mode");
183
- this.ctx = ctx, this.start = performance.now(), this.coverage = void 0;
191
+ this.ctx = ctx;
192
+ this.start = performance.now();
193
+ this.coverage = void 0;
184
194
  }
185
195
  onCoverage(coverage) {
186
196
  this.coverage = coverage;
187
197
  }
188
198
  async onTestRunEnd(testModules, unhandledErrors) {
189
- const executionTime = performance.now() - this.start, files = testModules.map((testModule) => testModule.task), errors = [...unhandledErrors], coverage = this.coverage;
199
+ const executionTime = performance.now() - this.start;
200
+ const files = testModules.map((testModule) => testModule.task);
201
+ const errors = [...unhandledErrors];
202
+ const coverage = this.coverage;
190
203
  let outputFile = this.options.outputFile ?? getOutputFile(this.ctx.config, "blob");
191
204
  if (!outputFile) {
192
205
  const shard = this.ctx.config.shard;
@@ -194,34 +207,40 @@ class BlobReporter {
194
207
  }
195
208
  const modules = this.ctx.projects.map((project) => {
196
209
  return [project.name, [...project.vite.moduleGraph.idToModuleMap.entries()].map((mod) => {
197
- return mod[1].file ? [
210
+ if (!mod[1].file) return null;
211
+ return [
198
212
  mod[0],
199
213
  mod[1].file,
200
214
  mod[1].url
201
- ] : null;
215
+ ];
202
216
  }).filter((x) => x != null)];
203
- }), report = [
217
+ });
218
+ const report = [
204
219
  this.ctx.version,
205
220
  files,
206
221
  errors,
207
222
  modules,
208
223
  coverage,
209
224
  executionTime
210
- ], reportFile = resolve$1(this.ctx.config.root, outputFile);
211
- await writeBlob(report, reportFile), this.ctx.logger.log("blob report written to", reportFile);
225
+ ];
226
+ const reportFile = resolve$1(this.ctx.config.root, outputFile);
227
+ await writeBlob(report, reportFile);
228
+ this.ctx.logger.log("blob report written to", reportFile);
212
229
  }
213
230
  }
214
231
  async function writeBlob(content, filename) {
215
- const report = stringify(content), dir = dirname(filename);
232
+ const report = stringify(content);
233
+ const dir = dirname(filename);
216
234
  if (!existsSync(dir)) await mkdir(dir, { recursive: true });
217
235
  await writeFile(filename, report, "utf-8");
218
236
  }
219
237
  async function readBlobs(currentVersion, blobsDirectory, projectsArray) {
220
238
  // using process.cwd() because --merge-reports can only be used in CLI
221
- const resolvedDir = resolve$1(process.cwd(), blobsDirectory), promises = (await readdir(resolvedDir)).map(async (filename) => {
239
+ const resolvedDir = resolve$1(process.cwd(), blobsDirectory);
240
+ const promises = (await readdir(resolvedDir)).map(async (filename) => {
222
241
  const fullPath = resolve$1(resolvedDir, filename);
223
242
  if (!(await stat(fullPath)).isFile()) throw new TypeError(`vitest.mergeReports() expects all paths in "${blobsDirectory}" to be files generated by the blob reporter, but "${filename}" is not a file`);
224
- const content = await readFile(fullPath, "utf-8"), [version, files, errors, moduleKeys, coverage, executionTime] = parse$1(content);
243
+ const [version, files, errors, moduleKeys, coverage, executionTime] = parse$1(await readFile(fullPath, "utf-8"));
225
244
  if (!version) throw new TypeError(`vitest.mergeReports() expects all paths in "${blobsDirectory}" to be files generated by the blob reporter, but "${filename}" is not a valid blob file`);
226
245
  return {
227
246
  version,
@@ -232,7 +251,8 @@ async function readBlobs(currentVersion, blobsDirectory, projectsArray) {
232
251
  file: filename,
233
252
  executionTime
234
253
  };
235
- }), blobs = await Promise.all(promises);
254
+ });
255
+ const blobs = await Promise.all(promises);
236
256
  if (!blobs.length) throw new Error(`vitest.mergeReports() requires at least one blob file in "${blobsDirectory}" directory, but none were found`);
237
257
  const versions = new Set(blobs.map((blob) => blob.version));
238
258
  if (versions.size > 1) throw new Error(`vitest.mergeReports() requires all blob files to be generated by the same Vitest version, received\n\n${blobs.map((b) => `- "${b.file}" uses v${b.version}`).join("\n")}`);
@@ -242,24 +262,26 @@ async function readBlobs(currentVersion, blobsDirectory, projectsArray) {
242
262
  blobs.forEach((blob) => {
243
263
  blob.moduleKeys.forEach(([projectName, moduleIds]) => {
244
264
  const project = projects[projectName];
245
- project && moduleIds.forEach(([moduleId, file, url]) => {
265
+ if (!project) return;
266
+ moduleIds.forEach(([moduleId, file, url]) => {
246
267
  const moduleNode = project.vite.moduleGraph.createFileOnlyEntry(file);
247
- moduleNode.url = url, moduleNode.id = moduleId, moduleNode.transformResult = {
268
+ moduleNode.url = url;
269
+ moduleNode.id = moduleId;
270
+ moduleNode.transformResult = {
248
271
  code: " ",
249
272
  map: null
250
- }, project.vite.moduleGraph.idToModuleMap.set(moduleId, moduleNode);
273
+ };
274
+ project.vite.moduleGraph.idToModuleMap.set(moduleId, moduleNode);
251
275
  });
252
276
  });
253
277
  });
254
- const files = blobs.flatMap((blob) => blob.files).sort((f1, f2) => {
255
- const time1 = f1.result?.startTime || 0, time2 = f2.result?.startTime || 0;
256
- return time1 - time2;
257
- }), errors = blobs.flatMap((blob) => blob.errors), coverages = blobs.map((blob) => blob.coverage), executionTimes = blobs.map((blob) => blob.executionTime);
258
278
  return {
259
- files,
260
- errors,
261
- coverages,
262
- executionTimes
279
+ files: blobs.flatMap((blob) => blob.files).sort((f1, f2) => {
280
+ return (f1.result?.startTime || 0) - (f2.result?.startTime || 0);
281
+ }),
282
+ errors: blobs.flatMap((blob) => blob.errors),
283
+ coverages: blobs.map((blob) => blob.coverage),
284
+ executionTimes: blobs.map((blob) => blob.executionTime)
263
285
  };
264
286
  }
265
287
 
@@ -269,44 +291,58 @@ function hasFailedSnapshot(suite) {
269
291
  });
270
292
  }
271
293
  function convertTasksToEvents(file, onTask) {
272
- const packs = [], events = [];
294
+ const packs = [];
295
+ const events = [];
273
296
  function visit(suite) {
274
- onTask?.(suite), packs.push([
297
+ onTask?.(suite);
298
+ packs.push([
275
299
  suite.id,
276
300
  suite.result,
277
301
  suite.meta
278
- ]), events.push([
302
+ ]);
303
+ events.push([
279
304
  suite.id,
280
305
  "suite-prepare",
281
306
  void 0
282
- ]), suite.tasks.forEach((task) => {
307
+ ]);
308
+ suite.tasks.forEach((task) => {
283
309
  if (task.type === "suite") visit(task);
284
- else if (onTask?.(task), suite.mode !== "skip" && suite.mode !== "todo") packs.push([
285
- task.id,
286
- task.result,
287
- task.meta
288
- ]), events.push([
289
- task.id,
290
- "test-prepare",
291
- void 0
292
- ]), task.annotations.forEach((annotation) => {
293
- events.push([
294
- task.id,
295
- "test-annotation",
296
- { annotation }
297
- ]);
298
- }), events.push([
299
- task.id,
300
- "test-finished",
301
- void 0
302
- ]);
303
- }), events.push([
310
+ else {
311
+ onTask?.(task);
312
+ if (suite.mode !== "skip" && suite.mode !== "todo") {
313
+ packs.push([
314
+ task.id,
315
+ task.result,
316
+ task.meta
317
+ ]);
318
+ events.push([
319
+ task.id,
320
+ "test-prepare",
321
+ void 0
322
+ ]);
323
+ task.annotations.forEach((annotation) => {
324
+ events.push([
325
+ task.id,
326
+ "test-annotation",
327
+ { annotation }
328
+ ]);
329
+ });
330
+ events.push([
331
+ task.id,
332
+ "test-finished",
333
+ void 0
334
+ ]);
335
+ }
336
+ }
337
+ });
338
+ events.push([
304
339
  suite.id,
305
340
  "suite-finished",
306
341
  void 0
307
342
  ]);
308
343
  }
309
- return visit(file), {
344
+ visit(file);
345
+ return {
310
346
  packs,
311
347
  events
312
348
  };
@@ -346,18 +382,26 @@ function errorBanner(message) {
346
382
  return divider(c.bold(c.bgRed(` ${message} `)), null, null, c.red);
347
383
  }
348
384
  function divider(text, left, right, color) {
349
- const cols = getCols(), c = color || ((text) => text);
385
+ const cols = getCols();
386
+ const c = color || ((text) => text);
350
387
  if (text) {
351
388
  const textLength = stripVTControlCharacters(text).length;
352
389
  if (left == null && right != null) left = cols - textLength - right;
353
- else left = left ?? Math.floor((cols - textLength) / 2), right = cols - textLength - left;
354
- return left = Math.max(0, left), right = Math.max(0, right), `${c(F_LONG_DASH.repeat(left))}${text}${c(F_LONG_DASH.repeat(right))}`;
390
+ else {
391
+ left = left ?? Math.floor((cols - textLength) / 2);
392
+ right = cols - textLength - left;
393
+ }
394
+ left = Math.max(0, left);
395
+ right = Math.max(0, right);
396
+ return `${c(F_LONG_DASH.repeat(left))}${text}${c(F_LONG_DASH.repeat(right))}`;
355
397
  }
356
398
  return F_LONG_DASH.repeat(cols);
357
399
  }
358
400
  function formatTestPath(root, path) {
359
401
  if (isAbsolute(path)) path = relative(root, path);
360
- const dir = dirname(path), ext = path.match(/(\.(spec|test)\.[cm]?[tj]sx?)$/)?.[0] || "", base = basename(path, ext);
402
+ const dir = dirname(path);
403
+ const ext = path.match(/(\.(spec|test)\.[cm]?[tj]sx?)$/)?.[0] || "";
404
+ const base = basename(path, ext);
361
405
  return slash(c.dim(`${dir}/`) + c.bold(base)) + c.dim(ext);
362
406
  }
363
407
  function renderSnapshotSummary(rootDir, snapshots) {
@@ -369,7 +413,8 @@ function renderSnapshotSummary(rootDir, snapshots) {
369
413
  else summary.push(c.bold(c.yellow(`${snapshots.filesRemoved} files obsolete `)));
370
414
  if (snapshots.filesRemovedList && snapshots.filesRemovedList.length) {
371
415
  const [head, ...tail] = snapshots.filesRemovedList;
372
- summary.push(`${c.gray(F_DOWN_RIGHT)} ${formatTestPath(rootDir, head)}`), tail.forEach((key) => {
416
+ summary.push(`${c.gray(F_DOWN_RIGHT)} ${formatTestPath(rootDir, head)}`);
417
+ tail.forEach((key) => {
373
418
  summary.push(` ${c.gray(F_DOT)} ${formatTestPath(rootDir, key)}`);
374
419
  });
375
420
  }
@@ -377,7 +422,8 @@ function renderSnapshotSummary(rootDir, snapshots) {
377
422
  if (snapshots.didUpdate) summary.push(c.bold(c.green(`${snapshots.unchecked} removed`)));
378
423
  else summary.push(c.bold(c.yellow(`${snapshots.unchecked} obsolete`)));
379
424
  snapshots.uncheckedKeysByFile.forEach((uncheckedFile) => {
380
- summary.push(`${c.gray(F_DOWN_RIGHT)} ${formatTestPath(rootDir, uncheckedFile.filePath)}`), uncheckedFile.keys.forEach((key) => summary.push(` ${c.gray(F_DOT)} ${key}`));
425
+ summary.push(`${c.gray(F_DOWN_RIGHT)} ${formatTestPath(rootDir, uncheckedFile.filePath)}`);
426
+ uncheckedFile.keys.forEach((key) => summary.push(` ${c.gray(F_DOT)} ${key}`));
381
427
  });
382
428
  }
383
429
  return summary;
@@ -387,7 +433,10 @@ function countTestErrors(tasks) {
387
433
  }
388
434
  function getStateString$1(tasks, name = "tests", showTotal = true) {
389
435
  if (tasks.length === 0) return c.dim(`no ${name}`);
390
- const passed = tasks.reduce((acc, i) => i.result?.state === "pass" ? acc + 1 : acc, 0), failed = tasks.reduce((acc, i) => i.result?.state === "fail" ? acc + 1 : acc, 0), skipped = tasks.reduce((acc, i) => i.mode === "skip" ? acc + 1 : acc, 0), todo = tasks.reduce((acc, i) => i.mode === "todo" ? acc + 1 : acc, 0);
436
+ const passed = tasks.reduce((acc, i) => i.result?.state === "pass" ? acc + 1 : acc, 0);
437
+ const failed = tasks.reduce((acc, i) => i.result?.state === "fail" ? acc + 1 : acc, 0);
438
+ const skipped = tasks.reduce((acc, i) => i.mode === "skip" ? acc + 1 : acc, 0);
439
+ const todo = tasks.reduce((acc, i) => i.mode === "todo" ? acc + 1 : acc, 0);
391
440
  return [
392
441
  failed ? c.bold(c.red(`${failed} failed`)) : null,
393
442
  passed ? c.bold(c.green(`${passed} passed`)) : null,
@@ -401,22 +450,22 @@ function getStateSymbol(task) {
401
450
  if (task.result.state === "run" || task.result.state === "queued") {
402
451
  if (task.type === "suite") return pointer;
403
452
  }
404
- return task.result.state === "pass" ? task.meta?.benchmark ? benchmarkPass : testPass : task.result.state === "fail" ? task.type === "suite" ? suiteFail : taskFail : " ";
453
+ if (task.result.state === "pass") return task.meta?.benchmark ? benchmarkPass : testPass;
454
+ if (task.result.state === "fail") return task.type === "suite" ? suiteFail : taskFail;
455
+ return " ";
405
456
  }
406
457
  function formatTimeString(date) {
407
458
  return date.toTimeString().split(" ")[0];
408
459
  }
409
460
  function formatTime(time) {
410
- return time > 1e3 ? `${(time / 1e3).toFixed(2)}s` : `${Math.round(time)}ms`;
461
+ if (time > 1e3) return `${(time / 1e3).toFixed(2)}s`;
462
+ return `${Math.round(time)}ms`;
411
463
  }
412
464
  function formatProjectName(project, suffix = " ") {
413
465
  if (!project?.name) return "";
414
466
  if (!c.isColorSupported) return `|${project.name}|${suffix}`;
415
467
  let background = project.color && c[`bg${capitalize(project.color)}`];
416
- if (!background) {
417
- const index = project.name.split("").reduce((acc, v, idx) => acc + v.charCodeAt(0) + idx, 0);
418
- background = labelDefaultColors[index % labelDefaultColors.length];
419
- }
468
+ if (!background) background = labelDefaultColors[project.name.split("").reduce((acc, v, idx) => acc + v.charCodeAt(0) + idx, 0) % labelDefaultColors.length];
420
469
  return c.black(background(` ${project.name} `)) + suffix;
421
470
  }
422
471
  function withLabel(color, label, message) {
@@ -428,7 +477,8 @@ function padSummaryTitle(str) {
428
477
  }
429
478
  function truncateString(text, maxLength) {
430
479
  const plainText = stripVTControlCharacters(text);
431
- return plainText.length <= maxLength ? text : `${plainText.slice(0, maxLength - 1)}…`;
480
+ if (plainText.length <= maxLength) return text;
481
+ return `${plainText.slice(0, maxLength - 1)}…`;
432
482
  }
433
483
  function capitalize(text) {
434
484
  return `${text[0].toUpperCase()}${text.slice(1)}`;
@@ -475,7 +525,9 @@ class BaseReporter {
475
525
  this.isTTY = options.isTTY ?? isTTY;
476
526
  }
477
527
  onInit(ctx) {
478
- this.ctx = ctx, this.ctx.logger.printBanner(), this.start = performance$1.now();
528
+ this.ctx = ctx;
529
+ this.ctx.logger.printBanner();
530
+ this.start = performance$1.now();
479
531
  }
480
532
  log(...messages) {
481
533
  this.ctx.logger.log(...messages);
@@ -487,8 +539,10 @@ class BaseReporter {
487
539
  return relative(this.ctx.config.root, path);
488
540
  }
489
541
  onTestRunEnd(testModules, unhandledErrors, _reason) {
490
- const files = testModules.map((testModule) => testModule.task), errors = [...unhandledErrors];
491
- if (this.end = performance$1.now(), !files.length && !errors.length) this.ctx.logger.printNoTestFound(this.ctx.filenamePattern);
542
+ const files = testModules.map((testModule) => testModule.task);
543
+ const errors = [...unhandledErrors];
544
+ this.end = performance$1.now();
545
+ if (!files.length && !errors.length) this.ctx.logger.printNoTestFound(this.ctx.filenamePattern);
492
546
  else this.reportSummary(files, errors);
493
547
  }
494
548
  onTestCaseResult(testCase) {
@@ -507,10 +561,13 @@ class BaseReporter {
507
561
  printTestModule(testModule) {
508
562
  const moduleState = testModule.state();
509
563
  if (moduleState === "queued" || moduleState === "pending") return;
510
- let testsCount = 0, failedCount = 0, skippedCount = 0;
564
+ let testsCount = 0;
565
+ let failedCount = 0;
566
+ let skippedCount = 0;
511
567
  // delaying logs to calculate the test stats first
512
568
  // which minimizes the amount of for loops
513
- const logs = [], originalLog = this.log.bind(this);
569
+ const logs = [];
570
+ const originalLog = this.log.bind(this);
514
571
  this.log = (msg) => logs.push(msg);
515
572
  const visit = (suiteState, children) => {
516
573
  for (const child of children) if (child.type === "suite") {
@@ -520,7 +577,8 @@ class BaseReporter {
520
577
  visit(suiteState, child.children);
521
578
  } else {
522
579
  const testResult = child.result();
523
- if (testsCount++, testResult.state === "failed") failedCount++;
580
+ testsCount++;
581
+ if (testResult.state === "failed") failedCount++;
524
582
  else if (testResult.state === "skipped") skippedCount++;
525
583
  if (this.ctx.config.hideSkippedTests && suiteState === "skipped")
526
584
  // Skipped suites are hidden when --hideSkippedTests
@@ -537,10 +595,14 @@ class BaseReporter {
537
595
  tests: testsCount,
538
596
  failed: failedCount,
539
597
  skipped: skippedCount
540
- })), logs.forEach((log) => this.log(log));
598
+ }));
599
+ logs.forEach((log) => this.log(log));
541
600
  }
542
601
  printTestCase(moduleState, test) {
543
- const testResult = test.result(), { duration = 0 } = test.diagnostic() || {}, padding = this.getTestIndentation(test.task), suffix = this.getTestCaseSuffix(test);
602
+ const testResult = test.result();
603
+ const { duration = 0 } = test.diagnostic() || {};
604
+ const padding = this.getTestIndentation(test.task);
605
+ const suffix = this.getTestCaseSuffix(test);
544
606
  if (testResult.state === "failed") this.log(c.red(` ${padding}${taskFail} ${this.getTestName(test.task, separator)}`) + suffix);
545
607
  else if (duration > this.ctx.config.slowTestThreshold) this.log(` ${padding}${c.yellow(c.dim(F_CHECK))} ${this.getTestName(test.task, separator)} ${suffix}`);
546
608
  else if (this.ctx.config.hideSkippedTests && testResult.state === "skipped") ; else if (this.renderSucceed || moduleState === "failed") this.log(` ${padding}${this.getStateSymbol(test)} ${this.getTestName(test.task, separator)}${suffix}`);
@@ -556,7 +618,9 @@ class BaseReporter {
556
618
  }
557
619
  printTestSuite(testSuite) {
558
620
  if (!this.renderSucceed) return;
559
- const indentation = " ".repeat(getIndentation(testSuite.task)), tests = Array.from(testSuite.children.allTests()), state = this.getStateSymbol(testSuite);
621
+ const indentation = " ".repeat(getIndentation(testSuite.task));
622
+ const tests = Array.from(testSuite.children.allTests());
623
+ const state = this.getStateSymbol(testSuite);
560
624
  this.log(` ${indentation}${state} ${testSuite.name} ${c.dim(`(${tests.length})`)}`);
561
625
  }
562
626
  getTestName(test, _separator) {
@@ -566,7 +630,9 @@ class BaseReporter {
566
630
  if (test === test.file) return test.name;
567
631
  let name = test.file.name;
568
632
  if (test.location) name += c.dim(`:${test.location.line}:${test.location.column}`);
569
- return name += separator, name += getTestName(test, separator), name;
633
+ name += separator;
634
+ name += getTestName(test, separator);
635
+ return name;
570
636
  }
571
637
  getTestIndentation(test) {
572
638
  return " ".repeat(getIndentation(test));
@@ -574,18 +640,24 @@ class BaseReporter {
574
640
  printAnnotations(test, console, padding = 0) {
575
641
  const annotations = test.annotations();
576
642
  if (!annotations.length) return;
577
- const PADDING = " ".repeat(padding), groupedAnnotations = {};
578
- for (const group in annotations.forEach((annotation) => {
643
+ const PADDING = " ".repeat(padding);
644
+ const groupedAnnotations = {};
645
+ annotations.forEach((annotation) => {
579
646
  const { location, type } = annotation;
580
647
  let group;
581
648
  if (location) {
582
649
  const file = relative(test.project.config.root, location.file);
583
650
  group = `${c.gray(`${file}:${location.line}:${location.column}`)} ${c.bold(type)}`;
584
651
  } else group = c.bold(type);
585
- groupedAnnotations[group] ??= [], groupedAnnotations[group].push(annotation);
586
- }), groupedAnnotations) this[console](`${PADDING}${c.blue(F_POINTER)} ${group}`), groupedAnnotations[group].forEach(({ message }) => {
587
- this[console](`${PADDING} ${c.blue(F_DOWN_RIGHT)} ${message}`);
652
+ groupedAnnotations[group] ??= [];
653
+ groupedAnnotations[group].push(annotation);
588
654
  });
655
+ for (const group in groupedAnnotations) {
656
+ this[console](`${PADDING}${c.blue(F_POINTER)} ${group}`);
657
+ groupedAnnotations[group].forEach(({ message }) => {
658
+ this[console](`${PADDING} ${c.blue(F_DOWN_RIGHT)} ${message}`);
659
+ });
660
+ }
589
661
  }
590
662
  getEntityPrefix(entity) {
591
663
  let title = this.getStateSymbol(entity);
@@ -594,7 +666,8 @@ class BaseReporter {
594
666
  return title;
595
667
  }
596
668
  getTestCaseSuffix(testCase) {
597
- const { heap, retryCount, repeatCount } = testCase.diagnostic() || {}, testResult = testCase.result();
669
+ const { heap, retryCount, repeatCount } = testCase.diagnostic() || {};
670
+ const testResult = testCase.result();
598
671
  let suffix = this.getDurationPrefix(testCase.task);
599
672
  if (retryCount != null && retryCount > 0) suffix += c.yellow(` (retry x${retryCount})`);
600
673
  if (repeatCount != null && repeatCount > 0) suffix += c.yellow(` (repeat x${repeatCount})`);
@@ -607,7 +680,8 @@ class BaseReporter {
607
680
  }
608
681
  getDurationPrefix(task) {
609
682
  const duration = task.result?.duration && Math.round(task.result?.duration);
610
- return duration == null ? "" : (duration > this.ctx.config.slowTestThreshold ? c.yellow : c.green)(` ${duration}${c.dim("ms")}`);
683
+ if (duration == null) return "";
684
+ return (duration > this.ctx.config.slowTestThreshold ? c.yellow : c.green)(` ${duration}${c.dim("ms")}`);
611
685
  }
612
686
  onWatcherStart(files = this.ctx.state.getFiles(), errors = this.ctx.state.getUnhandledErrors()) {
613
687
  if (errors.length > 0 || hasFailed(files)) this.log(withLabel("red", "FAIL", "Tests failed. Watching for file changes..."));
@@ -619,8 +693,10 @@ class BaseReporter {
619
693
  this.log(BADGE_PADDING + hints.join(c.dim(", ")));
620
694
  }
621
695
  onWatcherRerun(files, trigger) {
696
+ this.watchFilters = files;
697
+ this.failedUnwatchedFiles = this.ctx.state.getTestModules().filter((testModule) => !files.includes(testModule.task.filepath) && testModule.state() === "failed");
622
698
  // Update re-run count for each file
623
- this.watchFilters = files, this.failedUnwatchedFiles = this.ctx.state.getTestModules().filter((testModule) => !files.includes(testModule.task.filepath) && testModule.state() === "failed"), files.forEach((filepath) => {
699
+ files.forEach((filepath) => {
624
700
  let reruns = this._filesInWatchMode.get(filepath) ?? 0;
625
701
  this._filesInWatchMode.set(filepath, ++reruns);
626
702
  });
@@ -629,26 +705,35 @@ class BaseReporter {
629
705
  const rerun = this._filesInWatchMode.get(files[0]) ?? 1;
630
706
  banner += c.blue(`x${rerun} `);
631
707
  }
632
- if (this.ctx.logger.clearFullScreen(), this.log(withLabel("blue", "RERUN", banner)), this.ctx.configOverride.project) this.log(BADGE_PADDING + c.dim(" Project name: ") + c.blue(toArray(this.ctx.configOverride.project).join(", ")));
708
+ this.ctx.logger.clearFullScreen();
709
+ this.log(withLabel("blue", "RERUN", banner));
710
+ if (this.ctx.configOverride.project) this.log(BADGE_PADDING + c.dim(" Project name: ") + c.blue(toArray(this.ctx.configOverride.project).join(", ")));
633
711
  if (this.ctx.filenamePattern) this.log(BADGE_PADDING + c.dim(" Filename pattern: ") + c.blue(this.ctx.filenamePattern.join(", ")));
634
712
  if (this.ctx.configOverride.testNamePattern) this.log(BADGE_PADDING + c.dim(" Test name pattern: ") + c.blue(String(this.ctx.configOverride.testNamePattern)));
635
713
  this.log("");
636
714
  for (const testModule of this.failedUnwatchedFiles) this.printTestModule(testModule);
637
- this._timeStart = formatTimeString(/* @__PURE__ */ new Date()), this.start = performance$1.now();
715
+ this._timeStart = formatTimeString(/* @__PURE__ */ new Date());
716
+ this.start = performance$1.now();
638
717
  }
639
718
  onUserConsoleLog(log, taskState) {
640
719
  if (!this.shouldLog(log, taskState)) return;
641
- const output = log.type === "stdout" ? this.ctx.logger.outputStream : this.ctx.logger.errorStream, write = (msg) => output.write(msg);
720
+ const output = log.type === "stdout" ? this.ctx.logger.outputStream : this.ctx.logger.errorStream;
721
+ const write = (msg) => output.write(msg);
642
722
  let headerText = "unknown test";
643
723
  const task = log.taskId ? this.ctx.state.idMap.get(log.taskId) : void 0;
644
724
  if (task) headerText = this.getFullName(task, separator);
645
725
  else if (log.taskId && log.taskId !== "__vitest__unknown_test__") headerText = log.taskId;
646
- if (write(c.gray(log.type + c.dim(` | ${headerText}\n`)) + log.content), log.origin) {
726
+ write(c.gray(log.type + c.dim(` | ${headerText}\n`)) + log.content);
727
+ if (log.origin) {
647
728
  // browser logs don't have an extra end of line at the end like Node.js does
648
729
  if (log.browser) write("\n");
649
- const project = task ? this.ctx.getProjectByName(task.file.projectName || "") : this.ctx.getRootProject(), stack = log.browser ? project.browser?.parseStacktrace(log.origin) || [] : parseStacktrace(log.origin), highlight = task && stack.find((i) => i.file === task.file.filepath);
730
+ const project = task ? this.ctx.getProjectByName(task.file.projectName || "") : this.ctx.getRootProject();
731
+ const stack = log.browser ? project.browser?.parseStacktrace(log.origin) || [] : parseStacktrace(log.origin);
732
+ const highlight = task && stack.find((i) => i.file === task.file.filepath);
650
733
  for (const frame of stack) {
651
- const color = frame === highlight ? c.cyan : c.gray, path = relative(project.config.root, frame.file), positions = [frame.method, `${path}:${c.dim(`${frame.line}:${frame.column}`)}`].filter(Boolean).join(" ");
734
+ const color = frame === highlight ? c.cyan : c.gray;
735
+ const path = relative(project.config.root, frame.file);
736
+ const positions = [frame.method, `${path}:${c.dim(`${frame.line}:${frame.column}`)}`].filter(Boolean).join(" ");
652
737
  write(color(` ${c.dim(F_POINTER)} ${positions}\n`));
653
738
  }
654
739
  }
@@ -658,9 +743,11 @@ class BaseReporter {
658
743
  this.log(c.yellow("Test removed...") + (trigger ? c.dim(` [ ${this.relative(trigger)} ]\n`) : ""));
659
744
  }
660
745
  shouldLog(log, taskState) {
661
- if (this.ctx.config.silent === true || this.ctx.config.silent === "passed-only" && taskState !== "failed") return false;
746
+ if (this.ctx.config.silent === true) return false;
747
+ if (this.ctx.config.silent === "passed-only" && taskState !== "failed") return false;
662
748
  if (this.ctx.config.onConsoleLog) {
663
- const task = log.taskId ? this.ctx.state.idMap.get(log.taskId) : void 0, entity = task && this.ctx.state.getReportedEntity(task);
749
+ const task = log.taskId ? this.ctx.state.idMap.get(log.taskId) : void 0;
750
+ const entity = task && this.ctx.state.getReportedEntity(task);
664
751
  if (this.ctx.config.onConsoleLog(log.content, log.type, entity) === false) return false;
665
752
  }
666
753
  return true;
@@ -669,27 +756,41 @@ class BaseReporter {
669
756
  this.log(c.bold(c.magenta(reason === "config" ? "\nRestarting due to config changes..." : "\nRestarting Vitest...")));
670
757
  }
671
758
  reportSummary(files, errors) {
672
- if (this.printErrorsSummary(files, errors), this.ctx.config.mode === "benchmark") this.reportBenchmarkSummary(files);
759
+ this.printErrorsSummary(files, errors);
760
+ if (this.ctx.config.mode === "benchmark") this.reportBenchmarkSummary(files);
673
761
  else this.reportTestSummary(files, errors);
674
762
  }
675
763
  reportTestSummary(files, errors) {
676
764
  this.log();
677
- const affectedFiles = [...this.failedUnwatchedFiles.map((m) => m.task), ...files], tests = getTests(affectedFiles), snapshotOutput = renderSnapshotSummary(this.ctx.config.root, this.ctx.snapshot.summary);
765
+ const affectedFiles = [...this.failedUnwatchedFiles.map((m) => m.task), ...files];
766
+ const tests = getTests(affectedFiles);
767
+ const snapshotOutput = renderSnapshotSummary(this.ctx.config.root, this.ctx.snapshot.summary);
678
768
  for (const [index, snapshot] of snapshotOutput.entries()) {
679
769
  const title = index === 0 ? "Snapshots" : "";
680
770
  this.log(`${padSummaryTitle(title)} ${snapshot}`);
681
771
  }
682
772
  if (snapshotOutput.length > 1) this.log();
683
- if (this.log(padSummaryTitle("Test Files"), getStateString$1(affectedFiles)), this.log(padSummaryTitle("Tests"), getStateString$1(tests)), this.ctx.projects.some((c) => c.config.typecheck.enabled)) {
773
+ this.log(padSummaryTitle("Test Files"), getStateString$1(affectedFiles));
774
+ this.log(padSummaryTitle("Tests"), getStateString$1(tests));
775
+ if (this.ctx.projects.some((c) => c.config.typecheck.enabled)) {
684
776
  const failed = tests.filter((t) => t.meta?.typecheck && t.result?.errors?.length);
685
777
  this.log(padSummaryTitle("Type Errors"), failed.length ? c.bold(c.red(`${failed.length} failed`)) : c.dim("no errors"));
686
778
  }
687
779
  if (errors.length) this.log(padSummaryTitle("Errors"), c.bold(c.red(`${errors.length} error${errors.length > 1 ? "s" : ""}`)));
688
780
  this.log(padSummaryTitle("Start at"), this._timeStart);
689
- const collectTime = sum(files, (file) => file.collectDuration), testsTime = sum(files, (file) => file.result?.duration), setupTime = sum(files, (file) => file.setupDuration);
781
+ const collectTime = sum(files, (file) => file.collectDuration);
782
+ const testsTime = sum(files, (file) => file.result?.duration);
783
+ const setupTime = sum(files, (file) => file.setupDuration);
690
784
  if (this.watchFilters) this.log(padSummaryTitle("Duration"), formatTime(collectTime + testsTime + setupTime));
691
785
  else {
692
- const blobs = this.ctx.state.blobs, executionTime = blobs?.executionTimes ? sum(blobs.executionTimes, (time) => time) : this.end - this.start, environmentTime = sum(files, (file) => file.environmentLoad), prepareTime = sum(files, (file) => file.prepareDuration), transformTime = this.ctx.state.transformTime, typecheck = sum(this.ctx.projects, (project) => project.typechecker?.getResult().time), timers = [
786
+ const blobs = this.ctx.state.blobs;
787
+ // Execution time is either sum of all runs of `--merge-reports` or the current run's time
788
+ const executionTime = blobs?.executionTimes ? sum(blobs.executionTimes, (time) => time) : this.end - this.start;
789
+ const environmentTime = sum(files, (file) => file.environmentLoad);
790
+ const prepareTime = sum(files, (file) => file.prepareDuration);
791
+ const transformTime = this.ctx.state.transformTime;
792
+ const typecheck = sum(this.ctx.projects, (project) => project.typechecker?.getResult().time);
793
+ const timers = [
693
794
  `transform ${formatTime(transformTime)}`,
694
795
  `setup ${formatTime(setupTime)}`,
695
796
  `collect ${formatTime(collectTime)}`,
@@ -698,18 +799,32 @@ class BaseReporter {
698
799
  `prepare ${formatTime(prepareTime)}`,
699
800
  typecheck && `typecheck ${formatTime(typecheck)}`
700
801
  ].filter(Boolean).join(", ");
701
- if (this.log(padSummaryTitle("Duration"), formatTime(executionTime) + c.dim(` (${timers})`)), blobs?.executionTimes) this.log(padSummaryTitle("Per blob") + blobs.executionTimes.map((time) => ` ${formatTime(time)}`).join(""));
802
+ this.log(padSummaryTitle("Duration"), formatTime(executionTime) + c.dim(` (${timers})`));
803
+ if (blobs?.executionTimes) this.log(padSummaryTitle("Per blob") + blobs.executionTimes.map((time) => ` ${formatTime(time)}`).join(""));
702
804
  }
703
805
  this.log();
704
806
  }
705
807
  printErrorsSummary(files, errors) {
706
- const suites = getSuites(files), tests = getTests(files), failedSuites = suites.filter((i) => i.result?.errors), failedTests = tests.filter((i) => i.result?.state === "fail"), failedTotal = countTestErrors(failedSuites) + countTestErrors(failedTests);
808
+ const suites = getSuites(files);
809
+ const tests = getTests(files);
810
+ const failedSuites = suites.filter((i) => i.result?.errors);
811
+ const failedTests = tests.filter((i) => i.result?.state === "fail");
812
+ const failedTotal = countTestErrors(failedSuites) + countTestErrors(failedTests);
707
813
  // TODO: error divider should take into account merged errors for counting
708
814
  let current = 1;
709
815
  const errorDivider = () => this.error(`${c.red(c.dim(divider(`[${current++}/${failedTotal}]`, void 0, 1)))}\n`);
710
- if (failedSuites.length) this.error(`\n${errorBanner(`Failed Suites ${failedSuites.length}`)}\n`), this.printTaskErrors(failedSuites, errorDivider);
711
- if (failedTests.length) this.error(`\n${errorBanner(`Failed Tests ${failedTests.length}`)}\n`), this.printTaskErrors(failedTests, errorDivider);
712
- if (errors.length) this.ctx.logger.printUnhandledErrors(errors), this.error();
816
+ if (failedSuites.length) {
817
+ this.error(`\n${errorBanner(`Failed Suites ${failedSuites.length}`)}\n`);
818
+ this.printTaskErrors(failedSuites, errorDivider);
819
+ }
820
+ if (failedTests.length) {
821
+ this.error(`\n${errorBanner(`Failed Tests ${failedTests.length}`)}\n`);
822
+ this.printTaskErrors(failedTests, errorDivider);
823
+ }
824
+ if (errors.length) {
825
+ this.ctx.logger.printUnhandledErrors(errors);
826
+ this.error();
827
+ }
713
828
  }
714
829
  reportBenchmarkSummary(files) {
715
830
  const topBenches = getTests(files).filter((i) => i.result?.benchmark?.rank === 1);
@@ -717,7 +832,8 @@ class BaseReporter {
717
832
  for (const bench of topBenches) {
718
833
  const group = bench.suite || bench.file;
719
834
  if (!group) continue;
720
- const groupName = this.getFullName(group, separator), project = this.ctx.projects.find((p) => p.name === bench.file.projectName);
835
+ const groupName = this.getFullName(group, separator);
836
+ const project = this.ctx.projects.find((p) => p.name === bench.file.projectName);
721
837
  this.log(` ${formatProjectName(project)}${bench.name}${c.dim(` - ${groupName}`)}`);
722
838
  const siblings = group.tasks.filter((i) => i.meta.benchmark && i.result?.benchmark && i !== bench).sort((a, b) => a.result.benchmark.rank - b.result.benchmark.rank);
723
839
  for (const sibling of siblings) {
@@ -735,7 +851,10 @@ class BaseReporter {
735
851
  let previous;
736
852
  if (error?.stack) previous = errorsQueue.find((i) => {
737
853
  if (i[0]?.stack !== error.stack || i[0]?.diff !== error.diff) return false;
738
- const currentProjectName = task?.projectName || task.file?.projectName || "", projectName = i[1][0]?.projectName || i[1][0].file?.projectName || "", currentAnnotations = task.type === "test" && task.annotations, itemAnnotations = i[1][0].type === "test" && i[1][0].annotations;
854
+ const currentProjectName = task?.projectName || task.file?.projectName || "";
855
+ const projectName = i[1][0]?.projectName || i[1][0].file?.projectName || "";
856
+ const currentAnnotations = task.type === "test" && task.annotations;
857
+ const itemAnnotations = i[1][0].type === "test" && i[1][0].annotations;
739
858
  return projectName === currentProjectName && deepEqual(currentAnnotations, itemAnnotations);
740
859
  });
741
860
  if (previous) previous[1].push(task);
@@ -743,20 +862,24 @@ class BaseReporter {
743
862
  });
744
863
  for (const [error, tasks] of errorsQueue) {
745
864
  for (const task of tasks) {
746
- const filepath = task?.filepath || "", projectName = task?.projectName || task.file?.projectName || "", project = this.ctx.projects.find((p) => p.name === projectName);
865
+ const filepath = task?.filepath || "";
866
+ const projectName = task?.projectName || task.file?.projectName || "";
867
+ const project = this.ctx.projects.find((p) => p.name === projectName);
747
868
  let name = this.getFullName(task, separator);
748
869
  if (filepath) name += c.dim(` [ ${this.relative(filepath)} ]`);
749
870
  this.ctx.logger.error(`${c.bgRed(c.bold(" FAIL "))} ${formatProjectName(project)}${name}`);
750
871
  }
751
872
  const screenshotPaths = tasks.map((t) => t.meta?.failScreenshotPath).filter((screenshot) => screenshot != null);
752
- if (this.ctx.logger.printError(error, {
873
+ this.ctx.logger.printError(error, {
753
874
  project: this.ctx.getProjectByName(tasks[0].file.projectName || ""),
754
875
  verbose: this.verbose,
755
876
  screenshotPaths,
756
877
  task: tasks[0]
757
- }), tasks[0].type === "test" && tasks[0].annotations.length) {
878
+ });
879
+ if (tasks[0].type === "test" && tasks[0].annotations.length) {
758
880
  const test = this.ctx.state.getReportedEntity(tasks[0]);
759
- this.printAnnotations(test, "error", 1), this.error();
881
+ this.printAnnotations(test, "error", 1);
882
+ this.error();
760
883
  }
761
884
  errorDivider();
762
885
  }
@@ -765,7 +888,8 @@ class BaseReporter {
765
888
  function deepEqual(a, b) {
766
889
  if (a === b) return true;
767
890
  if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
768
- const keysA = Object.keys(a), keysB = Object.keys(b);
891
+ const keysA = Object.keys(a);
892
+ const keysB = Object.keys(b);
769
893
  if (keysA.length !== keysB.length) return false;
770
894
  for (const key of keysA) if (!keysB.includes(key) || !deepEqual(a[key], b[key])) return false;
771
895
  return true;
@@ -776,10 +900,16 @@ function sum(items, cb) {
776
900
  }, 0);
777
901
  }
778
902
  function getIndentation(suite, level = 1) {
779
- return suite.suite && !("filepath" in suite.suite) ? getIndentation(suite.suite, level + 1) : level;
903
+ if (suite.suite && !("filepath" in suite.suite)) return getIndentation(suite.suite, level + 1);
904
+ return level;
780
905
  }
781
906
 
782
- const DEFAULT_RENDER_INTERVAL_MS = 1e3, ESC = "\x1B[", CLEAR_LINE = `${ESC}K`, MOVE_CURSOR_ONE_ROW_UP = `${ESC}1A`, SYNC_START = `${ESC}?2026h`, SYNC_END = `${ESC}?2026l`;
907
+ const DEFAULT_RENDER_INTERVAL_MS = 1e3;
908
+ const ESC = "\x1B[";
909
+ const CLEAR_LINE = `${ESC}K`;
910
+ const MOVE_CURSOR_ONE_ROW_UP = `${ESC}1A`;
911
+ const SYNC_START = `${ESC}?2026h`;
912
+ const SYNC_END = `${ESC}?2026l`;
783
913
  /**
784
914
  * Renders content of `getWindow` at the bottom of the terminal and
785
915
  * forwards all other intercepted `stdout` and `stderr` logs above it.
@@ -795,37 +925,50 @@ class WindowRenderer {
795
925
  finished = false;
796
926
  cleanups = [];
797
927
  constructor(options) {
798
- // Write buffered content on unexpected exits, e.g. direct `process.exit()` calls
799
928
  this.options = {
800
929
  interval: DEFAULT_RENDER_INTERVAL_MS,
801
930
  ...options
802
- }, this.streams = {
931
+ };
932
+ this.streams = {
803
933
  output: options.logger.outputStream.write.bind(options.logger.outputStream),
804
934
  error: options.logger.errorStream.write.bind(options.logger.errorStream)
805
- }, this.cleanups.push(this.interceptStream(process.stdout, "output"), this.interceptStream(process.stderr, "error")), this.options.logger.onTerminalCleanup(() => {
806
- this.flushBuffer(), this.stop();
935
+ };
936
+ this.cleanups.push(this.interceptStream(process.stdout, "output"), this.interceptStream(process.stderr, "error"));
937
+ // Write buffered content on unexpected exits, e.g. direct `process.exit()` calls
938
+ this.options.logger.onTerminalCleanup(() => {
939
+ this.flushBuffer();
940
+ this.stop();
807
941
  });
808
942
  }
809
943
  start() {
810
- this.started = true, this.finished = false, this.renderInterval = setInterval(() => this.schedule(), this.options.interval).unref();
944
+ this.started = true;
945
+ this.finished = false;
946
+ this.renderInterval = setInterval(() => this.schedule(), this.options.interval).unref();
811
947
  }
812
948
  stop() {
813
- this.cleanups.splice(0).map((fn) => fn()), clearInterval(this.renderInterval);
949
+ this.cleanups.splice(0).map((fn) => fn());
950
+ clearInterval(this.renderInterval);
814
951
  }
815
952
  /**
816
953
  * Write all buffered output and stop buffering.
817
954
  * All intercepted writes are forwarded to actual write after this.
818
955
  */
819
956
  finish() {
820
- this.finished = true, this.flushBuffer(), clearInterval(this.renderInterval);
957
+ this.finished = true;
958
+ this.flushBuffer();
959
+ clearInterval(this.renderInterval);
821
960
  }
822
961
  /**
823
962
  * Queue new render update
824
963
  */
825
964
  schedule() {
826
- if (!this.renderScheduled) this.renderScheduled = true, this.flushBuffer(), setTimeout(() => {
827
- this.renderScheduled = false;
828
- }, 100).unref();
965
+ if (!this.renderScheduled) {
966
+ this.renderScheduled = true;
967
+ this.flushBuffer();
968
+ setTimeout(() => {
969
+ this.renderScheduled = false;
970
+ }, 100).unref();
971
+ }
829
972
  }
830
973
  flushBuffer() {
831
974
  if (this.buffer.length === 0) return this.render();
@@ -837,7 +980,8 @@ class WindowRenderer {
837
980
  continue;
838
981
  }
839
982
  if (current.type !== next.type) {
840
- this.render(current.message, current.type), current = next;
983
+ this.render(current.message, current.type);
984
+ current = next;
841
985
  continue;
842
986
  }
843
987
  current.message += next.message;
@@ -845,31 +989,40 @@ class WindowRenderer {
845
989
  if (current) this.render(current?.message, current?.type);
846
990
  }
847
991
  render(message, type = "output") {
848
- if (this.finished) return this.clearWindow(), this.write(message || "", type);
849
- const windowContent = this.options.getWindow(), rowCount = getRenderedRowCount(windowContent, this.options.logger.getColumns());
992
+ if (this.finished) {
993
+ this.clearWindow();
994
+ return this.write(message || "", type);
995
+ }
996
+ const windowContent = this.options.getWindow();
997
+ const rowCount = getRenderedRowCount(windowContent, this.options.logger.getColumns());
850
998
  let padding = this.windowHeight - rowCount;
851
999
  if (padding > 0 && message) padding -= getRenderedRowCount([message], this.options.logger.getColumns());
852
- if (this.write(SYNC_START), this.clearWindow(), message) this.write(message, type);
1000
+ this.write(SYNC_START);
1001
+ this.clearWindow();
1002
+ if (message) this.write(message, type);
853
1003
  if (padding > 0) this.write("\n".repeat(padding));
854
- this.write(windowContent.join("\n")), this.write(SYNC_END), this.windowHeight = rowCount + Math.max(0, padding);
1004
+ this.write(windowContent.join("\n"));
1005
+ this.write(SYNC_END);
1006
+ this.windowHeight = rowCount + Math.max(0, padding);
855
1007
  }
856
1008
  clearWindow() {
857
- if (this.windowHeight !== 0) {
858
- this.write(CLEAR_LINE);
859
- for (let i = 1; i < this.windowHeight; i++) this.write(`${MOVE_CURSOR_ONE_ROW_UP}${CLEAR_LINE}`);
860
- this.windowHeight = 0;
861
- }
1009
+ if (this.windowHeight === 0) return;
1010
+ this.write(CLEAR_LINE);
1011
+ for (let i = 1; i < this.windowHeight; i++) this.write(`${MOVE_CURSOR_ONE_ROW_UP}${CLEAR_LINE}`);
1012
+ this.windowHeight = 0;
862
1013
  }
863
1014
  interceptStream(stream, type) {
864
1015
  const original = stream.write;
865
- return stream.write = (chunk, _, callback) => {
1016
+ // @ts-expect-error -- not sure how 2 overloads should be typed
1017
+ stream.write = (chunk, _, callback) => {
866
1018
  if (chunk) if (this.finished || !this.started) this.write(chunk.toString(), type);
867
1019
  else this.buffer.push({
868
1020
  type,
869
1021
  message: chunk.toString()
870
1022
  });
871
1023
  callback?.();
872
- }, function restore() {
1024
+ };
1025
+ return function restore() {
873
1026
  stream.write = original;
874
1027
  };
875
1028
  }
@@ -887,7 +1040,8 @@ function getRenderedRowCount(rows, columns) {
887
1040
  return count;
888
1041
  }
889
1042
 
890
- const DURATION_UPDATE_INTERVAL_MS = 100, FINISHED_TEST_CLEANUP_TIME_MS = 1e3;
1043
+ const DURATION_UPDATE_INTERVAL_MS = 100;
1044
+ const FINISHED_TEST_CLEANUP_TIME_MS = 1e3;
891
1045
  /**
892
1046
  * Reporter extension that renders summary and forwards all other logs above itself.
893
1047
  * Intended to be used by other reporters, not as a standalone reporter.
@@ -908,21 +1062,34 @@ class SummaryReporter {
908
1062
  duration = 0;
909
1063
  durationInterval = void 0;
910
1064
  onInit(ctx, options = {}) {
911
- this.ctx = ctx, this.options = {
1065
+ this.ctx = ctx;
1066
+ this.options = {
912
1067
  verbose: false,
913
1068
  ...options
914
- }, this.renderer = new WindowRenderer({
1069
+ };
1070
+ this.renderer = new WindowRenderer({
915
1071
  logger: ctx.logger,
916
1072
  getWindow: () => this.createSummary()
917
- }), this.ctx.onClose(() => {
918
- clearInterval(this.durationInterval), this.renderer.stop();
1073
+ });
1074
+ this.ctx.onClose(() => {
1075
+ clearInterval(this.durationInterval);
1076
+ this.renderer.stop();
919
1077
  });
920
1078
  }
921
1079
  onTestRunStart(specifications) {
922
- this.runningModules.clear(), this.finishedModules.clear(), this.modules = emptyCounters(), this.tests = emptyCounters(), this.startTimers(), this.renderer.start(), this.modules.total = specifications.length;
1080
+ this.runningModules.clear();
1081
+ this.finishedModules.clear();
1082
+ this.modules = emptyCounters();
1083
+ this.tests = emptyCounters();
1084
+ this.startTimers();
1085
+ this.renderer.start();
1086
+ this.modules.total = specifications.length;
923
1087
  }
924
1088
  onTestRunEnd() {
925
- this.runningModules.clear(), this.finishedModules.clear(), this.renderer.finish(), clearInterval(this.durationInterval);
1089
+ this.runningModules.clear();
1090
+ this.finishedModules.clear();
1091
+ this.renderer.finish();
1092
+ clearInterval(this.durationInterval);
926
1093
  }
927
1094
  onTestModuleQueued(module) {
928
1095
  // When new test module starts, take the place of previously finished test module, if any
@@ -930,13 +1097,20 @@ class SummaryReporter {
930
1097
  const finished = this.finishedModules.keys().next().value;
931
1098
  this.removeTestModule(finished);
932
1099
  }
933
- this.runningModules.set(module.id, initializeStats(module)), this.renderer.schedule();
1100
+ this.runningModules.set(module.id, initializeStats(module));
1101
+ this.renderer.schedule();
934
1102
  }
935
1103
  onTestModuleCollected(module) {
936
1104
  let stats = this.runningModules.get(module.id);
937
- if (!stats) stats = initializeStats(module), this.runningModules.set(module.id, stats);
1105
+ if (!stats) {
1106
+ stats = initializeStats(module);
1107
+ this.runningModules.set(module.id, stats);
1108
+ }
938
1109
  const total = Array.from(module.children.allTests()).length;
939
- this.tests.total += total, stats.total = total, this.maxParallelTests = Math.max(this.maxParallelTests, this.runningModules.size), this.renderer.schedule();
1110
+ this.tests.total += total;
1111
+ stats.total = total;
1112
+ this.maxParallelTests = Math.max(this.maxParallelTests, this.runningModules.size);
1113
+ this.renderer.schedule();
940
1114
  }
941
1115
  onHookStart(options) {
942
1116
  const stats = this.getHookStats(options);
@@ -947,7 +1121,8 @@ class SummaryReporter {
947
1121
  startTime: performance.now(),
948
1122
  onFinish: () => {}
949
1123
  };
950
- stats.hook?.onFinish?.(), stats.hook = hook;
1124
+ stats.hook?.onFinish?.();
1125
+ stats.hook = hook;
951
1126
  const timeout = setTimeout(() => {
952
1127
  hook.visible = true;
953
1128
  }, this.ctx.config.slowTestThreshold).unref();
@@ -955,7 +1130,9 @@ class SummaryReporter {
955
1130
  }
956
1131
  onHookEnd(options) {
957
1132
  const stats = this.getHookStats(options);
958
- stats?.hook?.name === options.name && (stats.hook.onFinish(), stats.hook.visible = false);
1133
+ if (stats?.hook?.name !== options.name) return;
1134
+ stats.hook.onFinish();
1135
+ stats.hook.visible = false;
959
1136
  }
960
1137
  onTestCaseReady(test) {
961
1138
  // Track slow running tests only on verbose mode
@@ -967,17 +1144,22 @@ class SummaryReporter {
967
1144
  visible: false,
968
1145
  startTime: performance.now(),
969
1146
  onFinish: () => {}
970
- }, timeout = setTimeout(() => {
1147
+ };
1148
+ const timeout = setTimeout(() => {
971
1149
  slowTest.visible = true;
972
1150
  }, this.ctx.config.slowTestThreshold).unref();
973
1151
  slowTest.onFinish = () => {
974
- slowTest.hook?.onFinish(), clearTimeout(timeout);
975
- }, stats.tests.set(test.id, slowTest);
1152
+ slowTest.hook?.onFinish();
1153
+ clearTimeout(timeout);
1154
+ };
1155
+ stats.tests.set(test.id, slowTest);
976
1156
  }
977
1157
  onTestCaseResult(test) {
978
1158
  const stats = this.runningModules.get(test.module.id);
979
1159
  if (!stats) return;
980
- stats.tests.get(test.id)?.onFinish(), stats.tests.delete(test.id), stats.completed++;
1160
+ stats.tests.get(test.id)?.onFinish();
1161
+ stats.tests.delete(test.id);
1162
+ stats.completed++;
981
1163
  const result = test.result();
982
1164
  if (result?.state === "passed") this.tests.passed++;
983
1165
  else if (result?.state === "failed") this.tests.failed++;
@@ -986,7 +1168,8 @@ class SummaryReporter {
986
1168
  }
987
1169
  onTestModuleEnd(module) {
988
1170
  const state = module.state();
989
- if (this.modules.completed++, state === "passed") this.modules.passed++;
1171
+ this.modules.completed++;
1172
+ if (state === "passed") this.modules.passed++;
990
1173
  else if (state === "failed") this.modules.failed++;
991
1174
  else if (module.task.mode === "todo" && state === "skipped") this.modules.todo++;
992
1175
  else if (state === "skipped") this.modules.skipped++;
@@ -1005,8 +1188,10 @@ class SummaryReporter {
1005
1188
  getHookStats({ entity }) {
1006
1189
  // Track slow running hooks only on verbose mode
1007
1190
  if (!this.options.verbose) return;
1008
- const module = entity.type === "module" ? entity : entity.module, stats = this.runningModules.get(module.id);
1009
- if (stats) return entity.type === "test" ? stats.tests.get(entity.id) : stats;
1191
+ const module = entity.type === "module" ? entity : entity.module;
1192
+ const stats = this.runningModules.get(module.id);
1193
+ if (!stats) return;
1194
+ return entity.type === "test" ? stats.tests.get(entity.id) : stats;
1010
1195
  }
1011
1196
  createSummary() {
1012
1197
  const summary = [""];
@@ -1018,23 +1203,36 @@ class SummaryReporter {
1018
1203
  }) + typecheck + testFile.filename + c.dim(!testFile.completed && !testFile.total ? " [queued]" : ` ${testFile.completed}/${testFile.total}`));
1019
1204
  const slowTasks = [testFile.hook, ...testFile.tests.values()].filter((t) => t != null && t.visible);
1020
1205
  for (const [index, task] of slowTasks.entries()) {
1021
- const elapsed = this.currentTime - task.startTime, icon = index === slowTasks.length - 1 ? F_TREE_NODE_END : F_TREE_NODE_MIDDLE;
1022
- if (summary.push(c.bold(c.yellow(` ${icon} `)) + task.name + c.bold(c.yellow(` ${formatTime(Math.max(0, elapsed))}`))), task.hook?.visible) summary.push(c.bold(c.yellow(` ${F_TREE_NODE_END} `)) + task.hook.name);
1206
+ const elapsed = this.currentTime - task.startTime;
1207
+ const icon = index === slowTasks.length - 1 ? F_TREE_NODE_END : F_TREE_NODE_MIDDLE;
1208
+ summary.push(c.bold(c.yellow(` ${icon} `)) + task.name + c.bold(c.yellow(` ${formatTime(Math.max(0, elapsed))}`)));
1209
+ if (task.hook?.visible) summary.push(c.bold(c.yellow(` ${F_TREE_NODE_END} `)) + task.hook.name);
1023
1210
  }
1024
1211
  }
1025
1212
  if (this.runningModules.size > 0) summary.push("");
1026
- return summary.push(padSummaryTitle("Test Files") + getStateString(this.modules)), summary.push(padSummaryTitle("Tests") + getStateString(this.tests)), summary.push(padSummaryTitle("Start at") + this.startTime), summary.push(padSummaryTitle("Duration") + formatTime(this.duration)), summary.push(""), summary;
1213
+ summary.push(padSummaryTitle("Test Files") + getStateString(this.modules));
1214
+ summary.push(padSummaryTitle("Tests") + getStateString(this.tests));
1215
+ summary.push(padSummaryTitle("Start at") + this.startTime);
1216
+ summary.push(padSummaryTitle("Duration") + formatTime(this.duration));
1217
+ summary.push("");
1218
+ return summary;
1027
1219
  }
1028
1220
  startTimers() {
1029
1221
  const start = performance.now();
1030
- this.startTime = formatTimeString(/* @__PURE__ */ new Date()), this.durationInterval = setInterval(() => {
1031
- this.currentTime = performance.now(), this.duration = this.currentTime - start;
1222
+ this.startTime = formatTimeString(/* @__PURE__ */ new Date());
1223
+ this.durationInterval = setInterval(() => {
1224
+ this.currentTime = performance.now();
1225
+ this.duration = this.currentTime - start;
1032
1226
  }, DURATION_UPDATE_INTERVAL_MS).unref();
1033
1227
  }
1034
1228
  removeTestModule(id) {
1035
1229
  if (!id) return;
1036
1230
  const testFile = this.runningModules.get(id);
1037
- testFile?.hook?.onFinish(), testFile?.tests?.forEach((test) => test.onFinish()), this.runningModules.delete(id), clearTimeout(this.finishedModules.get(id)), this.finishedModules.delete(id);
1231
+ testFile?.hook?.onFinish();
1232
+ testFile?.tests?.forEach((test) => test.onFinish());
1233
+ this.runningModules.delete(id);
1234
+ clearTimeout(this.finishedModules.get(id));
1235
+ this.finishedModules.delete(id);
1038
1236
  }
1039
1237
  }
1040
1238
  function emptyCounters() {
@@ -1056,7 +1254,9 @@ function getStateString(entry) {
1056
1254
  ].filter(Boolean).join(c.dim(" | ")) + c.gray(` (${entry.total})`);
1057
1255
  }
1058
1256
  function sortRunningModules(a, b) {
1059
- return (a.projectName || "") > (b.projectName || "") ? 1 : (a.projectName || "") < (b.projectName || "") ? -1 : a.filename.localeCompare(b.filename);
1257
+ if ((a.projectName || "") > (b.projectName || "")) return 1;
1258
+ if ((a.projectName || "") < (b.projectName || "")) return -1;
1259
+ return a.filename.localeCompare(b.filename);
1060
1260
  }
1061
1261
  function initializeStats(module) {
1062
1262
  return {
@@ -1074,10 +1274,12 @@ class DefaultReporter extends BaseReporter {
1074
1274
  options;
1075
1275
  summary;
1076
1276
  constructor(options = {}) {
1077
- if (super(options), this.options = {
1277
+ super(options);
1278
+ this.options = {
1078
1279
  summary: true,
1079
1280
  ...options
1080
- }, !this.isTTY) this.options.summary = false;
1281
+ };
1282
+ if (!this.isTTY) this.options.summary = false;
1081
1283
  if (this.options.summary) this.summary = new SummaryReporter();
1082
1284
  }
1083
1285
  onTestRunStart(specifications) {
@@ -1088,7 +1290,8 @@ class DefaultReporter extends BaseReporter {
1088
1290
  this.summary?.onTestRunStart(specifications);
1089
1291
  }
1090
1292
  onTestRunEnd(testModules, unhandledErrors, reason) {
1091
- super.onTestRunEnd(testModules, unhandledErrors, reason), this.summary?.onTestRunEnd();
1293
+ super.onTestRunEnd(testModules, unhandledErrors, reason);
1294
+ this.summary?.onTestRunEnd();
1092
1295
  }
1093
1296
  onTestModuleQueued(file) {
1094
1297
  this.summary?.onTestModuleQueued(file);
@@ -1097,13 +1300,15 @@ class DefaultReporter extends BaseReporter {
1097
1300
  this.summary?.onTestModuleCollected(module);
1098
1301
  }
1099
1302
  onTestModuleEnd(module) {
1100
- super.onTestModuleEnd(module), this.summary?.onTestModuleEnd(module);
1303
+ super.onTestModuleEnd(module);
1304
+ this.summary?.onTestModuleEnd(module);
1101
1305
  }
1102
1306
  onTestCaseReady(test) {
1103
1307
  this.summary?.onTestCaseReady(test);
1104
1308
  }
1105
1309
  onTestCaseResult(test) {
1106
- super.onTestCaseResult(test), this.summary?.onTestCaseResult(test);
1310
+ super.onTestCaseResult(test);
1311
+ this.summary?.onTestCaseResult(test);
1107
1312
  }
1108
1313
  onHookStart(hook) {
1109
1314
  this.summary?.onHookStart(hook);
@@ -1112,7 +1317,8 @@ class DefaultReporter extends BaseReporter {
1112
1317
  this.summary?.onHookEnd(hook);
1113
1318
  }
1114
1319
  onInit(ctx) {
1115
- super.onInit(ctx), this.summary?.onInit(ctx, { verbose: this.verbose });
1320
+ super.onInit(ctx);
1321
+ this.summary?.onInit(ctx, { verbose: this.verbose });
1116
1322
  }
1117
1323
  }
1118
1324
 
@@ -1121,22 +1327,30 @@ class DotReporter extends BaseReporter {
1121
1327
  tests = /* @__PURE__ */ new Map();
1122
1328
  finishedTests = /* @__PURE__ */ new Set();
1123
1329
  onInit(ctx) {
1124
- if (super.onInit(ctx), this.isTTY) this.renderer = new WindowRenderer({
1125
- logger: ctx.logger,
1126
- getWindow: () => this.createSummary()
1127
- }), this.ctx.onClose(() => this.renderer?.stop());
1330
+ super.onInit(ctx);
1331
+ if (this.isTTY) {
1332
+ this.renderer = new WindowRenderer({
1333
+ logger: ctx.logger,
1334
+ getWindow: () => this.createSummary()
1335
+ });
1336
+ this.ctx.onClose(() => this.renderer?.stop());
1337
+ }
1128
1338
  }
1129
1339
  // Ignore default logging of base reporter
1130
1340
  printTestModule() {}
1131
1341
  onWatcherRerun(files, trigger) {
1132
- this.tests.clear(), this.renderer?.start(), super.onWatcherRerun(files, trigger);
1342
+ this.tests.clear();
1343
+ this.renderer?.start();
1344
+ super.onWatcherRerun(files, trigger);
1133
1345
  }
1134
1346
  onTestRunEnd(testModules, unhandledErrors, reason) {
1135
1347
  if (this.isTTY) {
1136
1348
  const finalLog = formatTests(Array.from(this.tests.values()));
1137
1349
  this.ctx.logger.log(finalLog);
1138
1350
  } else this.ctx.logger.log();
1139
- this.tests.clear(), this.renderer?.finish(), super.onTestRunEnd(testModules, unhandledErrors, reason);
1351
+ this.tests.clear();
1352
+ this.renderer?.finish();
1353
+ super.onTestRunEnd(testModules, unhandledErrors, reason);
1140
1354
  }
1141
1355
  onTestModuleCollected(module) {
1142
1356
  for (const test of module.children.allTests())
@@ -1144,16 +1358,22 @@ class DotReporter extends BaseReporter {
1144
1358
  this.onTestCaseReady(test);
1145
1359
  }
1146
1360
  onTestCaseReady(test) {
1147
- this.finishedTests.has(test.id) || (this.tests.set(test.id, test.result().state || "run"), this.renderer?.schedule());
1361
+ if (this.finishedTests.has(test.id)) return;
1362
+ this.tests.set(test.id, test.result().state || "run");
1363
+ this.renderer?.schedule();
1148
1364
  }
1149
1365
  onTestCaseResult(test) {
1150
1366
  const result = test.result().state;
1151
1367
  // On non-TTY the finished tests are printed immediately
1152
1368
  if (!this.isTTY && result !== "pending") this.ctx.logger.outputStream.write(formatTests([result]));
1153
- super.onTestCaseResult(test), this.finishedTests.add(test.id), this.tests.set(test.id, result || "skipped"), this.renderer?.schedule();
1369
+ super.onTestCaseResult(test);
1370
+ this.finishedTests.add(test.id);
1371
+ this.tests.set(test.id, result || "skipped");
1372
+ this.renderer?.schedule();
1154
1373
  }
1155
1374
  onTestModuleEnd(testModule) {
1156
- if (super.onTestModuleEnd(testModule), !this.isTTY) return;
1375
+ super.onTestModuleEnd(testModule);
1376
+ if (!this.isTTY) return;
1157
1377
  const columns = this.ctx.logger.getColumns();
1158
1378
  if (this.tests.size < columns) return;
1159
1379
  const finishedTests = Array.from(this.tests).filter((entry) => entry[1] !== "pending");
@@ -1163,9 +1383,11 @@ class DotReporter extends BaseReporter {
1163
1383
  let count = 0;
1164
1384
  for (const [id, state] of finishedTests) {
1165
1385
  if (count++ >= columns) break;
1166
- this.tests.delete(id), states.push(state);
1386
+ this.tests.delete(id);
1387
+ states.push(state);
1167
1388
  }
1168
- this.ctx.logger.log(formatTests(states)), this.renderer?.schedule();
1389
+ this.ctx.logger.log(formatTests(states));
1390
+ this.renderer?.schedule();
1169
1391
  }
1170
1392
  createSummary() {
1171
1393
  return [formatTests(Array.from(this.tests.values())), ""];
@@ -1175,13 +1397,16 @@ class DotReporter extends BaseReporter {
1175
1397
  const pass = {
1176
1398
  char: "·",
1177
1399
  color: c.green
1178
- }, fail = {
1400
+ };
1401
+ const fail = {
1179
1402
  char: "x",
1180
1403
  color: c.red
1181
- }, pending = {
1404
+ };
1405
+ const pending = {
1182
1406
  char: "*",
1183
1407
  color: c.yellow
1184
- }, skip = {
1408
+ };
1409
+ const skip = {
1185
1410
  char: "-",
1186
1411
  color: (char) => c.dim(c.gray(char))
1187
1412
  };
@@ -1198,16 +1423,22 @@ function getIcon(state) {
1198
1423
  * Sibling icons with same color are merged into a single c.color() call.
1199
1424
  */
1200
1425
  function formatTests(states) {
1201
- let currentIcon = pending, count = 0, output = "";
1426
+ let currentIcon = pending;
1427
+ let count = 0;
1428
+ let output = "";
1202
1429
  for (const state of states) {
1203
1430
  const icon = getIcon(state);
1204
1431
  if (currentIcon === icon) {
1205
1432
  count++;
1206
1433
  continue;
1207
1434
  }
1208
- output += currentIcon.color(currentIcon.char.repeat(count)), count = 1, currentIcon = icon;
1435
+ output += currentIcon.color(currentIcon.char.repeat(count));
1436
+ // Start tracking new group
1437
+ count = 1;
1438
+ currentIcon = icon;
1209
1439
  }
1210
- return output += currentIcon.color(currentIcon.char.repeat(count)), output;
1440
+ output += currentIcon.color(currentIcon.char.repeat(count));
1441
+ return output;
1211
1442
  }
1212
1443
 
1213
1444
  // src/vlq.ts
@@ -2102,10 +2333,13 @@ base.MethodDefinition = base.PropertyDefinition = base.Property = function (node
2102
2333
  async function collectTests(ctx, filepath) {
2103
2334
  const request = await ctx.vite.environments.ssr.transformRequest(filepath);
2104
2335
  if (!request) return null;
2105
- const ast = await parseAstAsync(request.code), testFilepath = relative(ctx.config.root, filepath), projectName = ctx.name, typecheckSubprojectName = projectName ? `${projectName}:__typecheck__` : "__typecheck__", file = {
2336
+ const ast = await parseAstAsync(request.code);
2337
+ const testFilepath = relative(ctx.config.root, filepath);
2338
+ const projectName = ctx.name;
2339
+ const file = {
2106
2340
  filepath,
2107
2341
  type: "suite",
2108
- id: generateHash(`${testFilepath}${typecheckSubprojectName}`),
2342
+ id: generateHash(`${testFilepath}${projectName ? `${projectName}:__typecheck__` : "__typecheck__"}`),
2109
2343
  name: testFilepath,
2110
2344
  mode: "run",
2111
2345
  tasks: [],
@@ -2116,19 +2350,24 @@ async function collectTests(ctx, filepath) {
2116
2350
  file: null
2117
2351
  };
2118
2352
  file.file = file;
2119
- const definitions = [], getName = (callee) => {
2353
+ const definitions = [];
2354
+ const getName = (callee) => {
2120
2355
  if (!callee) return null;
2121
2356
  if (callee.type === "Identifier") return callee.name;
2122
2357
  if (callee.type === "CallExpression") return getName(callee.callee);
2123
2358
  if (callee.type === "TaggedTemplateExpression") return getName(callee.tag);
2124
- if (callee.type === "MemberExpression")
2125
- // call as `__vite_ssr__.test.skip()`
2126
- return callee.object?.type === "Identifier" && [
2127
- "it",
2128
- "test",
2129
- "describe",
2130
- "suite"
2131
- ].includes(callee.object.name) ? callee.object?.name : callee.object?.name?.startsWith("__vite_ssr_") ? getName(callee.property) : getName(callee.object?.property);
2359
+ if (callee.type === "MemberExpression") {
2360
+ if (callee.object?.type === "Identifier" && [
2361
+ "it",
2362
+ "test",
2363
+ "describe",
2364
+ "suite"
2365
+ ].includes(callee.object.name)) return callee.object?.name;
2366
+ // direct call as `__vite_ssr_exports_0__.test()`
2367
+ if (callee.object?.name?.startsWith("__vite_ssr_")) return getName(callee.property);
2368
+ // call as `__vite_ssr__.test.skip()`
2369
+ return getName(callee.object?.property);
2370
+ }
2132
2371
  // unwrap (0, ...)
2133
2372
  if (callee.type === "SequenceExpression" && callee.expressions.length === 2) {
2134
2373
  const [e0, e1] = callee.expressions;
@@ -2137,8 +2376,10 @@ async function collectTests(ctx, filepath) {
2137
2376
  return null;
2138
2377
  };
2139
2378
  ancestor(ast, { CallExpression(node) {
2140
- const { callee } = node, name = getName(callee);
2141
- if (!name || ![
2379
+ const { callee } = node;
2380
+ const name = getName(callee);
2381
+ if (!name) return;
2382
+ if (![
2142
2383
  "it",
2143
2384
  "test",
2144
2385
  "describe",
@@ -2159,7 +2400,8 @@ async function collectTests(ctx, filepath) {
2159
2400
  if (callee.type === "CallExpression") start = callee.end;
2160
2401
  else if (callee.type === "TaggedTemplateExpression") start = callee.end + 1;
2161
2402
  else start = node.start;
2162
- const { arguments: [messageNode] } = node, message = messageNode?.type === "Literal" || messageNode?.type === "TemplateLiteral" ? request.code.slice(messageNode.start + 1, messageNode.end - 1) : request.code.slice(messageNode.start, messageNode.end);
2403
+ const { arguments: [messageNode] } = node;
2404
+ const message = messageNode?.type === "Literal" || messageNode?.type === "TemplateLiteral" ? request.code.slice(messageNode.start + 1, messageNode.end - 1) : request.code.slice(messageNode.start, messageNode.end);
2163
2405
  // cannot statically analyze, so we always skip it
2164
2406
  if (mode === "skipIf" || mode === "runIf") mode = "skip";
2165
2407
  definitions.push({
@@ -2195,7 +2437,9 @@ async function collectTests(ctx, filepath) {
2195
2437
  start: definition.start,
2196
2438
  meta: { typecheck: true }
2197
2439
  };
2198
- definition.task = task, latestSuite.tasks.push(task), lastSuite = task;
2440
+ definition.task = task;
2441
+ latestSuite.tasks.push(task);
2442
+ lastSuite = task;
2199
2443
  return;
2200
2444
  }
2201
2445
  const task = {
@@ -2212,10 +2456,13 @@ async function collectTests(ctx, filepath) {
2212
2456
  annotations: [],
2213
2457
  meta: { typecheck: true }
2214
2458
  };
2215
- definition.task = task, latestSuite.tasks.push(task);
2216
- }), calculateSuiteHash(file);
2459
+ definition.task = task;
2460
+ latestSuite.tasks.push(task);
2461
+ });
2462
+ calculateSuiteHash(file);
2217
2463
  const hasOnly = someTasksAreOnly(file);
2218
- return interpretTaskModes(file, ctx.config.testNamePattern, void 0, hasOnly, false, ctx.config.allowOnly), {
2464
+ interpretTaskModes(file, ctx.config.testNamePattern, void 0, hasOnly, false, ctx.config.allowOnly);
2465
+ return {
2219
2466
  file,
2220
2467
  parsed: request.code,
2221
2468
  filepath,
@@ -2224,11 +2471,14 @@ async function collectTests(ctx, filepath) {
2224
2471
  };
2225
2472
  }
2226
2473
 
2227
- const newLineRegExp = /\r?\n/, errCodeRegExp = /error TS(?<errCode>\d+)/;
2474
+ const newLineRegExp = /\r?\n/;
2475
+ const errCodeRegExp = /error TS(?<errCode>\d+)/;
2228
2476
  async function makeTscErrorInfo(errInfo) {
2229
2477
  const [errFilePathPos = "", ...errMsgRawArr] = errInfo.split(":");
2230
2478
  if (!errFilePathPos || errMsgRawArr.length === 0 || errMsgRawArr.join("").length === 0) return ["unknown filepath", null];
2231
- const errMsgRaw = errMsgRawArr.join("").trim(), [errFilePath, errPos] = errFilePathPos.slice(0, -1).split("(");
2479
+ const errMsgRaw = errMsgRawArr.join("").trim();
2480
+ // get filePath, line, col
2481
+ const [errFilePath, errPos] = errFilePathPos.slice(0, -1).split("(");
2232
2482
  if (!errFilePath || !errPos) return ["unknown filepath", null];
2233
2483
  const [errLine, errCol] = errPos.split(",");
2234
2484
  if (!errLine || !errCol) return [errFilePath, null];
@@ -2237,7 +2487,9 @@ async function makeTscErrorInfo(errInfo) {
2237
2487
  if (!execArr) return [errFilePath, null];
2238
2488
  const errCodeStr = execArr.groups?.errCode ?? "";
2239
2489
  if (!errCodeStr) return [errFilePath, null];
2240
- const line = Number(errLine), col = Number(errCol), errCode = Number(errCodeStr);
2490
+ const line = Number(errLine);
2491
+ const col = Number(errCol);
2492
+ const errCode = Number(errCodeStr);
2241
2493
  return [errFilePath, {
2242
2494
  filePath: errFilePath,
2243
2495
  errCode,
@@ -2248,29 +2500,40 @@ async function makeTscErrorInfo(errInfo) {
2248
2500
  }
2249
2501
  async function getRawErrsMapFromTsCompile(tscErrorStdout) {
2250
2502
  const rawErrsMap = /* @__PURE__ */ new Map();
2251
- return (await Promise.all(tscErrorStdout.split(newLineRegExp).reduce((prev, next) => {
2503
+ (await Promise.all(tscErrorStdout.split(newLineRegExp).reduce((prev, next) => {
2252
2504
  if (!next) return prev;
2253
- if (next[0] !== " ") prev.push(next);
2505
+ else if (next[0] !== " ") prev.push(next);
2254
2506
  else prev[prev.length - 1] += `\n${next}`;
2255
2507
  return prev;
2256
2508
  }, []).map((errInfoLine) => makeTscErrorInfo(errInfoLine)))).forEach(([errFilePath, errInfo]) => {
2257
- if (errInfo) if (!rawErrsMap.has(errFilePath)) rawErrsMap.set(errFilePath, [errInfo]);
2509
+ if (!errInfo) return;
2510
+ if (!rawErrsMap.has(errFilePath)) rawErrsMap.set(errFilePath, [errInfo]);
2258
2511
  else rawErrsMap.get(errFilePath)?.push(errInfo);
2259
- }), rawErrsMap;
2512
+ });
2513
+ return rawErrsMap;
2260
2514
  }
2261
2515
 
2262
2516
  function createIndexMap(source) {
2263
2517
  const map = /* @__PURE__ */ new Map();
2264
- let index = 0, line = 1, column = 1;
2265
- for (const char of source) if (map.set(`${line}:${column}`, index++), char === "\n" || char === "\r\n") line++, column = 0;
2266
- else column++;
2518
+ let index = 0;
2519
+ let line = 1;
2520
+ let column = 1;
2521
+ for (const char of source) {
2522
+ map.set(`${line}:${column}`, index++);
2523
+ if (char === "\n" || char === "\r\n") {
2524
+ line++;
2525
+ column = 0;
2526
+ } else column++;
2527
+ }
2267
2528
  return map;
2268
2529
  }
2269
2530
 
2270
2531
  class TypeCheckError extends Error {
2271
2532
  name = "TypeCheckError";
2272
2533
  constructor(message, stacks) {
2273
- super(message), this.message = message, this.stacks = stacks;
2534
+ super(message);
2535
+ this.message = message;
2536
+ this.stacks = stacks;
2274
2537
  }
2275
2538
  }
2276
2539
  class Typechecker {
@@ -2310,9 +2573,12 @@ class Typechecker {
2310
2573
  }
2311
2574
  async collectTests() {
2312
2575
  const tests = (await Promise.all(this.getFiles().map((filepath) => this.collectFileTests(filepath)))).reduce((acc, data) => {
2313
- return data && (acc[data.filepath] = data), acc;
2576
+ if (!data) return acc;
2577
+ acc[data.filepath] = data;
2578
+ return acc;
2314
2579
  }, {});
2315
- return this._tests = tests, tests;
2580
+ this._tests = tests;
2581
+ return tests;
2316
2582
  }
2317
2583
  markPassed(file) {
2318
2584
  if (!file.result?.state) file.result = { state: "pass" };
@@ -2325,17 +2591,26 @@ class Typechecker {
2325
2591
  markTasks(file.tasks);
2326
2592
  }
2327
2593
  async prepareResults(output) {
2328
- const typeErrors = await this.parseTscLikeOutput(output), testFiles = new Set(this.getFiles());
2594
+ const typeErrors = await this.parseTscLikeOutput(output);
2595
+ const testFiles = new Set(this.getFiles());
2329
2596
  if (!this._tests) this._tests = await this.collectTests();
2330
- const sourceErrors = [], files = [];
2331
- return testFiles.forEach((path) => {
2332
- const { file, definitions, map, parsed } = this._tests[path], errors = typeErrors.get(path);
2333
- if (files.push(file), !errors) {
2597
+ const sourceErrors = [];
2598
+ const files = [];
2599
+ testFiles.forEach((path) => {
2600
+ const { file, definitions, map, parsed } = this._tests[path];
2601
+ const errors = typeErrors.get(path);
2602
+ files.push(file);
2603
+ if (!errors) {
2334
2604
  this.markPassed(file);
2335
2605
  return;
2336
2606
  }
2337
- const sortedDefinitions = [...definitions.sort((a, b) => b.start - a.start)], traceMap = map && new TraceMap(map), indexMap = createIndexMap(parsed), markState = (task, state) => {
2338
- if (task.result = { state: task.mode === "run" || task.mode === "only" ? state : task.mode }, task.suite) markState(task.suite, state);
2607
+ const sortedDefinitions = [...definitions.sort((a, b) => b.start - a.start)];
2608
+ // has no map for ".js" files that use // @ts-check
2609
+ const traceMap = map && new TraceMap(map);
2610
+ const indexMap = createIndexMap(parsed);
2611
+ const markState = (task, state) => {
2612
+ task.result = { state: task.mode === "run" || task.mode === "only" ? state : task.mode };
2613
+ if (task.suite) markState(task.suite, state);
2339
2614
  else if (task.file && task !== task.file) markState(task.file, state);
2340
2615
  };
2341
2616
  errors.forEach(({ error, originalError }) => {
@@ -2343,37 +2618,53 @@ class Typechecker {
2343
2618
  line: originalError.line,
2344
2619
  column: originalError.column,
2345
2620
  source: basename(path)
2346
- }) : originalError, line = processedPos.line ?? originalError.line, column = processedPos.column ?? originalError.column, index = indexMap.get(`${line}:${column}`), definition = index != null && sortedDefinitions.find((def) => def.start <= index && def.end >= index), suite = definition ? definition.task : file, state = suite.mode === "run" || suite.mode === "only" ? "fail" : suite.mode, errors = suite.result?.errors || [];
2347
- if (suite.result = {
2621
+ }) : originalError;
2622
+ const line = processedPos.line ?? originalError.line;
2623
+ const column = processedPos.column ?? originalError.column;
2624
+ const index = indexMap.get(`${line}:${column}`);
2625
+ const definition = index != null && sortedDefinitions.find((def) => def.start <= index && def.end >= index);
2626
+ const suite = definition ? definition.task : file;
2627
+ const state = suite.mode === "run" || suite.mode === "only" ? "fail" : suite.mode;
2628
+ const errors = suite.result?.errors || [];
2629
+ suite.result = {
2348
2630
  state,
2349
2631
  errors
2350
- }, errors.push(error), state === "fail") {
2632
+ };
2633
+ errors.push(error);
2634
+ if (state === "fail") {
2351
2635
  if (suite.suite) markState(suite.suite, "fail");
2352
2636
  else if (suite.file && suite !== suite.file) markState(suite.file, "fail");
2353
2637
  }
2354
- }), this.markPassed(file);
2355
- }), typeErrors.forEach((errors, path) => {
2638
+ });
2639
+ this.markPassed(file);
2640
+ });
2641
+ typeErrors.forEach((errors, path) => {
2356
2642
  if (!testFiles.has(path)) sourceErrors.push(...errors.map(({ error }) => error));
2357
- }), {
2643
+ });
2644
+ return {
2358
2645
  files,
2359
2646
  sourceErrors,
2360
2647
  time: performance$1.now() - this._startTime
2361
2648
  };
2362
2649
  }
2363
2650
  async parseTscLikeOutput(output) {
2364
- const errorsMap = await getRawErrsMapFromTsCompile(output), typesErrors = /* @__PURE__ */ new Map();
2365
- return errorsMap.forEach((errors, path) => {
2366
- const filepath = resolve$1(this.project.config.root, path), suiteErrors = errors.map((info) => {
2651
+ const errorsMap = await getRawErrsMapFromTsCompile(output);
2652
+ const typesErrors = /* @__PURE__ */ new Map();
2653
+ errorsMap.forEach((errors, path) => {
2654
+ const filepath = resolve$1(this.project.config.root, path);
2655
+ const suiteErrors = errors.map((info) => {
2367
2656
  const limit = Error.stackTraceLimit;
2368
2657
  Error.stackTraceLimit = 0;
2369
2658
  // Some expect-type errors have the most useful information on the second line e.g. `This expression is not callable.\n Type 'ExpectString<number>' has no call signatures.`
2370
- const errMsg = info.errMsg.replace(/\r?\n\s*(Type .* has no call signatures)/g, " $1"), error = new TypeCheckError(errMsg, [{
2659
+ const errMsg = info.errMsg.replace(/\r?\n\s*(Type .* has no call signatures)/g, " $1");
2660
+ const error = new TypeCheckError(errMsg, [{
2371
2661
  file: filepath,
2372
2662
  line: info.line,
2373
2663
  column: info.column,
2374
2664
  method: ""
2375
2665
  }]);
2376
- return Error.stackTraceLimit = limit, {
2666
+ Error.stackTraceLimit = limit;
2667
+ return {
2377
2668
  originalError: info,
2378
2669
  error: {
2379
2670
  name: error.name,
@@ -2384,10 +2675,12 @@ class Typechecker {
2384
2675
  };
2385
2676
  });
2386
2677
  typesErrors.set(filepath, suiteErrors);
2387
- }), typesErrors;
2678
+ });
2679
+ return typesErrors;
2388
2680
  }
2389
2681
  async stop() {
2390
- this.process?.kill(), this.process = void 0;
2682
+ this.process?.kill();
2683
+ this.process = void 0;
2391
2684
  }
2392
2685
  async ensurePackageInstalled(ctx, checker) {
2393
2686
  if (checker !== "tsc" && checker !== "vue-tsc") return;
@@ -2401,7 +2694,8 @@ class Typechecker {
2401
2694
  return this._output;
2402
2695
  }
2403
2696
  async spawn() {
2404
- const { root, watch, typecheck } = this.project.config, args = [
2697
+ const { root, watch, typecheck } = this.project.config;
2698
+ const args = [
2405
2699
  "--noEmit",
2406
2700
  "--pretty",
2407
2701
  "false",
@@ -2413,7 +2707,8 @@ class Typechecker {
2413
2707
  if (watch) args.push("--watch");
2414
2708
  if (typecheck.allowJs) args.push("--allowJs", "--checkJs");
2415
2709
  if (typecheck.tsconfig) args.push("-p", resolve$1(root, typecheck.tsconfig));
2416
- this._output = "", this._startTime = performance$1.now();
2710
+ this._output = "";
2711
+ this._startTime = performance$1.now();
2417
2712
  const child = x(typecheck.checker, args, {
2418
2713
  nodeOptions: {
2419
2714
  cwd: root,
@@ -2422,26 +2717,44 @@ class Typechecker {
2422
2717
  throwOnError: false
2423
2718
  });
2424
2719
  this.process = child.process;
2425
- let rerunTriggered = false, dataReceived = false;
2720
+ let rerunTriggered = false;
2721
+ let dataReceived = false;
2426
2722
  return new Promise((resolve, reject) => {
2427
2723
  if (!child.process || !child.process.stdout) {
2428
2724
  reject(/* @__PURE__ */ new Error(`Failed to initialize ${typecheck.checker}. This is a bug in Vitest - please, open an issue with reproduction.`));
2429
2725
  return;
2430
2726
  }
2431
2727
  child.process.stdout.on("data", (chunk) => {
2432
- if (dataReceived = true, this._output += chunk, watch) {
2433
- if (this._output.includes("File change detected") && !rerunTriggered) this._onWatcherRerun?.(), this._startTime = performance$1.now(), this._result.sourceErrors = [], this._result.files = [], this._tests = null, rerunTriggered = true;
2434
- if (/Found \w+ errors*. Watching for/.test(this._output)) rerunTriggered = false, this.prepareResults(this._output).then((result) => {
2435
- this._result = result, this._onParseEnd?.(result);
2436
- }), this._output = "";
2728
+ dataReceived = true;
2729
+ this._output += chunk;
2730
+ if (!watch) return;
2731
+ if (this._output.includes("File change detected") && !rerunTriggered) {
2732
+ this._onWatcherRerun?.();
2733
+ this._startTime = performance$1.now();
2734
+ this._result.sourceErrors = [];
2735
+ this._result.files = [];
2736
+ this._tests = null;
2737
+ rerunTriggered = true;
2738
+ }
2739
+ if (/Found \w+ errors*. Watching for/.test(this._output)) {
2740
+ rerunTriggered = false;
2741
+ this.prepareResults(this._output).then((result) => {
2742
+ this._result = result;
2743
+ this._onParseEnd?.(result);
2744
+ });
2745
+ this._output = "";
2437
2746
  }
2438
2747
  });
2439
2748
  const timeout = setTimeout(() => reject(/* @__PURE__ */ new Error(`${typecheck.checker} spawn timed out`)), this.project.config.typecheck.spawnTimeout);
2440
2749
  function onError(cause) {
2441
- clearTimeout(timeout), reject(new Error("Spawning typechecker failed - is typescript installed?", { cause }));
2750
+ clearTimeout(timeout);
2751
+ reject(new Error("Spawning typechecker failed - is typescript installed?", { cause }));
2442
2752
  }
2443
- if (child.process.once("spawn", () => {
2444
- if (this._onParseStart?.(), child.process?.off("error", onError), clearTimeout(timeout), process.platform === "win32")
2753
+ child.process.once("spawn", () => {
2754
+ this._onParseStart?.();
2755
+ child.process?.off("error", onError);
2756
+ clearTimeout(timeout);
2757
+ if (process.platform === "win32")
2445
2758
  // on Windows, the process might be spawned but fail to start
2446
2759
  // we wait for a potential error here. if "close" event didn't trigger,
2447
2760
  // we resolve the promise
@@ -2449,7 +2762,8 @@ class Typechecker {
2449
2762
  resolve({ result: child });
2450
2763
  }, 200);
2451
2764
  else resolve({ result: child });
2452
- }), process.platform === "win32") child.process.once("close", (code) => {
2765
+ });
2766
+ if (process.platform === "win32") child.process.once("close", (code) => {
2453
2767
  if (code != null && code !== 0 && !dataReceived) onError(/* @__PURE__ */ new Error(`The ${typecheck.checker} command exited with code ${code}.`));
2454
2768
  });
2455
2769
  child.process.once("error", onError);
@@ -2457,8 +2771,13 @@ class Typechecker {
2457
2771
  }
2458
2772
  async start() {
2459
2773
  if (this.process) return;
2460
- const { watch } = this.project.config, { result: child } = await this.spawn();
2461
- if (!watch) await child, this._result = await this.prepareResults(this._output), await this._onParseEnd?.(this._result);
2774
+ const { watch } = this.project.config;
2775
+ const { result: child } = await this.spawn();
2776
+ if (!watch) {
2777
+ await child;
2778
+ this._result = await this.prepareResults(this._output);
2779
+ await this._onParseEnd?.(this._result);
2780
+ }
2462
2781
  }
2463
2782
  getResult() {
2464
2783
  return this._result;
@@ -2467,10 +2786,12 @@ class Typechecker {
2467
2786
  return Object.values(this._tests || {}).map((i) => i.file);
2468
2787
  }
2469
2788
  getTestPacksAndEvents() {
2470
- const packs = [], events = [];
2789
+ const packs = [];
2790
+ const events = [];
2471
2791
  for (const { file } of Object.values(this._tests || {})) {
2472
2792
  const result = convertTasksToEvents(file);
2473
- packs.push(...result.packs), events.push(...result.events);
2793
+ packs.push(...result.packs);
2794
+ events.push(...result.events);
2474
2795
  }
2475
2796
  return {
2476
2797
  packs,
@@ -2493,10 +2814,11 @@ function findGeneratedPosition(traceMap, { line, column, source }) {
2493
2814
  if (m.source === source && m.originalLine !== null && m.originalColumn !== null && (line === m.originalLine ? column < m.originalColumn : line < m.originalLine)) mappings.push(m);
2494
2815
  });
2495
2816
  const next = mappings.sort((a, b) => a.originalLine === b.originalLine ? a.originalColumn - b.originalColumn : a.originalLine - b.originalLine).at(0);
2496
- return next ? {
2817
+ if (next) return {
2497
2818
  line: next.generatedLine,
2498
2819
  column: next.generatedColumn
2499
- } : {
2820
+ };
2821
+ return {
2500
2822
  line: null,
2501
2823
  column: null
2502
2824
  };
@@ -2505,14 +2827,15 @@ function findGeneratedPosition(traceMap, { line, column, source }) {
2505
2827
  // use Logger with custom Console to capture entire error printing
2506
2828
  function capturePrintError(error, ctx, options) {
2507
2829
  let output = "";
2508
- const writable = new Writable({ write(chunk, _encoding, callback) {
2509
- output += String(chunk), callback();
2510
- } }), console = new Console(writable), logger = {
2511
- error: console.error.bind(console),
2512
- highlight: ctx.logger.highlight.bind(ctx.logger)
2513
- };
2830
+ const console = new Console(new Writable({ write(chunk, _encoding, callback) {
2831
+ output += String(chunk);
2832
+ callback();
2833
+ } }));
2514
2834
  return {
2515
- nearest: printError(error, ctx, logger, {
2835
+ nearest: printError(error, ctx, {
2836
+ error: console.error.bind(console),
2837
+ highlight: ctx.logger.highlight.bind(ctx.logger)
2838
+ }, {
2516
2839
  showCodeFrame: false,
2517
2840
  ...options
2518
2841
  })?.nearest,
@@ -2528,13 +2851,18 @@ function printError(error, ctx, logger, options) {
2528
2851
  screenshotPaths: options.screenshotPaths,
2529
2852
  printProperties: options.verbose,
2530
2853
  parseErrorStacktrace(error) {
2531
- // node.js stack trace already has correct source map locations
2532
- return error.stacks ? options.fullStack ? error.stacks : error.stacks.filter((stack) => {
2854
+ if (error.stacks) if (options.fullStack) return error.stacks;
2855
+ else return error.stacks.filter((stack) => {
2533
2856
  return !defaultStackIgnorePatterns.some((p) => stack.file.match(p));
2534
- }) : options.task?.file.pool === "browser" && project.browser ? project.browser.parseErrorStacktrace(error, {
2857
+ });
2858
+ // browser stack trace needs to be processed differently,
2859
+ // so there is a separate method for that
2860
+ if (options.task?.file.pool === "browser" && project.browser) return project.browser.parseErrorStacktrace(error, {
2535
2861
  frameFilter: project.config.onStackTrace,
2536
2862
  ignoreStackEntries: options.fullStack ? [] : void 0
2537
- }) : parseErrorStacktrace(error, {
2863
+ });
2864
+ // node.js stack trace already has correct source map locations
2865
+ return parseErrorStacktrace(error, {
2538
2866
  frameFilter: project.config.onStackTrace,
2539
2867
  ignoreStackEntries: options.fullStack ? [] : void 0
2540
2868
  });
@@ -2542,7 +2870,8 @@ function printError(error, ctx, logger, options) {
2542
2870
  });
2543
2871
  }
2544
2872
  function printErrorInner(error, project, options) {
2545
- const { showCodeFrame = true, type, printProperties = true } = options, logger = options.logger;
2873
+ const { showCodeFrame = true, type, printProperties = true } = options;
2874
+ const logger = options.logger;
2546
2875
  let e = error;
2547
2876
  if (isPrimitive(e)) e = {
2548
2877
  message: String(error).split(/\n/g)[0],
@@ -2560,7 +2889,8 @@ function printErrorInner(error, project, options) {
2560
2889
  printErrorMessage(e, logger);
2561
2890
  return;
2562
2891
  }
2563
- const stacks = options.parseErrorStacktrace(e), nearest = error instanceof TypeCheckError ? error.stacks[0] : stacks.find((stack) => {
2892
+ const stacks = options.parseErrorStacktrace(e);
2893
+ const nearest = error instanceof TypeCheckError ? error.stacks[0] : stacks.find((stack) => {
2564
2894
  // we are checking that this module was processed by us at one point
2565
2895
  try {
2566
2896
  return [...Object.values(project._vite?.environments || {}), ...Object.values(project.browser?.vite.environments || {})].some((environment) => {
@@ -2571,9 +2901,13 @@ function printErrorInner(error, project, options) {
2571
2901
  }
2572
2902
  });
2573
2903
  if (type) printErrorType(type, project.vitest);
2574
- if (printErrorMessage(e, logger), options.screenshotPaths?.length) {
2575
- const uniqueScreenshots = Array.from(new Set(options.screenshotPaths)), length = uniqueScreenshots.length;
2576
- if (logger.error(`\nFailure screenshot${length > 1 ? "s" : ""}:`), logger.error(uniqueScreenshots.map((p) => ` - ${c.dim(relative(process.cwd(), p))}`).join("\n")), !e.diff) logger.error();
2904
+ printErrorMessage(e, logger);
2905
+ if (options.screenshotPaths?.length) {
2906
+ const uniqueScreenshots = Array.from(new Set(options.screenshotPaths));
2907
+ const length = uniqueScreenshots.length;
2908
+ logger.error(`\nFailure screenshot${length > 1 ? "s" : ""}:`);
2909
+ logger.error(uniqueScreenshots.map((p) => ` - ${c.dim(relative(process.cwd(), p))}`).join("\n"));
2910
+ if (!e.diff) logger.error();
2577
2911
  }
2578
2912
  if (e.codeFrame) logger.error(`${e.codeFrame}\n`);
2579
2913
  if ("__vitest_rollup_error__" in e) {
@@ -2589,27 +2923,29 @@ function printErrorInner(error, project, options) {
2589
2923
  if (e.diff) logger.error(`\n${e.diff}\n`);
2590
2924
  // if the error provide the frame
2591
2925
  if (e.frame) logger.error(c.yellow(e.frame));
2592
- else {
2593
- const errorProperties = printProperties ? getErrorProperties(e) : {};
2594
- printStack(logger, project, stacks, nearest, errorProperties, (s) => {
2595
- if (showCodeFrame && s === nearest && nearest) {
2596
- const sourceCode = readFileSync(nearest.file, "utf-8");
2597
- logger.error(generateCodeFrame(sourceCode.length > 1e5 ? sourceCode : logger.highlight(nearest.file, sourceCode), 4, s));
2598
- }
2599
- });
2600
- }
2601
- const testPath = e.VITEST_TEST_PATH, testName = e.VITEST_TEST_NAME;
2926
+ else printStack(logger, project, stacks, nearest, printProperties ? getErrorProperties(e) : {}, (s) => {
2927
+ if (showCodeFrame && s === nearest && nearest) {
2928
+ const sourceCode = readFileSync(nearest.file, "utf-8");
2929
+ logger.error(generateCodeFrame(sourceCode.length > 1e5 ? sourceCode : logger.highlight(nearest.file, sourceCode), 4, s));
2930
+ }
2931
+ });
2932
+ const testPath = e.VITEST_TEST_PATH;
2933
+ const testName = e.VITEST_TEST_NAME;
2602
2934
  // testName has testPath inside
2603
2935
  if (testPath) logger.error(c.red(`This error originated in "${c.bold(relative(project.config.root, testPath))}" test file. It doesn't mean the error was thrown inside the file itself, but while it was running.`));
2604
2936
  if (testName) logger.error(c.red(`The latest test that might've caused the error is "${c.bold(testName)}". It might mean one of the following:
2605
2937
  - The error was thrown, while Vitest was running this test.
2606
2938
  - If the error occurred after the test had been completed, this was the last documented test before it was thrown.`));
2607
- if (typeof e.cause === "object" && e.cause && "name" in e.cause) e.cause.name = `Caused by: ${e.cause.name}`, printErrorInner(e.cause, project, {
2608
- showCodeFrame: false,
2609
- logger: options.logger,
2610
- parseErrorStacktrace: options.parseErrorStacktrace
2611
- });
2612
- return handleImportOutsideModuleError(e.stack || "", logger), { nearest };
2939
+ if (typeof e.cause === "object" && e.cause && "name" in e.cause) {
2940
+ e.cause.name = `Caused by: ${e.cause.name}`;
2941
+ printErrorInner(e.cause, project, {
2942
+ showCodeFrame: false,
2943
+ logger: options.logger,
2944
+ parseErrorStacktrace: options.parseErrorStacktrace
2945
+ });
2946
+ }
2947
+ handleImportOutsideModuleError(e.stack || "", logger);
2948
+ return { nearest };
2613
2949
  }
2614
2950
  function printErrorType(type, ctx) {
2615
2951
  ctx.logger.error(`\n${errorBanner(type)}`);
@@ -2691,8 +3027,10 @@ function printErrorMessage(error, logger) {
2691
3027
  }
2692
3028
  function printStack(logger, project, stack, highlight, errorProperties, onStack) {
2693
3029
  for (const frame of stack) {
2694
- const color = frame === highlight ? c.cyan : c.gray, path = relative(project.config.root, frame.file);
2695
- logger.error(color(` ${c.dim(F_POINTER)} ${[frame.method, `${path}:${c.dim(`${frame.line}:${frame.column}`)}`].filter(Boolean).join(" ")}`)), onStack?.(frame);
3030
+ const color = frame === highlight ? c.cyan : c.gray;
3031
+ const path = relative(project.config.root, frame.file);
3032
+ logger.error(color(` ${c.dim(F_POINTER)} ${[frame.method, `${path}:${c.dim(`${frame.line}:${frame.column}`)}`].filter(Boolean).join(" ")}`));
3033
+ onStack?.(frame);
2696
3034
  }
2697
3035
  if (stack.length) logger.error();
2698
3036
  if (hasProperties(errorProperties)) {
@@ -2707,19 +3045,28 @@ function hasProperties(obj) {
2707
3045
  return false;
2708
3046
  }
2709
3047
  function generateCodeFrame(source, indent = 0, loc, range = 2) {
2710
- const start = typeof loc === "object" ? positionToOffset(source, loc.line, loc.column) : loc, end = start, lines = source.split(lineSplitRE), nl = /\r\n/.test(source) ? 2 : 1;
2711
- let count = 0, res = [];
3048
+ const start = typeof loc === "object" ? positionToOffset(source, loc.line, loc.column) : loc;
3049
+ const end = start;
3050
+ const lines = source.split(lineSplitRE);
3051
+ const nl = /\r\n/.test(source) ? 2 : 1;
3052
+ let count = 0;
3053
+ let res = [];
2712
3054
  const columns = process.stdout?.columns || 80;
2713
- for (let i = 0; i < lines.length; i++) if (count += lines[i].length + nl, count >= start) {
2714
- for (let j = i - range; j <= i + range || end > count; j++) {
2715
- if (j < 0 || j >= lines.length) continue;
2716
- const lineLength = lines[j].length, strippedContent = stripVTControlCharacters(lines[j]);
2717
- if (!strippedContent.startsWith("//# sourceMappingURL")) {
3055
+ for (let i = 0; i < lines.length; i++) {
3056
+ count += lines[i].length + nl;
3057
+ if (count >= start) {
3058
+ for (let j = i - range; j <= i + range || end > count; j++) {
3059
+ if (j < 0 || j >= lines.length) continue;
3060
+ const lineLength = lines[j].length;
3061
+ const strippedContent = stripVTControlCharacters(lines[j]);
3062
+ if (strippedContent.startsWith("//# sourceMappingURL")) continue;
2718
3063
  // too long, maybe it's a minified file, skip for codeframe
2719
3064
  if (strippedContent.length > 200) return "";
2720
- if (res.push(lineNo(j + 1) + truncateString(lines[j].replace(/\t/g, " "), columns - 5 - indent)), j === i) {
3065
+ res.push(lineNo(j + 1) + truncateString(lines[j].replace(/\t/g, " "), columns - 5 - indent));
3066
+ if (j === i) {
2721
3067
  // push underline
2722
- const pad = start - (count - lineLength) + (nl - 1), length = Math.max(1, end > count ? lineLength - pad : end - start);
3068
+ const pad = start - (count - lineLength) + (nl - 1);
3069
+ const length = Math.max(1, end > count ? lineLength - pad : end - start);
2723
3070
  res.push(lineNo() + " ".repeat(pad) + c.red("^".repeat(length)));
2724
3071
  } else if (j > i) {
2725
3072
  if (end > count) {
@@ -2729,8 +3076,8 @@ function generateCodeFrame(source, indent = 0, loc, range = 2) {
2729
3076
  count += lineLength + 1;
2730
3077
  }
2731
3078
  }
3079
+ break;
2732
3080
  }
2733
- break;
2734
3081
  }
2735
3082
  if (indent) res = res.map((line) => " ".repeat(indent) + line);
2736
3083
  return res.join("\n");
@@ -2750,7 +3097,8 @@ class GithubActionsReporter {
2750
3097
  }
2751
3098
  onTestCaseAnnotate(testCase, annotation) {
2752
3099
  if (!annotation.location || this.options.displayAnnotations === false) return;
2753
- const type = getTitle(annotation.type), formatted = formatMessage({
3100
+ const type = getTitle(annotation.type);
3101
+ const formatted = formatMessage({
2754
3102
  command: getType(annotation.type),
2755
3103
  properties: {
2756
3104
  file: annotation.location.file,
@@ -2763,14 +3111,18 @@ class GithubActionsReporter {
2763
3111
  this.ctx.logger.log(`\n${formatted}`);
2764
3112
  }
2765
3113
  onTestRunEnd(testModules, unhandledErrors) {
2766
- const files = testModules.map((testModule) => testModule.task), errors = [...unhandledErrors], projectErrors = new Array();
3114
+ const files = testModules.map((testModule) => testModule.task);
3115
+ const errors = [...unhandledErrors];
3116
+ // collect all errors and associate them with projects
3117
+ const projectErrors = new Array();
2767
3118
  for (const error of errors) projectErrors.push({
2768
3119
  project: this.ctx.getRootProject(),
2769
3120
  title: "Unhandled error",
2770
3121
  error
2771
3122
  });
2772
3123
  for (const file of files) {
2773
- const tasks = getTasks(file), project = this.ctx.getProjectByName(file.projectName || "");
3124
+ const tasks = getTasks(file);
3125
+ const project = this.ctx.getProjectByName(file.projectName || "");
2774
3126
  for (const task of tasks) {
2775
3127
  if (task.result?.state !== "fail") continue;
2776
3128
  const title = getFullName(task, " > ");
@@ -2788,7 +3140,8 @@ class GithubActionsReporter {
2788
3140
  const result = capturePrintError(error, this.ctx, {
2789
3141
  project,
2790
3142
  task: file
2791
- }), stack = result?.nearest;
3143
+ });
3144
+ const stack = result?.nearest;
2792
3145
  if (!stack) continue;
2793
3146
  const formatted = formatMessage({
2794
3147
  command: "error",
@@ -2810,10 +3163,12 @@ const BUILT_IN_TYPES = [
2810
3163
  "warning"
2811
3164
  ];
2812
3165
  function getTitle(type) {
2813
- if (!BUILT_IN_TYPES.includes(type)) return type;
3166
+ if (BUILT_IN_TYPES.includes(type)) return;
3167
+ return type;
2814
3168
  }
2815
3169
  function getType(type) {
2816
- return BUILT_IN_TYPES.includes(type) ? type : "notice";
3170
+ if (BUILT_IN_TYPES.includes(type)) return type;
3171
+ return "notice";
2817
3172
  }
2818
3173
  function defaultOnWritePath(path) {
2819
3174
  return path;
@@ -2823,9 +3178,12 @@ function defaultOnWritePath(path) {
2823
3178
  // https://github.com/actions/toolkit/blob/f1d9b4b985e6f0f728b4b766db73498403fd5ca3/packages/core/src/command.ts#L80-L85
2824
3179
  function formatMessage({ command, properties, message }) {
2825
3180
  let result = `::${command}`;
2826
- return Object.entries(properties).forEach(([k, v], i) => {
2827
- result += i === 0 ? " " : ",", result += `${k}=${escapeProperty(v)}`;
2828
- }), result += `::${escapeData(message)}`, result;
3181
+ Object.entries(properties).forEach(([k, v], i) => {
3182
+ result += i === 0 ? " " : ",";
3183
+ result += `${k}=${escapeProperty(v)}`;
3184
+ });
3185
+ result += `::${escapeData(message)}`;
3186
+ return result;
2829
3187
  }
2830
3188
  function escapeData(s) {
2831
3189
  return s.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
@@ -2862,22 +3220,42 @@ class JsonReporter {
2862
3220
  this.options = options;
2863
3221
  }
2864
3222
  onInit(ctx) {
2865
- this.ctx = ctx, this.start = Date.now(), this.coverageMap = void 0;
3223
+ this.ctx = ctx;
3224
+ this.start = Date.now();
3225
+ this.coverageMap = void 0;
2866
3226
  }
2867
3227
  onCoverage(coverageMap) {
2868
3228
  this.coverageMap = coverageMap;
2869
3229
  }
2870
3230
  async onTestRunEnd(testModules) {
2871
- const files = testModules.map((testModule) => testModule.task), suites = getSuites(files), numTotalTestSuites = suites.length, tests = getTests(files), numTotalTests = tests.length, numFailedTestSuites = suites.filter((s) => s.result?.state === "fail").length, numPendingTestSuites = suites.filter((s) => s.result?.state === "run" || s.result?.state === "queued" || s.mode === "todo").length, numPassedTestSuites = numTotalTestSuites - numFailedTestSuites - numPendingTestSuites, numFailedTests = tests.filter((t) => t.result?.state === "fail").length, numPassedTests = tests.filter((t) => t.result?.state === "pass").length, numPendingTests = tests.filter((t) => t.result?.state === "run" || t.result?.state === "queued" || t.mode === "skip" || t.result?.state === "skip").length, numTodoTests = tests.filter((t) => t.mode === "todo").length, testResults = [], success = !!(files.length > 0 || this.ctx.config.passWithNoTests) && numFailedTestSuites === 0 && numFailedTests === 0;
3231
+ const files = testModules.map((testModule) => testModule.task);
3232
+ const suites = getSuites(files);
3233
+ const numTotalTestSuites = suites.length;
3234
+ const tests = getTests(files);
3235
+ const numTotalTests = tests.length;
3236
+ const numFailedTestSuites = suites.filter((s) => s.result?.state === "fail").length;
3237
+ const numPendingTestSuites = suites.filter((s) => s.result?.state === "run" || s.result?.state === "queued" || s.mode === "todo").length;
3238
+ const numPassedTestSuites = numTotalTestSuites - numFailedTestSuites - numPendingTestSuites;
3239
+ const numFailedTests = tests.filter((t) => t.result?.state === "fail").length;
3240
+ const numPassedTests = tests.filter((t) => t.result?.state === "pass").length;
3241
+ const numPendingTests = tests.filter((t) => t.result?.state === "run" || t.result?.state === "queued" || t.mode === "skip" || t.result?.state === "skip").length;
3242
+ const numTodoTests = tests.filter((t) => t.mode === "todo").length;
3243
+ const testResults = [];
3244
+ const success = !!(files.length > 0 || this.ctx.config.passWithNoTests) && numFailedTestSuites === 0 && numFailedTests === 0;
2872
3245
  for (const file of files) {
2873
3246
  const tests = getTests([file]);
2874
3247
  let startTime = tests.reduce((prev, next) => Math.min(prev, next.result?.startTime ?? Number.POSITIVE_INFINITY), Number.POSITIVE_INFINITY);
2875
3248
  if (startTime === Number.POSITIVE_INFINITY) startTime = this.start;
2876
- const endTime = tests.reduce((prev, next) => Math.max(prev, (next.result?.startTime ?? 0) + (next.result?.duration ?? 0)), startTime), assertionResults = tests.map((t) => {
3249
+ const endTime = tests.reduce((prev, next) => Math.max(prev, (next.result?.startTime ?? 0) + (next.result?.duration ?? 0)), startTime);
3250
+ const assertionResults = tests.map((t) => {
2877
3251
  const ancestorTitles = [];
2878
3252
  let iter = t.suite;
2879
- while (iter) ancestorTitles.push(iter.name), iter = iter.suite;
2880
- return ancestorTitles.reverse(), {
3253
+ while (iter) {
3254
+ ancestorTitles.push(iter.name);
3255
+ iter = iter.suite;
3256
+ }
3257
+ ancestorTitles.reverse();
3258
+ return {
2881
3259
  ancestorTitles,
2882
3260
  fullName: t.name ? [...ancestorTitles, t.name].join(" ") : ancestorTitles.join(" "),
2883
3261
  status: StatusMap[t.result?.state || t.mode] || "skipped",
@@ -2925,9 +3303,11 @@ class JsonReporter {
2925
3303
  async writeReport(report) {
2926
3304
  const outputFile = this.options.outputFile ?? getOutputFile(this.ctx.config, "json");
2927
3305
  if (outputFile) {
2928
- const reportFile = resolve$1(this.ctx.config.root, outputFile), outputDirectory = dirname(reportFile);
3306
+ const reportFile = resolve$1(this.ctx.config.root, outputFile);
3307
+ const outputDirectory = dirname(reportFile);
2929
3308
  if (!existsSync(outputDirectory)) await promises.mkdir(outputDirectory, { recursive: true });
2930
- await promises.writeFile(reportFile, report, "utf-8"), this.ctx.logger.log(`JSON report written to ${reportFile}`);
3309
+ await promises.writeFile(reportFile, report, "utf-8");
3310
+ this.ctx.logger.log(`JSON report written to ${reportFile}`);
2931
3311
  } else this.ctx.logger.log(report);
2932
3312
  }
2933
3313
  }
@@ -2950,7 +3330,8 @@ class IndentedLogger {
2950
3330
 
2951
3331
  function flattenTasks$1(task, baseName = "") {
2952
3332
  const base = baseName ? `${baseName} > ` : "";
2953
- return task.type === "suite" ? task.tasks.flatMap((child) => flattenTasks$1(child, `${base}${task.name}`)) : [{
3333
+ if (task.type === "suite") return task.tasks.flatMap((child) => flattenTasks$1(child, `${base}${task.name}`));
3334
+ else return [{
2954
3335
  ...task,
2955
3336
  name: `${base}${task.name}`
2956
3337
  }];
@@ -2958,16 +3339,21 @@ function flattenTasks$1(task, baseName = "") {
2958
3339
  // https://gist.github.com/john-doherty/b9195065884cdbfd2017a4756e6409cc
2959
3340
  function removeInvalidXMLCharacters(value, removeDiscouragedChars) {
2960
3341
  let regex = /([\0-\x08\v\f\x0E-\x1F\uFFFD\uFFFE\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])/g;
2961
- if (value = String(value || "").replace(regex, ""), removeDiscouragedChars) regex = new RegExp(
2962
- /* eslint-disable regexp/prefer-character-class, regexp/no-obscure-range, regexp/no-useless-non-capturing-group */
2963
- "([\\x7F-\\x84]|[\\x86-\\x9F]|[\\uFDD0-\\uFDEF]|\\uD83F[\\uDFFE\\uDFFF]|(?:\\uD87F[\\uDFFE\\uDFFF])|\\uD8BF[\\uDFFE\\uDFFF]|\\uD8FF[\\uDFFE\\uDFFF]|(?:\\uD93F[\\uDFFE\\uDFFF])|\\uD97F[\\uDFFE\\uDFFF]|\\uD9BF[\\uDFFE\\uDFFF]|\\uD9FF[\\uDFFE\\uDFFF]|\\uDA3F[\\uDFFE\\uDFFF]|\\uDA7F[\\uDFFE\\uDFFF]|\\uDABF[\\uDFFE\\uDFFF]|(?:\\uDAFF[\\uDFFE\\uDFFF])|\\uDB3F[\\uDFFE\\uDFFF]|\\uDB7F[\\uDFFE\\uDFFF]|(?:\\uDBBF[\\uDFFE\\uDFFF])|\\uDBFF[\\uDFFE\\uDFFF](?:[\\0-\\t\\v\\f\\x0E-\\u2027\\u202A-\\uD7FF\\uE000-\\uFFFF]|[\\uD800-\\uDBFF][\\uDC00-\\uDFFF]|[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|(?:[^\\uD800-\\uDBFF]|^)[\\uDC00-\\uDFFF]))",
2964
- "g"
2965
- /* eslint-enable */
2966
- ), value = value.replace(regex, "");
3342
+ value = String(value || "").replace(regex, "");
3343
+ {
3344
+ // remove everything discouraged by XML 1.0 specifications
3345
+ regex = new RegExp(
3346
+ /* eslint-disable regexp/prefer-character-class, regexp/no-obscure-range, regexp/no-useless-non-capturing-group */
3347
+ "([\\x7F-\\x84]|[\\x86-\\x9F]|[\\uFDD0-\\uFDEF]|\\uD83F[\\uDFFE\\uDFFF]|(?:\\uD87F[\\uDFFE\\uDFFF])|\\uD8BF[\\uDFFE\\uDFFF]|\\uD8FF[\\uDFFE\\uDFFF]|(?:\\uD93F[\\uDFFE\\uDFFF])|\\uD97F[\\uDFFE\\uDFFF]|\\uD9BF[\\uDFFE\\uDFFF]|\\uD9FF[\\uDFFE\\uDFFF]|\\uDA3F[\\uDFFE\\uDFFF]|\\uDA7F[\\uDFFE\\uDFFF]|\\uDABF[\\uDFFE\\uDFFF]|(?:\\uDAFF[\\uDFFE\\uDFFF])|\\uDB3F[\\uDFFE\\uDFFF]|\\uDB7F[\\uDFFE\\uDFFF]|(?:\\uDBBF[\\uDFFE\\uDFFF])|\\uDBFF[\\uDFFE\\uDFFF](?:[\\0-\\t\\v\\f\\x0E-\\u2027\\u202A-\\uD7FF\\uE000-\\uFFFF]|[\\uD800-\\uDBFF][\\uDC00-\\uDFFF]|[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|(?:[^\\uD800-\\uDBFF]|^)[\\uDC00-\\uDFFF]))",
3348
+ "g"
3349
+ /* eslint-enable */
3350
+ );
3351
+ value = value.replace(regex, "");
3352
+ }
2967
3353
  return value;
2968
3354
  }
2969
3355
  function escapeXML(value) {
2970
- return removeInvalidXMLCharacters(String(value).replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/'/g, "&apos;").replace(/</g, "&lt;").replace(/>/g, "&gt;"), true);
3356
+ return removeInvalidXMLCharacters(String(value).replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/'/g, "&apos;").replace(/</g, "&lt;").replace(/>/g, "&gt;"));
2971
3357
  }
2972
3358
  function executionTime(durationMS) {
2973
3359
  return (durationMS / 1e3).toLocaleString("en-US", {
@@ -2976,8 +3362,7 @@ function executionTime(durationMS) {
2976
3362
  });
2977
3363
  }
2978
3364
  function getDuration(task) {
2979
- const duration = task.result?.duration ?? 0;
2980
- return executionTime(duration);
3365
+ return executionTime(task.result?.duration ?? 0);
2981
3366
  }
2982
3367
  class JUnitReporter {
2983
3368
  ctx;
@@ -2988,7 +3373,8 @@ class JUnitReporter {
2988
3373
  fileFd;
2989
3374
  options;
2990
3375
  constructor(options) {
2991
- this.options = { ...options }, this.options.includeConsoleOutput ??= true;
3376
+ this.options = { ...options };
3377
+ this.options.includeConsoleOutput ??= true;
2992
3378
  }
2993
3379
  async onInit(ctx) {
2994
3380
  this.ctx = ctx;
@@ -2997,12 +3383,14 @@ class JUnitReporter {
2997
3383
  this.reportFile = resolve$1(this.ctx.config.root, outputFile);
2998
3384
  const outputDirectory = dirname(this.reportFile);
2999
3385
  if (!existsSync(outputDirectory)) await promises.mkdir(outputDirectory, { recursive: true });
3000
- this.fileFd = await promises.open(this.reportFile, "w+"), this.baseLog = async (text) => {
3386
+ this.fileFd = await promises.open(this.reportFile, "w+");
3387
+ this.baseLog = async (text) => {
3001
3388
  if (!this.fileFd) this.fileFd = await promises.open(this.reportFile, "w+");
3002
3389
  await promises.writeFile(this.fileFd, `${text}\n`);
3003
3390
  };
3004
3391
  } else this.baseLog = async (text) => this.ctx.logger.log(text);
3005
- this._timeStart = /* @__PURE__ */ new Date(), this.logger = new IndentedLogger(this.baseLog);
3392
+ this._timeStart = /* @__PURE__ */ new Date();
3393
+ this.logger = new IndentedLogger(this.baseLog);
3006
3394
  }
3007
3395
  async writeElement(name, attrs, children) {
3008
3396
  const pairs = [];
@@ -3011,12 +3399,18 @@ class JUnitReporter {
3011
3399
  if (attr === void 0) continue;
3012
3400
  pairs.push(`${key}="${escapeXML(attr)}"`);
3013
3401
  }
3014
- await this.logger.log(`<${name}${pairs.length ? ` ${pairs.join(" ")}` : ""}>`), this.logger.indent(), await children.call(this), this.logger.unindent(), await this.logger.log(`</${name}>`);
3402
+ await this.logger.log(`<${name}${pairs.length ? ` ${pairs.join(" ")}` : ""}>`);
3403
+ this.logger.indent();
3404
+ await children.call(this);
3405
+ this.logger.unindent();
3406
+ await this.logger.log(`</${name}>`);
3015
3407
  }
3016
3408
  async writeLogs(task, type) {
3017
3409
  if (task.logs == null || task.logs.length === 0) return;
3018
- const logType = type === "err" ? "stderr" : "stdout", logs = task.logs.filter((log) => log.type === logType);
3019
- logs.length !== 0 && await this.writeElement(`system-${type}`, {}, async () => {
3410
+ const logType = type === "err" ? "stderr" : "stdout";
3411
+ const logs = task.logs.filter((log) => log.type === logType);
3412
+ if (logs.length === 0) return;
3413
+ await this.writeElement(`system-${type}`, {}, async () => {
3020
3414
  for (const log of logs) await this.baseLog(escapeXML(log.content));
3021
3415
  });
3022
3416
  }
@@ -3035,12 +3429,20 @@ class JUnitReporter {
3035
3429
  name: task.name,
3036
3430
  time: getDuration(task)
3037
3431
  }, async () => {
3038
- if (this.options.includeConsoleOutput) await this.writeLogs(task, "out"), await this.writeLogs(task, "err");
3432
+ if (this.options.includeConsoleOutput) {
3433
+ await this.writeLogs(task, "out");
3434
+ await this.writeLogs(task, "err");
3435
+ }
3039
3436
  if (task.mode === "skip" || task.mode === "todo") await this.logger.log("<skipped/>");
3040
3437
  if (task.type === "test" && task.annotations.length) {
3041
- await this.logger.log("<properties>"), this.logger.indent();
3042
- for (const annotation of task.annotations) await this.logger.log(`<property name="${escapeXML(annotation.type)}" value="${escapeXML(annotation.message)}">`), await this.logger.log("</property>");
3043
- this.logger.unindent(), await this.logger.log("</properties>");
3438
+ await this.logger.log("<properties>");
3439
+ this.logger.indent();
3440
+ for (const annotation of task.annotations) {
3441
+ await this.logger.log(`<property name="${escapeXML(annotation.type)}" value="${escapeXML(annotation.message)}">`);
3442
+ await this.logger.log("</property>");
3443
+ }
3444
+ this.logger.unindent();
3445
+ await this.logger.log("</properties>");
3044
3446
  }
3045
3447
  if (task.result?.state === "fail") {
3046
3448
  const errors = task.result.errors || [];
@@ -3063,7 +3465,8 @@ class JUnitReporter {
3063
3465
  const files = testModules.map((testModule) => testModule.task);
3064
3466
  await this.logger.log("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>");
3065
3467
  const transformed = files.map((file) => {
3066
- const tasks = file.tasks.flatMap((task) => flattenTasks$1(task)), stats = tasks.reduce((stats, task) => {
3468
+ const tasks = file.tasks.flatMap((task) => flattenTasks$1(task));
3469
+ const stats = tasks.reduce((stats, task) => {
3067
3470
  return {
3068
3471
  passed: stats.passed + Number(task.result?.state === "pass"),
3069
3472
  failures: stats.failures + Number(task.result?.state === "fail"),
@@ -3073,29 +3476,41 @@ class JUnitReporter {
3073
3476
  passed: 0,
3074
3477
  failures: 0,
3075
3478
  skipped: 0
3076
- }), suites = getSuites(file);
3077
- for (const suite of suites) if (suite.result?.errors) tasks.push(suite), stats.failures += 1;
3078
- // If there are no tests, but the file failed to load, we still want to report it as a failure
3079
- if (tasks.length === 0 && file.result?.state === "fail") stats.failures = 1, tasks.push({
3080
- id: file.id,
3081
- type: "test",
3082
- name: file.name,
3083
- mode: "run",
3084
- result: file.result,
3085
- meta: {},
3086
- timeout: 0,
3087
- context: null,
3088
- suite: null,
3089
- file: null,
3090
- annotations: []
3091
3479
  });
3480
+ // inject failed suites to surface errors during beforeAll/afterAll
3481
+ const suites = getSuites(file);
3482
+ for (const suite of suites) if (suite.result?.errors) {
3483
+ tasks.push(suite);
3484
+ stats.failures += 1;
3485
+ }
3486
+ // If there are no tests, but the file failed to load, we still want to report it as a failure
3487
+ if (tasks.length === 0 && file.result?.state === "fail") {
3488
+ stats.failures = 1;
3489
+ tasks.push({
3490
+ id: file.id,
3491
+ type: "test",
3492
+ name: file.name,
3493
+ mode: "run",
3494
+ result: file.result,
3495
+ meta: {},
3496
+ timeout: 0,
3497
+ context: null,
3498
+ suite: null,
3499
+ file: null,
3500
+ annotations: []
3501
+ });
3502
+ }
3092
3503
  return {
3093
3504
  ...file,
3094
3505
  tasks,
3095
3506
  stats
3096
3507
  };
3097
- }), stats = transformed.reduce((stats, file) => {
3098
- return stats.tests += file.tasks.length, stats.failures += file.stats.failures, stats.time += file.result?.duration || 0, stats;
3508
+ });
3509
+ const stats = transformed.reduce((stats, file) => {
3510
+ stats.tests += file.tasks.length;
3511
+ stats.failures += file.stats.failures;
3512
+ stats.time += file.result?.duration || 0;
3513
+ return stats;
3099
3514
  }, {
3100
3515
  name: this.options.suiteName || "vitest tests",
3101
3516
  tests: 0,
@@ -3103,7 +3518,7 @@ class JUnitReporter {
3103
3518
  errors: 0,
3104
3519
  time: 0
3105
3520
  });
3106
- if (await this.writeElement("testsuites", {
3521
+ await this.writeElement("testsuites", {
3107
3522
  ...stats,
3108
3523
  time: executionTime(stats.time)
3109
3524
  }, async () => {
@@ -3122,13 +3537,16 @@ class JUnitReporter {
3122
3537
  await this.writeTasks(file.tasks, filename);
3123
3538
  });
3124
3539
  }
3125
- }), this.reportFile) this.ctx.logger.log(`JUNIT report written to ${this.reportFile}`);
3126
- await this.fileFd?.close(), this.fileFd = void 0;
3540
+ });
3541
+ if (this.reportFile) this.ctx.logger.log(`JUNIT report written to ${this.reportFile}`);
3542
+ await this.fileFd?.close();
3543
+ this.fileFd = void 0;
3127
3544
  }
3128
3545
  }
3129
3546
 
3130
3547
  function yamlString(str) {
3131
- return str ? `"${str.replace(/"/g, "\\\"")}"` : "";
3548
+ if (!str) return "";
3549
+ return `"${str.replace(/"/g, "\\\"")}"`;
3132
3550
  }
3133
3551
  function tapString(str) {
3134
3552
  return str.replace(/\\/g, "\\\\").replace(/#/g, "\\#").replace(/\n/g, " ");
@@ -3137,45 +3555,77 @@ class TapReporter {
3137
3555
  ctx;
3138
3556
  logger;
3139
3557
  onInit(ctx) {
3140
- this.ctx = ctx, this.logger = new IndentedLogger(ctx.logger.log.bind(ctx.logger));
3558
+ this.ctx = ctx;
3559
+ this.logger = new IndentedLogger(ctx.logger.log.bind(ctx.logger));
3141
3560
  }
3142
3561
  static getComment(task) {
3143
- return task.mode === "skip" ? " # SKIP" : task.mode === "todo" ? " # TODO" : task.result?.duration == null ? "" : ` # time=${task.result.duration.toFixed(2)}ms`;
3562
+ if (task.mode === "skip") return " # SKIP";
3563
+ else if (task.mode === "todo") return " # TODO";
3564
+ else if (task.result?.duration != null) return ` # time=${task.result.duration.toFixed(2)}ms`;
3565
+ else return "";
3144
3566
  }
3145
3567
  logErrorDetails(error, stack) {
3146
3568
  const errorName = error.name || "Unknown Error";
3147
- if (this.logger.log(`name: ${yamlString(String(errorName))}`), this.logger.log(`message: ${yamlString(String(error.message))}`), stack)
3569
+ this.logger.log(`name: ${yamlString(String(errorName))}`);
3570
+ this.logger.log(`message: ${yamlString(String(error.message))}`);
3571
+ if (stack)
3148
3572
  // For compatibility with tap-mocha-reporter
3149
3573
  this.logger.log(`stack: ${yamlString(`${stack.file}:${stack.line}:${stack.column}`)}`);
3150
3574
  }
3151
3575
  logTasks(tasks) {
3152
3576
  this.logger.log(`1..${tasks.length}`);
3153
3577
  for (const [i, task] of tasks.entries()) {
3154
- const id = i + 1, ok = task.result?.state === "pass" || task.mode === "skip" || task.mode === "todo" ? "ok" : "not ok", comment = TapReporter.getComment(task);
3155
- if (task.type === "suite" && task.tasks.length > 0) this.logger.log(`${ok} ${id} - ${tapString(task.name)}${comment} {`), this.logger.indent(), this.logTasks(task.tasks), this.logger.unindent(), this.logger.log("}");
3156
- else {
3578
+ const id = i + 1;
3579
+ const ok = task.result?.state === "pass" || task.mode === "skip" || task.mode === "todo" ? "ok" : "not ok";
3580
+ const comment = TapReporter.getComment(task);
3581
+ if (task.type === "suite" && task.tasks.length > 0) {
3582
+ this.logger.log(`${ok} ${id} - ${tapString(task.name)}${comment} {`);
3583
+ this.logger.indent();
3584
+ this.logTasks(task.tasks);
3585
+ this.logger.unindent();
3586
+ this.logger.log("}");
3587
+ } else {
3157
3588
  this.logger.log(`${ok} ${id} - ${tapString(task.name)}${comment}`);
3158
3589
  const project = this.ctx.getProjectByName(task.file.projectName || "");
3159
- if (task.type === "test" && task.annotations) this.logger.indent(), task.annotations.forEach(({ type, message }) => {
3160
- this.logger.log(`# ${type}: ${message}`);
3161
- }), this.logger.unindent();
3162
- if (task.result?.state === "fail" && task.result.errors) this.logger.indent(), task.result.errors.forEach((error) => {
3163
- const stack = (task.file.pool === "browser" ? project.browser?.parseErrorStacktrace(error) || [] : parseErrorStacktrace(error, { frameFilter: this.ctx.config.onStackTrace }))[0];
3164
- if (this.logger.log("---"), this.logger.log("error:"), this.logger.indent(), this.logErrorDetails(error), this.logger.unindent(), stack) this.logger.log(`at: ${yamlString(`${stack.file}:${stack.line}:${stack.column}`)}`);
3165
- if (error.showDiff) this.logger.log(`actual: ${yamlString(error.actual)}`), this.logger.log(`expected: ${yamlString(error.expected)}`);
3166
- }), this.logger.log("..."), this.logger.unindent();
3590
+ if (task.type === "test" && task.annotations) {
3591
+ this.logger.indent();
3592
+ task.annotations.forEach(({ type, message }) => {
3593
+ this.logger.log(`# ${type}: ${message}`);
3594
+ });
3595
+ this.logger.unindent();
3596
+ }
3597
+ if (task.result?.state === "fail" && task.result.errors) {
3598
+ this.logger.indent();
3599
+ task.result.errors.forEach((error) => {
3600
+ const stack = (task.file.pool === "browser" ? project.browser?.parseErrorStacktrace(error) || [] : parseErrorStacktrace(error, { frameFilter: this.ctx.config.onStackTrace }))[0];
3601
+ this.logger.log("---");
3602
+ this.logger.log("error:");
3603
+ this.logger.indent();
3604
+ this.logErrorDetails(error);
3605
+ this.logger.unindent();
3606
+ if (stack) this.logger.log(`at: ${yamlString(`${stack.file}:${stack.line}:${stack.column}`)}`);
3607
+ if (error.showDiff) {
3608
+ this.logger.log(`actual: ${yamlString(error.actual)}`);
3609
+ this.logger.log(`expected: ${yamlString(error.expected)}`);
3610
+ }
3611
+ });
3612
+ this.logger.log("...");
3613
+ this.logger.unindent();
3614
+ }
3167
3615
  }
3168
3616
  }
3169
3617
  }
3170
3618
  onTestRunEnd(testModules) {
3171
3619
  const files = testModules.map((testModule) => testModule.task);
3172
- this.logger.log("TAP version 13"), this.logTasks(files);
3620
+ this.logger.log("TAP version 13");
3621
+ this.logTasks(files);
3173
3622
  }
3174
3623
  }
3175
3624
 
3176
3625
  function flattenTasks(task, baseName = "") {
3177
3626
  const base = baseName ? `${baseName} > ` : "";
3178
- return task.type === "suite" && task.tasks.length > 0 ? task.tasks.flatMap((child) => flattenTasks(child, `${base}${task.name}`)) : [{
3627
+ if (task.type === "suite" && task.tasks.length > 0) return task.tasks.flatMap((child) => flattenTasks(child, `${base}${task.name}`));
3628
+ else return [{
3179
3629
  ...task,
3180
3630
  name: `${base}${task.name}`
3181
3631
  }];
@@ -3207,9 +3657,18 @@ class VerboseReporter extends DefaultReporter {
3207
3657
  const testResult = test.result();
3208
3658
  if (this.ctx.config.hideSkippedTests && testResult.state === "skipped") return;
3209
3659
  let title = ` ${this.getEntityPrefix(test)} `;
3210
- if (title += test.module.task.name, test.location) title += c.dim(`:${test.location.line}:${test.location.column}`);
3211
- if (title += separator, title += getTestName(test.task, separator), title += this.getTestCaseSuffix(test), this.log(title), testResult.state === "failed") testResult.errors.forEach((error) => this.log(c.red(` ${F_RIGHT} ${error.message}`)));
3212
- if (test.annotations().length) this.log(), this.printAnnotations(test, "log", 3), this.log();
3660
+ title += test.module.task.name;
3661
+ if (test.location) title += c.dim(`:${test.location.line}:${test.location.column}`);
3662
+ title += separator;
3663
+ title += getTestName(test.task, separator);
3664
+ title += this.getTestCaseSuffix(test);
3665
+ this.log(title);
3666
+ if (testResult.state === "failed") testResult.errors.forEach((error) => this.log(c.red(` ${F_RIGHT} ${error.message}`)));
3667
+ if (test.annotations().length) {
3668
+ this.log();
3669
+ this.printAnnotations(test, "log", 3);
3670
+ this.log();
3671
+ }
3213
3672
  }
3214
3673
  }
3215
3674