vitest 4.0.6 → 4.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +1 -1
- package/dist/browser.d.ts +2 -2
- package/dist/browser.js +2 -2
- package/dist/chunks/base.BgTO2qAg.js +156 -0
- package/dist/chunks/{benchmark.DHKMYAts.js → benchmark.B3N2zMcH.js} +9 -4
- package/dist/chunks/{browser.d.ScGeWTou.d.ts → browser.d.DTTM2PTh.d.ts} +1 -1
- package/dist/chunks/{cac.BBqWH4nd.js → cac.CfkWq8Qy.js} +117 -43
- package/dist/chunks/{cli-api.UL3SwFUb.js → cli-api.BQ-bjcRi.js} +1870 -847
- package/dist/chunks/console.Cf-YriPC.js +146 -0
- package/dist/chunks/{coverage.DuCn_Tmx.js → coverage.NVjCOln1.js} +281 -103
- package/dist/chunks/{creator.cqqifzG7.js → creator.fzVyoMf3.js} +74 -30
- package/dist/chunks/{date.-jtEtIeV.js → date.Bq6ZW5rf.js} +17 -6
- package/dist/chunks/{git.BFNcloKD.js → git.Bm2pzPAa.js} +3 -3
- package/dist/chunks/{global.d.DdOkMiVb.d.ts → global.d.DVdCfKp5.d.ts} +1 -1
- package/dist/chunks/{globals.BGT_RUsD.js → globals.DOh96BiR.js} +5 -5
- package/dist/chunks/{resolveSnapshotEnvironment.BZzLjzkh.js → index.BY4-tcno.js} +42 -25
- package/dist/chunks/{index.Bgo3tNWt.js → index.DAL392Ss.js} +40 -15
- package/dist/chunks/{index.RwjEGCQ0.js → index.DIFZf73e.js} +2 -2
- package/dist/chunks/{index.DV0mQLEO.js → index.DfKyPFVi.js} +195 -64
- package/dist/chunks/{index.BL8Hg4Uk.js → index.kotH7DY7.js} +837 -380
- package/dist/chunks/{index.CpdwpN7L.js → index.op2Re5rn.js} +22 -12
- package/dist/chunks/{init-forks.CSGFj9zN.js → init-forks.2hx7cf78.js} +16 -5
- package/dist/chunks/{init-threads.CIJLeFO8.js → init-threads.Cm4OCIWA.js} +3 -2
- package/dist/chunks/{init.DUeOfNO9.js → init.DMDG-idf.js} +124 -54
- package/dist/chunks/{inspector.DLZxSeU3.js → inspector.CvyFGlXm.js} +25 -10
- package/dist/chunks/{moduleRunner.d.TP-w6tIQ.d.ts → moduleRunner.d.CzOZ_4wC.d.ts} +1 -1
- package/dist/chunks/{node.BwAWWjHZ.js → node.Ce0vMQM7.js} +1 -1
- package/dist/chunks/{plugin.d.lctzD3Wk.d.ts → plugin.d.D4RrtywJ.d.ts} +1 -1
- package/dist/chunks/{reporters.d.PEs0tXod.d.ts → reporters.d.Da1D1VbQ.d.ts} +19 -9
- package/dist/chunks/rpc.BUV7uWKJ.js +76 -0
- package/dist/chunks/{setup-common.DR1sucx6.js → setup-common.LGjNSzXp.js} +20 -8
- package/dist/chunks/{startModuleRunner.Di-EZqh0.js → startModuleRunner.BOmUtLIO.js} +228 -105
- package/dist/chunks/{test.CnspO-X4.js → test.ClrAtjMv.js} +48 -22
- package/dist/chunks/{utils.CG9h5ccR.js → utils.DvEY5TfP.js} +14 -5
- package/dist/chunks/{vi.BZvkKVkM.js → vi.Bgcdy3bQ.js} +261 -111
- package/dist/chunks/{vm.Co_lR2NL.js → vm.BIkCDs68.js} +177 -70
- package/dist/chunks/{worker.d.B4Hthdvt.d.ts → worker.d.DadbA89M.d.ts} +52 -6
- package/dist/cli.js +2 -2
- package/dist/config.d.ts +5 -5
- package/dist/coverage.d.ts +3 -3
- package/dist/coverage.js +1 -1
- package/dist/environments.js +2 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.js +5 -5
- package/dist/module-evaluator.d.ts +2 -2
- package/dist/module-evaluator.js +85 -35
- package/dist/module-runner.js +2 -2
- package/dist/node.d.ts +7 -7
- package/dist/node.js +16 -12
- package/dist/reporters.d.ts +3 -3
- package/dist/reporters.js +2 -2
- package/dist/runners.js +7 -7
- package/dist/snapshot.js +2 -2
- package/dist/suite.js +2 -2
- package/dist/worker.d.ts +2 -1
- package/dist/worker.js +27 -27
- package/dist/workers/forks.js +34 -31
- package/dist/workers/runVmTests.js +41 -22
- package/dist/workers/threads.js +34 -31
- package/dist/workers/vmForks.js +14 -14
- package/dist/workers/vmThreads.js +14 -14
- package/package.json +20 -20
- package/dist/chunks/base.BAf_bYeI.js +0 -128
- package/dist/chunks/console.CTJL2nuH.js +0 -115
- package/dist/chunks/rpc.Dv1Jt3i2.js +0 -66
|
@@ -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,
|
|
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
|
|
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
|
-
|
|
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]
|
|
165
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
210
|
+
if (!mod[1].file) return null;
|
|
211
|
+
return [
|
|
198
212
|
mod[0],
|
|
199
213
|
mod[1].file,
|
|
200
214
|
mod[1].url
|
|
201
|
-
]
|
|
215
|
+
];
|
|
202
216
|
}).filter((x) => x != null)];
|
|
203
|
-
})
|
|
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
|
-
]
|
|
211
|
-
|
|
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)
|
|
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)
|
|
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
|
|
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
|
-
})
|
|
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
|
-
|
|
265
|
+
if (!project) return;
|
|
266
|
+
moduleIds.forEach(([moduleId, file, url]) => {
|
|
246
267
|
const moduleNode = project.vite.moduleGraph.createFileOnlyEntry(file);
|
|
247
|
-
moduleNode.url = url
|
|
268
|
+
moduleNode.url = url;
|
|
269
|
+
moduleNode.id = moduleId;
|
|
270
|
+
moduleNode.transformResult = {
|
|
248
271
|
code: " ",
|
|
249
272
|
map: null
|
|
250
|
-
}
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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 = []
|
|
294
|
+
const packs = [];
|
|
295
|
+
const events = [];
|
|
273
296
|
function visit(suite) {
|
|
274
|
-
onTask?.(suite)
|
|
297
|
+
onTask?.(suite);
|
|
298
|
+
packs.push([
|
|
275
299
|
suite.id,
|
|
276
300
|
suite.result,
|
|
277
301
|
suite.meta
|
|
278
|
-
])
|
|
302
|
+
]);
|
|
303
|
+
events.push([
|
|
279
304
|
suite.id,
|
|
280
305
|
"suite-prepare",
|
|
281
306
|
void 0
|
|
282
|
-
])
|
|
307
|
+
]);
|
|
308
|
+
suite.tasks.forEach((task) => {
|
|
283
309
|
if (task.type === "suite") visit(task);
|
|
284
|
-
else
|
|
285
|
-
task
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
|
|
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()
|
|
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
|
|
354
|
-
|
|
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)
|
|
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)}`)
|
|
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)}`)
|
|
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,12 +433,15 @@ 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.
|
|
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
|
-
failed
|
|
393
|
-
passed
|
|
394
|
-
skipped
|
|
395
|
-
todo
|
|
441
|
+
failed ? c.bold(c.red(`${failed} failed`)) : null,
|
|
442
|
+
passed ? c.bold(c.green(`${passed} passed`)) : null,
|
|
443
|
+
skipped ? c.yellow(`${skipped} skipped`) : null,
|
|
444
|
+
todo ? c.gray(`${todo} todo`) : null
|
|
396
445
|
].filter(Boolean).join(c.dim(" | ")) + (showTotal ? c.gray(` (${tasks.length})`) : "");
|
|
397
446
|
}
|
|
398
447
|
function getStateSymbol(task) {
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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)
|
|
491
|
-
|
|
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
|
|
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 = []
|
|
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
|
-
|
|
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
|
-
}))
|
|
598
|
+
}));
|
|
599
|
+
logs.forEach((log) => this.log(log));
|
|
541
600
|
}
|
|
542
601
|
printTestCase(moduleState, test) {
|
|
543
|
-
const testResult = test.result()
|
|
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))
|
|
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
|
-
|
|
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)
|
|
578
|
-
|
|
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] ??= []
|
|
586
|
-
|
|
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() || {}
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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())
|
|
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
|
|
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
|
-
|
|
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()
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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]
|
|
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
|
-
|
|
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)
|
|
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
|
|
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
|
-
|
|
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)
|
|
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)
|
|
711
|
-
|
|
712
|
-
|
|
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)
|
|
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 || ""
|
|
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 || ""
|
|
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
|
-
|
|
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
|
-
})
|
|
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)
|
|
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)
|
|
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
|
-
|
|
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
|
|
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
|
-
}
|
|
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
|
-
}
|
|
806
|
-
|
|
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
|
|
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())
|
|
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
|
|
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)
|
|
827
|
-
this.renderScheduled =
|
|
828
|
-
|
|
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)
|
|
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)
|
|
849
|
-
|
|
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
|
-
|
|
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"))
|
|
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
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
|
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
|
|
1065
|
+
this.ctx = ctx;
|
|
1066
|
+
this.options = {
|
|
912
1067
|
verbose: false,
|
|
913
1068
|
...options
|
|
914
|
-
}
|
|
1069
|
+
};
|
|
1070
|
+
this.renderer = new WindowRenderer({
|
|
915
1071
|
logger: ctx.logger,
|
|
916
1072
|
getWindow: () => this.createSummary()
|
|
917
|
-
})
|
|
918
|
-
|
|
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()
|
|
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()
|
|
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))
|
|
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)
|
|
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
|
|
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?.()
|
|
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
|
|
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
|
-
}
|
|
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()
|
|
975
|
-
|
|
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()
|
|
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
|
-
|
|
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
|
|
1009
|
-
|
|
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 = [""];
|
|
@@ -1016,25 +1201,38 @@ class SummaryReporter {
|
|
|
1016
1201
|
name: testFile.projectName,
|
|
1017
1202
|
color: testFile.projectColor
|
|
1018
1203
|
}) + typecheck + testFile.filename + c.dim(!testFile.completed && !testFile.total ? " [queued]" : ` ${testFile.completed}/${testFile.total}`));
|
|
1019
|
-
const slowTasks = [testFile.hook, ...
|
|
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
|
|
1022
|
-
|
|
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
|
-
|
|
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())
|
|
1031
|
-
|
|
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()
|
|
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
|
-
|
|
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
|
-
|
|
1277
|
+
super(options);
|
|
1278
|
+
this.options = {
|
|
1078
1279
|
summary: true,
|
|
1079
1280
|
...options
|
|
1080
|
-
}
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
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()
|
|
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()
|
|
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)
|
|
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)
|
|
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
|
-
|
|
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)
|
|
1386
|
+
this.tests.delete(id);
|
|
1387
|
+
states.push(state);
|
|
1167
1388
|
}
|
|
1168
|
-
this.ctx.logger.log(formatTests(states))
|
|
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
|
-
}
|
|
1400
|
+
};
|
|
1401
|
+
const fail = {
|
|
1179
1402
|
char: "x",
|
|
1180
1403
|
color: c.red
|
|
1181
|
-
}
|
|
1404
|
+
};
|
|
1405
|
+
const pending = {
|
|
1182
1406
|
char: "*",
|
|
1183
1407
|
color: c.yellow
|
|
1184
|
-
}
|
|
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
|
|
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))
|
|
1435
|
+
output += currentIcon.color(currentIcon.char.repeat(count));
|
|
1436
|
+
// Start tracking new group
|
|
1437
|
+
count = 1;
|
|
1438
|
+
currentIcon = icon;
|
|
1209
1439
|
}
|
|
1210
|
-
|
|
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)
|
|
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}${
|
|
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 = []
|
|
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
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
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
|
|
2141
|
-
|
|
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
|
|
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
|
|
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
|
|
2216
|
-
|
|
2459
|
+
definition.task = task;
|
|
2460
|
+
latestSuite.tasks.push(task);
|
|
2461
|
+
});
|
|
2462
|
+
calculateSuiteHash(file);
|
|
2217
2463
|
const hasOnly = someTasksAreOnly(file);
|
|
2218
|
-
|
|
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
|
|
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()
|
|
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)
|
|
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
|
-
|
|
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)
|
|
2509
|
+
if (!errInfo) return;
|
|
2510
|
+
if (!rawErrsMap.has(errFilePath)) rawErrsMap.set(errFilePath, [errInfo]);
|
|
2258
2511
|
else rawErrsMap.get(errFilePath)?.push(errInfo);
|
|
2259
|
-
})
|
|
2512
|
+
});
|
|
2513
|
+
return rawErrsMap;
|
|
2260
2514
|
}
|
|
2261
2515
|
|
|
2262
2516
|
function createIndexMap(source) {
|
|
2263
2517
|
const map = /* @__PURE__ */ new Map();
|
|
2264
|
-
let index = 0
|
|
2265
|
-
|
|
2266
|
-
|
|
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)
|
|
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
|
-
|
|
2576
|
+
if (!data) return acc;
|
|
2577
|
+
acc[data.filepath] = data;
|
|
2578
|
+
return acc;
|
|
2314
2579
|
}, {});
|
|
2315
|
-
|
|
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)
|
|
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 = []
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
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)]
|
|
2338
|
-
|
|
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
|
|
2347
|
-
|
|
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
|
-
}
|
|
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
|
-
})
|
|
2355
|
-
|
|
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)
|
|
2365
|
-
|
|
2366
|
-
|
|
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")
|
|
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
|
-
|
|
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
|
-
})
|
|
2678
|
+
});
|
|
2679
|
+
return typesErrors;
|
|
2388
2680
|
}
|
|
2389
2681
|
async stop() {
|
|
2390
|
-
this.process?.kill()
|
|
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
|
|
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 = ""
|
|
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
|
|
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
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
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)
|
|
2750
|
+
clearTimeout(timeout);
|
|
2751
|
+
reject(new Error("Spawning typechecker failed - is typescript installed?", { cause }));
|
|
2442
2752
|
}
|
|
2443
|
-
|
|
2444
|
-
|
|
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
|
-
})
|
|
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
|
|
2461
|
-
|
|
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 = []
|
|
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)
|
|
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
|
-
|
|
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
|
|
2509
|
-
output += String(chunk)
|
|
2510
|
-
|
|
2511
|
-
|
|
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,
|
|
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
|
-
|
|
2532
|
-
return error.stacks
|
|
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
|
-
})
|
|
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
|
-
})
|
|
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
|
|
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)
|
|
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
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
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,28 +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
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
}
|
|
2601
|
-
const testPath = e.VITEST_TEST_PATH, testName = e.VITEST_TEST_NAME, afterEnvTeardown = e.VITEST_AFTER_ENV_TEARDOWN;
|
|
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 (
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
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 };
|
|
2614
2949
|
}
|
|
2615
2950
|
function printErrorType(type, ctx) {
|
|
2616
2951
|
ctx.logger.error(`\n${errorBanner(type)}`);
|
|
@@ -2636,7 +2971,6 @@ const skipErrorProperties = new Set([
|
|
|
2636
2971
|
"columnNumber",
|
|
2637
2972
|
"VITEST_TEST_NAME",
|
|
2638
2973
|
"VITEST_TEST_PATH",
|
|
2639
|
-
"VITEST_AFTER_ENV_TEARDOWN",
|
|
2640
2974
|
"__vitest_rollup_error__",
|
|
2641
2975
|
...Object.getOwnPropertyNames(Error.prototype),
|
|
2642
2976
|
...Object.getOwnPropertyNames(Object.prototype)
|
|
@@ -2693,8 +3027,10 @@ function printErrorMessage(error, logger) {
|
|
|
2693
3027
|
}
|
|
2694
3028
|
function printStack(logger, project, stack, highlight, errorProperties, onStack) {
|
|
2695
3029
|
for (const frame of stack) {
|
|
2696
|
-
const color = frame === highlight ? c.cyan : c.gray
|
|
2697
|
-
|
|
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);
|
|
2698
3034
|
}
|
|
2699
3035
|
if (stack.length) logger.error();
|
|
2700
3036
|
if (hasProperties(errorProperties)) {
|
|
@@ -2709,19 +3045,28 @@ function hasProperties(obj) {
|
|
|
2709
3045
|
return false;
|
|
2710
3046
|
}
|
|
2711
3047
|
function generateCodeFrame(source, indent = 0, loc, range = 2) {
|
|
2712
|
-
const start = typeof loc === "object" ? positionToOffset(source, loc.line, loc.column) : loc
|
|
2713
|
-
|
|
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 = [];
|
|
2714
3054
|
const columns = process.stdout?.columns || 80;
|
|
2715
|
-
for (let i = 0; i < lines.length; i++)
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
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;
|
|
2720
3063
|
// too long, maybe it's a minified file, skip for codeframe
|
|
2721
3064
|
if (strippedContent.length > 200) return "";
|
|
2722
|
-
|
|
3065
|
+
res.push(lineNo(j + 1) + truncateString(lines[j].replace(/\t/g, " "), columns - 5 - indent));
|
|
3066
|
+
if (j === i) {
|
|
2723
3067
|
// push underline
|
|
2724
|
-
const pad = start - (count - lineLength) + (nl - 1)
|
|
3068
|
+
const pad = start - (count - lineLength) + (nl - 1);
|
|
3069
|
+
const length = Math.max(1, end > count ? lineLength - pad : end - start);
|
|
2725
3070
|
res.push(lineNo() + " ".repeat(pad) + c.red("^".repeat(length)));
|
|
2726
3071
|
} else if (j > i) {
|
|
2727
3072
|
if (end > count) {
|
|
@@ -2731,8 +3076,8 @@ function generateCodeFrame(source, indent = 0, loc, range = 2) {
|
|
|
2731
3076
|
count += lineLength + 1;
|
|
2732
3077
|
}
|
|
2733
3078
|
}
|
|
3079
|
+
break;
|
|
2734
3080
|
}
|
|
2735
|
-
break;
|
|
2736
3081
|
}
|
|
2737
3082
|
if (indent) res = res.map((line) => " ".repeat(indent) + line);
|
|
2738
3083
|
return res.join("\n");
|
|
@@ -2752,7 +3097,8 @@ class GithubActionsReporter {
|
|
|
2752
3097
|
}
|
|
2753
3098
|
onTestCaseAnnotate(testCase, annotation) {
|
|
2754
3099
|
if (!annotation.location || this.options.displayAnnotations === false) return;
|
|
2755
|
-
const type = getTitle(annotation.type)
|
|
3100
|
+
const type = getTitle(annotation.type);
|
|
3101
|
+
const formatted = formatMessage({
|
|
2756
3102
|
command: getType(annotation.type),
|
|
2757
3103
|
properties: {
|
|
2758
3104
|
file: annotation.location.file,
|
|
@@ -2765,14 +3111,18 @@ class GithubActionsReporter {
|
|
|
2765
3111
|
this.ctx.logger.log(`\n${formatted}`);
|
|
2766
3112
|
}
|
|
2767
3113
|
onTestRunEnd(testModules, unhandledErrors) {
|
|
2768
|
-
const files = testModules.map((testModule) => testModule.task)
|
|
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();
|
|
2769
3118
|
for (const error of errors) projectErrors.push({
|
|
2770
3119
|
project: this.ctx.getRootProject(),
|
|
2771
3120
|
title: "Unhandled error",
|
|
2772
3121
|
error
|
|
2773
3122
|
});
|
|
2774
3123
|
for (const file of files) {
|
|
2775
|
-
const tasks = getTasks(file)
|
|
3124
|
+
const tasks = getTasks(file);
|
|
3125
|
+
const project = this.ctx.getProjectByName(file.projectName || "");
|
|
2776
3126
|
for (const task of tasks) {
|
|
2777
3127
|
if (task.result?.state !== "fail") continue;
|
|
2778
3128
|
const title = getFullName(task, " > ");
|
|
@@ -2790,7 +3140,8 @@ class GithubActionsReporter {
|
|
|
2790
3140
|
const result = capturePrintError(error, this.ctx, {
|
|
2791
3141
|
project,
|
|
2792
3142
|
task: file
|
|
2793
|
-
})
|
|
3143
|
+
});
|
|
3144
|
+
const stack = result?.nearest;
|
|
2794
3145
|
if (!stack) continue;
|
|
2795
3146
|
const formatted = formatMessage({
|
|
2796
3147
|
command: "error",
|
|
@@ -2812,10 +3163,12 @@ const BUILT_IN_TYPES = [
|
|
|
2812
3163
|
"warning"
|
|
2813
3164
|
];
|
|
2814
3165
|
function getTitle(type) {
|
|
2815
|
-
if (
|
|
3166
|
+
if (BUILT_IN_TYPES.includes(type)) return;
|
|
3167
|
+
return type;
|
|
2816
3168
|
}
|
|
2817
3169
|
function getType(type) {
|
|
2818
|
-
|
|
3170
|
+
if (BUILT_IN_TYPES.includes(type)) return type;
|
|
3171
|
+
return "notice";
|
|
2819
3172
|
}
|
|
2820
3173
|
function defaultOnWritePath(path) {
|
|
2821
3174
|
return path;
|
|
@@ -2825,9 +3178,12 @@ function defaultOnWritePath(path) {
|
|
|
2825
3178
|
// https://github.com/actions/toolkit/blob/f1d9b4b985e6f0f728b4b766db73498403fd5ca3/packages/core/src/command.ts#L80-L85
|
|
2826
3179
|
function formatMessage({ command, properties, message }) {
|
|
2827
3180
|
let result = `::${command}`;
|
|
2828
|
-
|
|
2829
|
-
result += i === 0 ? " " : ","
|
|
2830
|
-
|
|
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;
|
|
2831
3187
|
}
|
|
2832
3188
|
function escapeData(s) {
|
|
2833
3189
|
return s.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
|
|
@@ -2864,22 +3220,42 @@ class JsonReporter {
|
|
|
2864
3220
|
this.options = options;
|
|
2865
3221
|
}
|
|
2866
3222
|
onInit(ctx) {
|
|
2867
|
-
this.ctx = ctx
|
|
3223
|
+
this.ctx = ctx;
|
|
3224
|
+
this.start = Date.now();
|
|
3225
|
+
this.coverageMap = void 0;
|
|
2868
3226
|
}
|
|
2869
3227
|
onCoverage(coverageMap) {
|
|
2870
3228
|
this.coverageMap = coverageMap;
|
|
2871
3229
|
}
|
|
2872
3230
|
async onTestRunEnd(testModules) {
|
|
2873
|
-
const files = testModules.map((testModule) => testModule.task)
|
|
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;
|
|
2874
3245
|
for (const file of files) {
|
|
2875
3246
|
const tests = getTests([file]);
|
|
2876
3247
|
let startTime = tests.reduce((prev, next) => Math.min(prev, next.result?.startTime ?? Number.POSITIVE_INFINITY), Number.POSITIVE_INFINITY);
|
|
2877
3248
|
if (startTime === Number.POSITIVE_INFINITY) startTime = this.start;
|
|
2878
|
-
const endTime = tests.reduce((prev, next) => Math.max(prev, (next.result?.startTime ?? 0) + (next.result?.duration ?? 0)), startTime)
|
|
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) => {
|
|
2879
3251
|
const ancestorTitles = [];
|
|
2880
3252
|
let iter = t.suite;
|
|
2881
|
-
while (iter)
|
|
2882
|
-
|
|
3253
|
+
while (iter) {
|
|
3254
|
+
ancestorTitles.push(iter.name);
|
|
3255
|
+
iter = iter.suite;
|
|
3256
|
+
}
|
|
3257
|
+
ancestorTitles.reverse();
|
|
3258
|
+
return {
|
|
2883
3259
|
ancestorTitles,
|
|
2884
3260
|
fullName: t.name ? [...ancestorTitles, t.name].join(" ") : ancestorTitles.join(" "),
|
|
2885
3261
|
status: StatusMap[t.result?.state || t.mode] || "skipped",
|
|
@@ -2927,9 +3303,11 @@ class JsonReporter {
|
|
|
2927
3303
|
async writeReport(report) {
|
|
2928
3304
|
const outputFile = this.options.outputFile ?? getOutputFile(this.ctx.config, "json");
|
|
2929
3305
|
if (outputFile) {
|
|
2930
|
-
const reportFile = resolve$1(this.ctx.config.root, outputFile)
|
|
3306
|
+
const reportFile = resolve$1(this.ctx.config.root, outputFile);
|
|
3307
|
+
const outputDirectory = dirname(reportFile);
|
|
2931
3308
|
if (!existsSync(outputDirectory)) await promises.mkdir(outputDirectory, { recursive: true });
|
|
2932
|
-
await promises.writeFile(reportFile, report, "utf-8")
|
|
3309
|
+
await promises.writeFile(reportFile, report, "utf-8");
|
|
3310
|
+
this.ctx.logger.log(`JSON report written to ${reportFile}`);
|
|
2933
3311
|
} else this.ctx.logger.log(report);
|
|
2934
3312
|
}
|
|
2935
3313
|
}
|
|
@@ -2952,7 +3330,8 @@ class IndentedLogger {
|
|
|
2952
3330
|
|
|
2953
3331
|
function flattenTasks$1(task, baseName = "") {
|
|
2954
3332
|
const base = baseName ? `${baseName} > ` : "";
|
|
2955
|
-
|
|
3333
|
+
if (task.type === "suite") return task.tasks.flatMap((child) => flattenTasks$1(child, `${base}${task.name}`));
|
|
3334
|
+
else return [{
|
|
2956
3335
|
...task,
|
|
2957
3336
|
name: `${base}${task.name}`
|
|
2958
3337
|
}];
|
|
@@ -2960,16 +3339,21 @@ function flattenTasks$1(task, baseName = "") {
|
|
|
2960
3339
|
// https://gist.github.com/john-doherty/b9195065884cdbfd2017a4756e6409cc
|
|
2961
3340
|
function removeInvalidXMLCharacters(value, removeDiscouragedChars) {
|
|
2962
3341
|
let regex = /([\0-\x08\v\f\x0E-\x1F\uFFFD\uFFFE\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])/g;
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
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
|
+
}
|
|
2969
3353
|
return value;
|
|
2970
3354
|
}
|
|
2971
3355
|
function escapeXML(value) {
|
|
2972
|
-
return removeInvalidXMLCharacters(String(value).replace(/&/g, "&").replace(/"/g, """).replace(/'/g, "'").replace(/</g, "<").replace(/>/g, ">")
|
|
3356
|
+
return removeInvalidXMLCharacters(String(value).replace(/&/g, "&").replace(/"/g, """).replace(/'/g, "'").replace(/</g, "<").replace(/>/g, ">"));
|
|
2973
3357
|
}
|
|
2974
3358
|
function executionTime(durationMS) {
|
|
2975
3359
|
return (durationMS / 1e3).toLocaleString("en-US", {
|
|
@@ -2978,8 +3362,7 @@ function executionTime(durationMS) {
|
|
|
2978
3362
|
});
|
|
2979
3363
|
}
|
|
2980
3364
|
function getDuration(task) {
|
|
2981
|
-
|
|
2982
|
-
return executionTime(duration);
|
|
3365
|
+
return executionTime(task.result?.duration ?? 0);
|
|
2983
3366
|
}
|
|
2984
3367
|
class JUnitReporter {
|
|
2985
3368
|
ctx;
|
|
@@ -2990,7 +3373,8 @@ class JUnitReporter {
|
|
|
2990
3373
|
fileFd;
|
|
2991
3374
|
options;
|
|
2992
3375
|
constructor(options) {
|
|
2993
|
-
this.options = { ...options }
|
|
3376
|
+
this.options = { ...options };
|
|
3377
|
+
this.options.includeConsoleOutput ??= true;
|
|
2994
3378
|
}
|
|
2995
3379
|
async onInit(ctx) {
|
|
2996
3380
|
this.ctx = ctx;
|
|
@@ -2999,12 +3383,14 @@ class JUnitReporter {
|
|
|
2999
3383
|
this.reportFile = resolve$1(this.ctx.config.root, outputFile);
|
|
3000
3384
|
const outputDirectory = dirname(this.reportFile);
|
|
3001
3385
|
if (!existsSync(outputDirectory)) await promises.mkdir(outputDirectory, { recursive: true });
|
|
3002
|
-
this.fileFd = await promises.open(this.reportFile, "w+")
|
|
3386
|
+
this.fileFd = await promises.open(this.reportFile, "w+");
|
|
3387
|
+
this.baseLog = async (text) => {
|
|
3003
3388
|
if (!this.fileFd) this.fileFd = await promises.open(this.reportFile, "w+");
|
|
3004
3389
|
await promises.writeFile(this.fileFd, `${text}\n`);
|
|
3005
3390
|
};
|
|
3006
3391
|
} else this.baseLog = async (text) => this.ctx.logger.log(text);
|
|
3007
|
-
this._timeStart = /* @__PURE__ */ new Date()
|
|
3392
|
+
this._timeStart = /* @__PURE__ */ new Date();
|
|
3393
|
+
this.logger = new IndentedLogger(this.baseLog);
|
|
3008
3394
|
}
|
|
3009
3395
|
async writeElement(name, attrs, children) {
|
|
3010
3396
|
const pairs = [];
|
|
@@ -3013,12 +3399,18 @@ class JUnitReporter {
|
|
|
3013
3399
|
if (attr === void 0) continue;
|
|
3014
3400
|
pairs.push(`${key}="${escapeXML(attr)}"`);
|
|
3015
3401
|
}
|
|
3016
|
-
await this.logger.log(`<${name}${pairs.length ? ` ${pairs.join(" ")}` : ""}>`)
|
|
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}>`);
|
|
3017
3407
|
}
|
|
3018
3408
|
async writeLogs(task, type) {
|
|
3019
3409
|
if (task.logs == null || task.logs.length === 0) return;
|
|
3020
|
-
const logType = type === "err" ? "stderr" : "stdout"
|
|
3021
|
-
logs
|
|
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 () => {
|
|
3022
3414
|
for (const log of logs) await this.baseLog(escapeXML(log.content));
|
|
3023
3415
|
});
|
|
3024
3416
|
}
|
|
@@ -3037,12 +3429,20 @@ class JUnitReporter {
|
|
|
3037
3429
|
name: task.name,
|
|
3038
3430
|
time: getDuration(task)
|
|
3039
3431
|
}, async () => {
|
|
3040
|
-
if (this.options.includeConsoleOutput)
|
|
3432
|
+
if (this.options.includeConsoleOutput) {
|
|
3433
|
+
await this.writeLogs(task, "out");
|
|
3434
|
+
await this.writeLogs(task, "err");
|
|
3435
|
+
}
|
|
3041
3436
|
if (task.mode === "skip" || task.mode === "todo") await this.logger.log("<skipped/>");
|
|
3042
3437
|
if (task.type === "test" && task.annotations.length) {
|
|
3043
|
-
await this.logger.log("<properties>")
|
|
3044
|
-
|
|
3045
|
-
|
|
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>");
|
|
3046
3446
|
}
|
|
3047
3447
|
if (task.result?.state === "fail") {
|
|
3048
3448
|
const errors = task.result.errors || [];
|
|
@@ -3065,7 +3465,8 @@ class JUnitReporter {
|
|
|
3065
3465
|
const files = testModules.map((testModule) => testModule.task);
|
|
3066
3466
|
await this.logger.log("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>");
|
|
3067
3467
|
const transformed = files.map((file) => {
|
|
3068
|
-
const tasks = file.tasks.flatMap((task) => flattenTasks$1(task))
|
|
3468
|
+
const tasks = file.tasks.flatMap((task) => flattenTasks$1(task));
|
|
3469
|
+
const stats = tasks.reduce((stats, task) => {
|
|
3069
3470
|
return {
|
|
3070
3471
|
passed: stats.passed + Number(task.result?.state === "pass"),
|
|
3071
3472
|
failures: stats.failures + Number(task.result?.state === "fail"),
|
|
@@ -3075,29 +3476,41 @@ class JUnitReporter {
|
|
|
3075
3476
|
passed: 0,
|
|
3076
3477
|
failures: 0,
|
|
3077
3478
|
skipped: 0
|
|
3078
|
-
}), suites = getSuites(file);
|
|
3079
|
-
for (const suite of suites) if (suite.result?.errors) tasks.push(suite), stats.failures += 1;
|
|
3080
|
-
// If there are no tests, but the file failed to load, we still want to report it as a failure
|
|
3081
|
-
if (tasks.length === 0 && file.result?.state === "fail") stats.failures = 1, tasks.push({
|
|
3082
|
-
id: file.id,
|
|
3083
|
-
type: "test",
|
|
3084
|
-
name: file.name,
|
|
3085
|
-
mode: "run",
|
|
3086
|
-
result: file.result,
|
|
3087
|
-
meta: {},
|
|
3088
|
-
timeout: 0,
|
|
3089
|
-
context: null,
|
|
3090
|
-
suite: null,
|
|
3091
|
-
file: null,
|
|
3092
|
-
annotations: []
|
|
3093
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
|
+
}
|
|
3094
3503
|
return {
|
|
3095
3504
|
...file,
|
|
3096
3505
|
tasks,
|
|
3097
3506
|
stats
|
|
3098
3507
|
};
|
|
3099
|
-
})
|
|
3100
|
-
|
|
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;
|
|
3101
3514
|
}, {
|
|
3102
3515
|
name: this.options.suiteName || "vitest tests",
|
|
3103
3516
|
tests: 0,
|
|
@@ -3105,7 +3518,7 @@ class JUnitReporter {
|
|
|
3105
3518
|
errors: 0,
|
|
3106
3519
|
time: 0
|
|
3107
3520
|
});
|
|
3108
|
-
|
|
3521
|
+
await this.writeElement("testsuites", {
|
|
3109
3522
|
...stats,
|
|
3110
3523
|
time: executionTime(stats.time)
|
|
3111
3524
|
}, async () => {
|
|
@@ -3124,13 +3537,16 @@ class JUnitReporter {
|
|
|
3124
3537
|
await this.writeTasks(file.tasks, filename);
|
|
3125
3538
|
});
|
|
3126
3539
|
}
|
|
3127
|
-
})
|
|
3128
|
-
|
|
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;
|
|
3129
3544
|
}
|
|
3130
3545
|
}
|
|
3131
3546
|
|
|
3132
3547
|
function yamlString(str) {
|
|
3133
|
-
|
|
3548
|
+
if (!str) return "";
|
|
3549
|
+
return `"${str.replace(/"/g, "\\\"")}"`;
|
|
3134
3550
|
}
|
|
3135
3551
|
function tapString(str) {
|
|
3136
3552
|
return str.replace(/\\/g, "\\\\").replace(/#/g, "\\#").replace(/\n/g, " ");
|
|
@@ -3139,45 +3555,77 @@ class TapReporter {
|
|
|
3139
3555
|
ctx;
|
|
3140
3556
|
logger;
|
|
3141
3557
|
onInit(ctx) {
|
|
3142
|
-
this.ctx = ctx
|
|
3558
|
+
this.ctx = ctx;
|
|
3559
|
+
this.logger = new IndentedLogger(ctx.logger.log.bind(ctx.logger));
|
|
3143
3560
|
}
|
|
3144
3561
|
static getComment(task) {
|
|
3145
|
-
|
|
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 "";
|
|
3146
3566
|
}
|
|
3147
3567
|
logErrorDetails(error, stack) {
|
|
3148
3568
|
const errorName = error.name || "Unknown Error";
|
|
3149
|
-
|
|
3569
|
+
this.logger.log(`name: ${yamlString(String(errorName))}`);
|
|
3570
|
+
this.logger.log(`message: ${yamlString(String(error.message))}`);
|
|
3571
|
+
if (stack)
|
|
3150
3572
|
// For compatibility with tap-mocha-reporter
|
|
3151
3573
|
this.logger.log(`stack: ${yamlString(`${stack.file}:${stack.line}:${stack.column}`)}`);
|
|
3152
3574
|
}
|
|
3153
3575
|
logTasks(tasks) {
|
|
3154
3576
|
this.logger.log(`1..${tasks.length}`);
|
|
3155
3577
|
for (const [i, task] of tasks.entries()) {
|
|
3156
|
-
const id = i + 1
|
|
3157
|
-
|
|
3158
|
-
|
|
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 {
|
|
3159
3588
|
this.logger.log(`${ok} ${id} - ${tapString(task.name)}${comment}`);
|
|
3160
3589
|
const project = this.ctx.getProjectByName(task.file.projectName || "");
|
|
3161
|
-
if (task.type === "test" && task.annotations)
|
|
3162
|
-
this.logger.
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
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
|
+
}
|
|
3169
3615
|
}
|
|
3170
3616
|
}
|
|
3171
3617
|
}
|
|
3172
3618
|
onTestRunEnd(testModules) {
|
|
3173
3619
|
const files = testModules.map((testModule) => testModule.task);
|
|
3174
|
-
this.logger.log("TAP version 13")
|
|
3620
|
+
this.logger.log("TAP version 13");
|
|
3621
|
+
this.logTasks(files);
|
|
3175
3622
|
}
|
|
3176
3623
|
}
|
|
3177
3624
|
|
|
3178
3625
|
function flattenTasks(task, baseName = "") {
|
|
3179
3626
|
const base = baseName ? `${baseName} > ` : "";
|
|
3180
|
-
|
|
3627
|
+
if (task.type === "suite" && task.tasks.length > 0) return task.tasks.flatMap((child) => flattenTasks(child, `${base}${task.name}`));
|
|
3628
|
+
else return [{
|
|
3181
3629
|
...task,
|
|
3182
3630
|
name: `${base}${task.name}`
|
|
3183
3631
|
}];
|
|
@@ -3209,9 +3657,18 @@ class VerboseReporter extends DefaultReporter {
|
|
|
3209
3657
|
const testResult = test.result();
|
|
3210
3658
|
if (this.ctx.config.hideSkippedTests && testResult.state === "skipped") return;
|
|
3211
3659
|
let title = ` ${this.getEntityPrefix(test)} `;
|
|
3212
|
-
|
|
3213
|
-
if (
|
|
3214
|
-
|
|
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
|
+
}
|
|
3215
3672
|
}
|
|
3216
3673
|
}
|
|
3217
3674
|
|