@vitest-agent/ui 2.2.12 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dispatcher/classify.js +6 -1
- package/dispatcher/dispatch.js +26 -2
- package/dispatcher/helpers.js +29 -15
- package/index.d.ts +62 -1
- package/index.js +1 -1
- package/package.json +2 -2
- package/reducer.js +6 -2
- package/render-agent.js +18 -0
- package/render-ink/StreamApp.js +52 -29
- package/synthesize.js +15 -5
package/dispatcher/classify.js
CHANGED
|
@@ -36,7 +36,11 @@ const classifyRunShape = (state, projects) => {
|
|
|
36
36
|
* run whose only non-passing signal is one or more timed-out tests
|
|
37
37
|
* (`totals.timeoutCount > 0`, `totals.failCount === 0`) also
|
|
38
38
|
* classifies as `some-fail` — timeouts are not passes, and must not
|
|
39
|
-
* route to a passing cell (issue #224).
|
|
39
|
+
* route to a passing cell (issue #224). A run whose only non-passing
|
|
40
|
+
* signal is a process-level unhandled error (a non-empty
|
|
41
|
+
* `unhandledErrors`, all counts otherwise clean) also classifies as `some-fail` —
|
|
42
|
+
* a green-looking count must never hide an unhandled error behind an
|
|
43
|
+
* all-pass cell (issue #240).
|
|
40
44
|
*
|
|
41
45
|
* @param state - the fully-reduced render state
|
|
42
46
|
* @returns the outcome classification
|
|
@@ -44,6 +48,7 @@ const classifyRunShape = (state, projects) => {
|
|
|
44
48
|
*/
|
|
45
49
|
const classifyOutcome = (state) => {
|
|
46
50
|
if (state.totals.failCount > 0) return "some-fail";
|
|
51
|
+
if (state.unhandledErrors.length > 0) return "some-fail";
|
|
47
52
|
if (state.totals.timeoutCount > 0) return "some-fail";
|
|
48
53
|
if (state.coverage !== null && state.coverage.violations.length > 0) return "threshold-violation";
|
|
49
54
|
return "all-pass";
|
package/dispatcher/dispatch.js
CHANGED
|
@@ -10,6 +10,9 @@ import { renderSingleTestThreshold } from "./cells/single-test-threshold.js";
|
|
|
10
10
|
import { renderWorkspaceFail } from "./cells/workspace-fail.js";
|
|
11
11
|
import { renderWorkspacePass } from "./cells/workspace-pass.js";
|
|
12
12
|
import { renderWorkspaceThreshold } from "./cells/workspace-threshold.js";
|
|
13
|
+
import { formatScopedCoverageNote } from "@vitest-agent/sdk";
|
|
14
|
+
import { Box, Text } from "ink";
|
|
15
|
+
import { createElement } from "react";
|
|
13
16
|
|
|
14
17
|
//#region src/dispatcher/dispatch.ts
|
|
15
18
|
/**
|
|
@@ -51,7 +54,24 @@ const dispatcherTable = {
|
|
|
51
54
|
* @public
|
|
52
55
|
*/
|
|
53
56
|
const dispatch = (inputs, opts) => {
|
|
54
|
-
|
|
57
|
+
const body = dispatcherTable[inputs.shape][inputs.outcome].agent(inputs, opts);
|
|
58
|
+
const note = scopedCoverageNoteFor(inputs);
|
|
59
|
+
return note !== null ? `${body}\n${note}` : body;
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Build the scoped-coverage note for this run, or `null` on a full
|
|
63
|
+
* (non-scoped) run. Shared by {@link dispatch} and {@link dispatchInk} so
|
|
64
|
+
* both render paths surface the same information (issue #160 gap 1) —
|
|
65
|
+
* Vitest's coverage thresholds are meaningless against a subset of the
|
|
66
|
+
* project's test files, so every cell's own threshold-flavored coverage
|
|
67
|
+
* text is followed by an explanation of why to disregard it.
|
|
68
|
+
*
|
|
69
|
+
* @internal
|
|
70
|
+
*/
|
|
71
|
+
const scopedCoverageNoteFor = (inputs) => {
|
|
72
|
+
const cov = inputs.state.coverage;
|
|
73
|
+
if (cov === null || cov.scoped !== true) return null;
|
|
74
|
+
return formatScopedCoverageNote(cov.scopedFiles ?? 0, cov.totalFiles);
|
|
55
75
|
};
|
|
56
76
|
/**
|
|
57
77
|
* Dispatch to the cell that matches `(inputs.shape, inputs.outcome)`
|
|
@@ -66,7 +86,11 @@ const dispatch = (inputs, opts) => {
|
|
|
66
86
|
*/
|
|
67
87
|
const dispatchInk = (inputs, opts) => {
|
|
68
88
|
const cell = dispatcherTable[inputs.shape][inputs.outcome];
|
|
69
|
-
|
|
89
|
+
if (cell.ink === void 0) return null;
|
|
90
|
+
const element = cell.ink(inputs, opts);
|
|
91
|
+
const note = scopedCoverageNoteFor(inputs);
|
|
92
|
+
if (note === null) return element;
|
|
93
|
+
return createElement(Box, { flexDirection: "column" }, element, createElement(Text, null, note));
|
|
70
94
|
};
|
|
71
95
|
|
|
72
96
|
//#endregion
|
package/dispatcher/helpers.js
CHANGED
|
@@ -102,9 +102,13 @@ const formatTrendLine = (trend) => {
|
|
|
102
102
|
* project carrying failures or violations.
|
|
103
103
|
*/
|
|
104
104
|
const formatProjectRow = (project, nameWidth) => {
|
|
105
|
-
const
|
|
106
|
-
const
|
|
107
|
-
const
|
|
105
|
+
const timeoutCount = project.timeoutCount ?? 0;
|
|
106
|
+
const total = project.passCount + project.failCount + project.skipCount + timeoutCount;
|
|
107
|
+
const glyph = project.failCount > 0 || timeoutCount > 0 ? "✗" : "✓";
|
|
108
|
+
const countParts = [`${project.passCount}/${total} passed`];
|
|
109
|
+
if (project.failCount > 0) countParts.push(`${project.failCount} failed`);
|
|
110
|
+
if (timeoutCount > 0) countParts.push(`${timeoutCount} timed out`);
|
|
111
|
+
const counts = project.failCount > 0 || timeoutCount > 0 ? countParts.join(", ") : `${project.passCount} passed`;
|
|
108
112
|
const tagSuffix = formatTagCountSuffix(project.tagCounts);
|
|
109
113
|
const base = ` ${glyph} ${project.name.padEnd(nameWidth)} ${counts} (${formatDisplayDuration(project.durationMs)})`;
|
|
110
114
|
return tagSuffix.length === 0 ? base : `${base} ${tagSuffix}`;
|
|
@@ -132,36 +136,46 @@ const formatWorkspaceTotal = (projects) => {
|
|
|
132
136
|
let pass = 0;
|
|
133
137
|
let fail = 0;
|
|
134
138
|
let skip = 0;
|
|
139
|
+
let timeout = 0;
|
|
135
140
|
let durationMs = 0;
|
|
136
141
|
for (const p of projects) {
|
|
137
142
|
pass += p.passCount;
|
|
138
143
|
fail += p.failCount;
|
|
139
144
|
skip += p.skipCount;
|
|
145
|
+
timeout += p.timeoutCount ?? 0;
|
|
140
146
|
durationMs += p.durationMs;
|
|
141
147
|
}
|
|
142
|
-
const total = pass + fail + skip;
|
|
148
|
+
const total = pass + fail + skip + timeout;
|
|
143
149
|
const parts = [`${pass}/${total} passed`];
|
|
144
150
|
if (fail > 0) parts.push(`${fail} failed`);
|
|
151
|
+
if (timeout > 0) parts.push(`${timeout} timed out`);
|
|
145
152
|
if (skip > 0) parts.push(`${skip} skipped`);
|
|
146
153
|
return `Total: ${parts.join(", ")} (${formatDisplayDuration(durationMs)})`;
|
|
147
154
|
};
|
|
148
|
-
const
|
|
155
|
+
const TABLE_COL_FILE_MIN = 60;
|
|
149
156
|
/**
|
|
150
157
|
* Format the `Files below aspirational target:` block — a pipe-delimited
|
|
151
158
|
* table truncated to the first `limit` files with a "+N more" suffix.
|
|
152
159
|
* Returns an empty array when `belowTarget` is empty.
|
|
160
|
+
*
|
|
161
|
+
* The file column is sized to the longest path among the printed rows
|
|
162
|
+
* (never narrower than {@link TABLE_COL_FILE_MIN}), so a printed path is
|
|
163
|
+
* never truncated (issue #237 follow-up) — only the omitted rows (past
|
|
164
|
+
* `limit`) are unaffected, since their width never entered the
|
|
165
|
+
* calculation.
|
|
153
166
|
*/
|
|
154
167
|
const formatBelowTargetTable = (belowTarget, limit) => {
|
|
155
168
|
if (belowTarget.length === 0) return [];
|
|
156
169
|
const top = belowTarget.slice(0, limit);
|
|
157
170
|
const omitted = belowTarget.length - top.length;
|
|
171
|
+
const fileColWidth = Math.max(TABLE_COL_FILE_MIN, ...top.map((f) => f.file.length + 2));
|
|
158
172
|
const header = [
|
|
159
173
|
"Files below aspirational target:",
|
|
160
|
-
buildTableSeparator(),
|
|
161
|
-
buildTableHeader(),
|
|
162
|
-
buildTableSeparator()
|
|
174
|
+
buildTableSeparator(fileColWidth),
|
|
175
|
+
buildTableHeader(fileColWidth),
|
|
176
|
+
buildTableSeparator(fileColWidth)
|
|
163
177
|
];
|
|
164
|
-
const rows = top.map((file) => buildTableRow(file));
|
|
178
|
+
const rows = top.map((file) => buildTableRow(file, fileColWidth));
|
|
165
179
|
const footer = [];
|
|
166
180
|
if (omitted > 0) footer.push(`… ${omitted} more (use the test_coverage MCP tool for the full list)`);
|
|
167
181
|
return [
|
|
@@ -170,14 +184,14 @@ const formatBelowTargetTable = (belowTarget, limit) => {
|
|
|
170
184
|
...footer
|
|
171
185
|
];
|
|
172
186
|
};
|
|
173
|
-
const buildTableSeparator = () => {
|
|
174
|
-
return `${"-".repeat(
|
|
187
|
+
const buildTableSeparator = (fileColWidth) => {
|
|
188
|
+
return `${"-".repeat(fileColWidth)}|---------|---------|---------|---------|-------------------`;
|
|
175
189
|
};
|
|
176
|
-
const buildTableHeader = () => {
|
|
177
|
-
return ` ${"File".padEnd(
|
|
190
|
+
const buildTableHeader = (fileColWidth) => {
|
|
191
|
+
return ` ${"File".padEnd(fileColWidth - 1)}| % Stmts | % Branch| % Funcs | % Lines | Uncovered Line #s`;
|
|
178
192
|
};
|
|
179
|
-
const buildTableRow = (file) => {
|
|
180
|
-
return `${` ${
|
|
193
|
+
const buildTableRow = (file, fileColWidth) => {
|
|
194
|
+
return `${` ${file.file.padEnd(fileColWidth - 1)}`}|${pctCell(file.summary.statements)}|${pctCell(file.summary.branches)}|${pctCell(file.summary.functions)}|${pctCell(file.summary.lines)}|${` ${file.uncoveredLines}`}`;
|
|
181
195
|
};
|
|
182
196
|
const pctCell = (n) => {
|
|
183
197
|
const text = `${Math.round(n)}`;
|
package/index.d.ts
CHANGED
|
@@ -154,6 +154,9 @@ declare const CoverageRenderState: Schema.Struct<{
|
|
|
154
154
|
readonly expected: Schema.Number;
|
|
155
155
|
readonly actual: Schema.Number;
|
|
156
156
|
}>>;
|
|
157
|
+
readonly scoped: Schema.optional<Schema.Boolean>;
|
|
158
|
+
readonly scopedFiles: Schema.optional<Schema.Number>;
|
|
159
|
+
readonly totalFiles: Schema.optional<Schema.Number>;
|
|
157
160
|
}>;
|
|
158
161
|
/** @public */
|
|
159
162
|
type CoverageRenderState = typeof CoverageRenderState.Type;
|
|
@@ -272,6 +275,9 @@ declare const RenderState: Schema.Struct<{
|
|
|
272
275
|
readonly expected: Schema.Number;
|
|
273
276
|
readonly actual: Schema.Number;
|
|
274
277
|
}>>;
|
|
278
|
+
readonly scoped: Schema.optional<Schema.Boolean>;
|
|
279
|
+
readonly scopedFiles: Schema.optional<Schema.Number>;
|
|
280
|
+
readonly totalFiles: Schema.optional<Schema.Number>;
|
|
275
281
|
}>>;
|
|
276
282
|
readonly trend: Schema.NullOr<Schema.Struct<{
|
|
277
283
|
readonly direction: Schema.Literals<readonly ["improving", "regressing", "stable"]>;
|
|
@@ -307,6 +313,19 @@ declare const RenderState: Schema.Struct<{
|
|
|
307
313
|
* fixtures) falls back to `moduleOrder.length`.
|
|
308
314
|
*/
|
|
309
315
|
readonly collectedModules: Schema.optional<Schema.Number>;
|
|
316
|
+
/**
|
|
317
|
+
* Process-level errors with no owning module, folded from
|
|
318
|
+
* `RunFinished.unhandledErrors`. A run that never carried the field
|
|
319
|
+
* (older replay data, hand-built fixtures) defaults to `[]`. See
|
|
320
|
+
* issue #240.
|
|
321
|
+
*/
|
|
322
|
+
readonly unhandledErrors: Schema.$Array<Schema.Struct<{
|
|
323
|
+
readonly message: Schema.String;
|
|
324
|
+
readonly stack: Schema.optional<Schema.String>;
|
|
325
|
+
readonly diff: Schema.optional<Schema.String>;
|
|
326
|
+
readonly expected: Schema.optional<Schema.String>;
|
|
327
|
+
readonly received: Schema.optional<Schema.String>;
|
|
328
|
+
}>>;
|
|
310
329
|
}>;
|
|
311
330
|
/** @public */
|
|
312
331
|
type RenderState = typeof RenderState.Type;
|
|
@@ -356,6 +375,7 @@ interface ProjectSummary {
|
|
|
356
375
|
readonly failCount: number;
|
|
357
376
|
readonly skipCount: number;
|
|
358
377
|
readonly durationMs: number;
|
|
378
|
+
readonly timeoutCount?: number;
|
|
359
379
|
readonly tagCounts?: Record<string, number>;
|
|
360
380
|
readonly belowTarget?: number;
|
|
361
381
|
readonly violations?: number;
|
|
@@ -549,6 +569,7 @@ declare const AgentReport: Schema.Struct<{
|
|
|
549
569
|
}>>;
|
|
550
570
|
readonly scoped: Schema.withDecodingDefaultKey<Schema.Boolean, never>;
|
|
551
571
|
readonly scopedFiles: Schema.optional<Schema.$Array<Schema.String>>;
|
|
572
|
+
readonly totalFiles: Schema.optional<Schema.Number>;
|
|
552
573
|
readonly lowCoverage: Schema.$Array<Schema.Struct<{
|
|
553
574
|
readonly file: Schema.String;
|
|
554
575
|
readonly summary: Schema.Struct<{
|
|
@@ -587,6 +608,10 @@ declare const AgentReport: Schema.Struct<{
|
|
|
587
608
|
readonly sample: Schema.optional<Schema.String>;
|
|
588
609
|
}>>;
|
|
589
610
|
readonly truncated: Schema.optional<Schema.Boolean>;
|
|
611
|
+
readonly fromFailingTests: Schema.optional<Schema.Struct<{
|
|
612
|
+
readonly total: Schema.Number;
|
|
613
|
+
readonly files: Schema.Number;
|
|
614
|
+
}>>;
|
|
590
615
|
}>>;
|
|
591
616
|
}>;
|
|
592
617
|
/** @public */
|
|
@@ -758,6 +783,9 @@ declare const RunEvent: Schema.Union<readonly [Schema.TaggedStruct<"RunStarted",
|
|
|
758
783
|
}>;
|
|
759
784
|
readonly uncoveredLines: Schema.optional<Schema.String>;
|
|
760
785
|
}>>;
|
|
786
|
+
readonly scoped: Schema.optional<Schema.Boolean>;
|
|
787
|
+
readonly scopedFiles: Schema.optional<Schema.Number>;
|
|
788
|
+
readonly totalFiles: Schema.optional<Schema.Number>;
|
|
761
789
|
}>, Schema.TaggedStruct<"ThresholdViolation", {
|
|
762
790
|
readonly metric: Schema.Literals<readonly ["lines", "branches", "functions", "statements"]>;
|
|
763
791
|
readonly expected: Schema.Number;
|
|
@@ -787,6 +815,19 @@ declare const RunEvent: Schema.Union<readonly [Schema.TaggedStruct<"RunStarted",
|
|
|
787
815
|
* tracks failing modules (report replay). See issue #204.
|
|
788
816
|
*/
|
|
789
817
|
readonly collectedModules: Schema.optional<Schema.Number>;
|
|
818
|
+
/**
|
|
819
|
+
* Process-level errors with no owning module (an unhandled
|
|
820
|
+
* rejection, a worker crash) — Vitest's `unhandledErrors`
|
|
821
|
+
* argument to `onTestRunEnd`. Optional so hand-built fixtures
|
|
822
|
+
* and older replay data keep decoding. See issue #240.
|
|
823
|
+
*/
|
|
824
|
+
readonly unhandledErrors: Schema.optional<Schema.$Array<Schema.Struct<{
|
|
825
|
+
readonly message: Schema.String;
|
|
826
|
+
readonly stack: Schema.optional<Schema.String>;
|
|
827
|
+
readonly diff: Schema.optional<Schema.String>;
|
|
828
|
+
readonly expected: Schema.optional<Schema.String>;
|
|
829
|
+
readonly received: Schema.optional<Schema.String>;
|
|
830
|
+
}>>>;
|
|
790
831
|
}>]>;
|
|
791
832
|
/** @public */
|
|
792
833
|
type RunEvent = typeof RunEvent.Type;
|
|
@@ -1014,7 +1055,11 @@ declare const classifyRunShape: (state: RenderState, projects: ReadonlyArray<Pro
|
|
|
1014
1055
|
* run whose only non-passing signal is one or more timed-out tests
|
|
1015
1056
|
* (`totals.timeoutCount > 0`, `totals.failCount === 0`) also
|
|
1016
1057
|
* classifies as `some-fail` — timeouts are not passes, and must not
|
|
1017
|
-
* route to a passing cell (issue #224).
|
|
1058
|
+
* route to a passing cell (issue #224). A run whose only non-passing
|
|
1059
|
+
* signal is a process-level unhandled error (a non-empty
|
|
1060
|
+
* `unhandledErrors`, all counts otherwise clean) also classifies as `some-fail` —
|
|
1061
|
+
* a green-looking count must never hide an unhandled error behind an
|
|
1062
|
+
* all-pass cell (issue #240).
|
|
1018
1063
|
*
|
|
1019
1064
|
* @param state - the fully-reduced render state
|
|
1020
1065
|
* @returns the outcome classification
|
|
@@ -1247,6 +1292,9 @@ declare const RunEventChannel_base: Context.ServiceClass<RunEventChannel, "vites
|
|
|
1247
1292
|
};
|
|
1248
1293
|
readonly uncoveredLines?: string | undefined;
|
|
1249
1294
|
}[];
|
|
1295
|
+
readonly scoped?: boolean | undefined;
|
|
1296
|
+
readonly scopedFiles?: number | undefined;
|
|
1297
|
+
readonly totalFiles?: number | undefined;
|
|
1250
1298
|
} | {
|
|
1251
1299
|
readonly _tag: "ThresholdViolation";
|
|
1252
1300
|
readonly metric: "branches" | "functions" | "lines" | "statements";
|
|
@@ -1273,6 +1321,13 @@ declare const RunEventChannel_base: Context.ServiceClass<RunEventChannel, "vites
|
|
|
1273
1321
|
readonly durationMs: number;
|
|
1274
1322
|
readonly timeoutCount?: number | undefined;
|
|
1275
1323
|
readonly collectedModules?: number | undefined;
|
|
1324
|
+
readonly unhandledErrors?: readonly {
|
|
1325
|
+
readonly message: string;
|
|
1326
|
+
readonly stack?: string | undefined;
|
|
1327
|
+
readonly diff?: string | undefined;
|
|
1328
|
+
readonly expected?: string | undefined;
|
|
1329
|
+
readonly received?: string | undefined;
|
|
1330
|
+
}[] | undefined;
|
|
1276
1331
|
}>>;
|
|
1277
1332
|
/**
|
|
1278
1333
|
* Service tag for the run-event channel. Consumers depend on this tag;
|
|
@@ -1833,6 +1888,12 @@ interface SynthesizedCoverage {
|
|
|
1833
1888
|
readonly expected: number;
|
|
1834
1889
|
readonly actual: number;
|
|
1835
1890
|
}>;
|
|
1891
|
+
/** Set when this run's coverage was scoped to a subset of test files (issue #160). */
|
|
1892
|
+
readonly scoped?: boolean;
|
|
1893
|
+
/** Number of test files actually run, when `scoped` is true. */
|
|
1894
|
+
readonly scopedFiles?: number;
|
|
1895
|
+
/** Total test-file count for the project, when known. */
|
|
1896
|
+
readonly totalFiles?: number;
|
|
1836
1897
|
}
|
|
1837
1898
|
/**
|
|
1838
1899
|
* Project a fully-completed Vitest run into the canonical
|
package/index.js
CHANGED
|
@@ -33,7 +33,7 @@ import { ActionSeverity, CoverageFile, CoverageGap, CoverageMetric, CoverageRend
|
|
|
33
33
|
*
|
|
34
34
|
* @public
|
|
35
35
|
*/
|
|
36
|
-
const CURRENT_UI_VERSION = "2.
|
|
36
|
+
const CURRENT_UI_VERSION = "2.3.0";
|
|
37
37
|
|
|
38
38
|
//#endregion
|
|
39
39
|
export { ActionSeverity, CURRENT_UI_VERSION, CountColumns, CoverageBlock, CoverageFile, CoverageGap, CoverageMetric, CoverageRenderState, DURATION_CELL_WIDTH, FailureRecord, FailureSection, FailuresSection, ModuleHeader, ModuleRecord, ProjectRow, RenderState, RenderTotals, RunEvent, RunEventChannel, RunEventChannelLive, SPINNER_FRAMES, SPINNER_FRAME_MS, SUITE_LOAD_FAILURE_LABEL, StatusIcon, StreamApp, SuggestedActionRecord, SuggestedActions, TagColumns, TestRecord, TestRow, TrendLine, accumulateUntilFinished, buildFooter, classifyOutcome, classifyRunShape, dispatch, dispatchInk, dispatcherTable, dominantClassification, forEachRenderState, formatDisplayDuration, initialRenderState, publish, publishAll, reduceRenderState, reduceRenderStateAll, renderAgent, renderStateStream, spinnerFrame, spinnerFrameForTime, subscribeRaw, synthesizeFromAgentReport, synthesizeRunEvents, tagUnion };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vitest-agent/ui",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Shared event-sourced renderer for vitest-agent. React Ink view for humans, plain-string view for agents, both fed by the same reducer over a RunEvent stream.",
|
|
6
6
|
"keywords": [
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"./package.json": "./package.json"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@vitest-agent/sdk": "2.
|
|
42
|
+
"@vitest-agent/sdk": "2.5.0",
|
|
43
43
|
"effect": "4.0.0-rc.109"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
package/reducer.js
CHANGED
|
@@ -175,7 +175,10 @@ const reduceRenderState = (state, event) => {
|
|
|
175
175
|
metrics: e.metrics,
|
|
176
176
|
thresholds: e.thresholds,
|
|
177
177
|
gaps: e.gaps,
|
|
178
|
-
violations: state.coverage?.violations ?? []
|
|
178
|
+
violations: state.coverage?.violations ?? [],
|
|
179
|
+
...e.scoped !== void 0 ? { scoped: e.scoped } : {},
|
|
180
|
+
...e.scopedFiles !== void 0 ? { scopedFiles: e.scopedFiles } : {},
|
|
181
|
+
...e.totalFiles !== void 0 ? { totalFiles: e.totalFiles } : {}
|
|
179
182
|
}
|
|
180
183
|
}),
|
|
181
184
|
ThresholdViolation: (e) => {
|
|
@@ -219,7 +222,8 @@ const reduceRenderState = (state, event) => {
|
|
|
219
222
|
timeoutCount: e.timeoutCount ?? state.totals.timeoutCount,
|
|
220
223
|
durationMs: e.durationMs
|
|
221
224
|
},
|
|
222
|
-
...e.collectedModules !== void 0 && { collectedModules: e.collectedModules }
|
|
225
|
+
...e.collectedModules !== void 0 && { collectedModules: e.collectedModules },
|
|
226
|
+
...e.unhandledErrors !== void 0 && { unhandledErrors: e.unhandledErrors }
|
|
223
227
|
}),
|
|
224
228
|
RunTimedOut: () => ({
|
|
225
229
|
...state,
|
package/render-agent.js
CHANGED
|
@@ -14,6 +14,10 @@ const formatHeader = (state) => {
|
|
|
14
14
|
if (failCount > 0) parts.push(`${failCount} failed`);
|
|
15
15
|
if (timeoutCount > 0) parts.push(`${timeoutCount} timed out`);
|
|
16
16
|
if (skipCount > 0) parts.push(`${skipCount} skipped`);
|
|
17
|
+
if (state.unhandledErrors.length > 0) {
|
|
18
|
+
const noun = state.unhandledErrors.length === 1 ? "unhandled error" : "unhandled errors";
|
|
19
|
+
parts.push(`${state.unhandledErrors.length} ${noun}`);
|
|
20
|
+
}
|
|
17
21
|
return `Tests: ${parts.join(", ")} (${formatDisplayDuration(durationMs)})`;
|
|
18
22
|
};
|
|
19
23
|
const formatModulesSection = (state) => {
|
|
@@ -61,6 +65,18 @@ const formatFailuresSection = (state, width, includeStack) => {
|
|
|
61
65
|
for (const f of state.failures) lines.push(formatFailure(f, width, includeStack));
|
|
62
66
|
return lines.join("\n");
|
|
63
67
|
};
|
|
68
|
+
const formatUnhandledError = (err, width, includeStack) => {
|
|
69
|
+
const firstLine = err.message.split("\n", 1)[0] ?? "";
|
|
70
|
+
const lines = [`- ${truncate(firstLine, Math.max(20, width - 2))}`];
|
|
71
|
+
if (includeStack && err.stack !== void 0) for (const stackLine of err.stack.split("\n")) lines.push(` ${truncate(stackLine, Math.max(20, width - 2))}`);
|
|
72
|
+
return lines.join("\n");
|
|
73
|
+
};
|
|
74
|
+
const formatUnhandledErrorsSection = (state, width, includeStack) => {
|
|
75
|
+
if (state.unhandledErrors.length === 0) return null;
|
|
76
|
+
const lines = ["Unhandled errors:"];
|
|
77
|
+
for (const err of state.unhandledErrors) lines.push(formatUnhandledError(err, width, includeStack));
|
|
78
|
+
return lines.join("\n");
|
|
79
|
+
};
|
|
64
80
|
const formatPercent = (n) => {
|
|
65
81
|
const rounded = Math.round(n * 10) / 10;
|
|
66
82
|
return Number.isInteger(rounded) ? `${rounded}%` : `${rounded}%`;
|
|
@@ -128,6 +144,8 @@ const renderAgent = (state, options = {}) => {
|
|
|
128
144
|
const sections = [formatHeader(state)];
|
|
129
145
|
const failures = formatFailuresSection(state, width, includeStack);
|
|
130
146
|
if (failures !== null) sections.push(failures);
|
|
147
|
+
const unhandledErrors = formatUnhandledErrorsSection(state, width, includeStack);
|
|
148
|
+
if (unhandledErrors !== null) sections.push(unhandledErrors);
|
|
131
149
|
const modules = formatModulesSection(state);
|
|
132
150
|
if (modules !== null) sections.push(modules);
|
|
133
151
|
const coverage = formatCoverageSection(state, maxGaps);
|
package/render-ink/StreamApp.js
CHANGED
|
@@ -58,6 +58,21 @@ const sumCounts = (modules) => {
|
|
|
58
58
|
timeoutCount
|
|
59
59
|
};
|
|
60
60
|
};
|
|
61
|
+
/**
|
|
62
|
+
* Build a `ProjectSummary` from a project's name and its rolled-up
|
|
63
|
+
* counts. Includes `timeoutCount` only when nonzero, matching
|
|
64
|
+
* `summarizeProject`'s convention in `@vitest-agent/reporter` (issue #242).
|
|
65
|
+
*
|
|
66
|
+
* @public
|
|
67
|
+
*/
|
|
68
|
+
const buildProjectSummary = (name, counts) => ({
|
|
69
|
+
name,
|
|
70
|
+
passCount: counts.passCount,
|
|
71
|
+
failCount: counts.failCount,
|
|
72
|
+
skipCount: counts.skipCount,
|
|
73
|
+
durationMs: 0,
|
|
74
|
+
...counts.timeoutCount > 0 ? { timeoutCount: counts.timeoutCount } : {}
|
|
75
|
+
});
|
|
61
76
|
const mergeTagCounts = (modules) => {
|
|
62
77
|
const out = {};
|
|
63
78
|
for (const m of modules) for (const [tag, count] of Object.entries(m.tagCounts ?? {})) out[tag] = (out[tag] ?? 0) + count;
|
|
@@ -166,6 +181,21 @@ const InlineError = ({ failure }) => {
|
|
|
166
181
|
});
|
|
167
182
|
};
|
|
168
183
|
const failureKey = (f) => `failure:${f.modulePath}::${f.suitePath.join("/")}::${f.testName}`;
|
|
184
|
+
/** A process-level unhandled error, rendered in the aggregate Unhandled errors block. */
|
|
185
|
+
const UnhandledErrorItem = ({ error }) => /* @__PURE__ */ jsxs(Box, {
|
|
186
|
+
flexDirection: "column",
|
|
187
|
+
children: [/* @__PURE__ */ jsxs(Text, { children: [
|
|
188
|
+
" ",
|
|
189
|
+
/* @__PURE__ */ jsx(Text, {
|
|
190
|
+
color: "red",
|
|
191
|
+
children: "✗"
|
|
192
|
+
}),
|
|
193
|
+
" unhandled error"
|
|
194
|
+
] }), /* @__PURE__ */ jsxs(Text, {
|
|
195
|
+
dimColor: true,
|
|
196
|
+
children: [" ", error.message.split("\n", 1)[0] ?? ""]
|
|
197
|
+
})]
|
|
198
|
+
});
|
|
169
199
|
const testKey = (modulePath, t) => `test:${modulePath}::${t.suitePath.join("/")}::${t.testName}`;
|
|
170
200
|
const failurePath = (f) => [
|
|
171
201
|
f.modulePath,
|
|
@@ -248,6 +278,13 @@ const liveRegion = (state, shape, nowMs, frame) => {
|
|
|
248
278
|
const timedOut = state.phase === "timed-out";
|
|
249
279
|
const finished = state.phase === "finished" || state.phase === "timed-out";
|
|
250
280
|
const ordered = state.moduleOrder.map((p) => state.modules[p]).filter((m) => m !== void 0);
|
|
281
|
+
const unhandledErrorsSection = state.unhandledErrors.length > 0 ? /* @__PURE__ */ jsxs(Box, {
|
|
282
|
+
flexDirection: "column",
|
|
283
|
+
children: [/* @__PURE__ */ jsx(Text, {
|
|
284
|
+
bold: true,
|
|
285
|
+
children: "Unhandled errors:"
|
|
286
|
+
}), state.unhandledErrors.map((err, i) => /* @__PURE__ */ jsx(UnhandledErrorItem, { error: err }, `unhandled:${i}:${err.message}`))]
|
|
287
|
+
}) : null;
|
|
251
288
|
if (shape === "single-test") {
|
|
252
289
|
const sole = ordered[0];
|
|
253
290
|
if (sole === void 0 || sole.tests.length === 0) return /* @__PURE__ */ jsx(Text, {
|
|
@@ -260,10 +297,14 @@ const liveRegion = (state, shape, nowMs, frame) => {
|
|
|
260
297
|
children: "discovering tests…"
|
|
261
298
|
});
|
|
262
299
|
const failure = only.status === "failed" || only.status === "timed-out" ? state.failures.find((f) => f.testName === only.testName && f.modulePath === sole.modulePath && samePath(f.suitePath, only.suitePath)) : void 0;
|
|
263
|
-
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
300
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
301
|
+
/* @__PURE__ */ jsx(TestRow, {
|
|
302
|
+
test: only,
|
|
303
|
+
indent: 0
|
|
304
|
+
}),
|
|
305
|
+
failure !== void 0 ? /* @__PURE__ */ jsx(InlineError, { failure }) : null,
|
|
306
|
+
unhandledErrorsSection
|
|
307
|
+
] });
|
|
267
308
|
}
|
|
268
309
|
if (groups.length === 0) return /* @__PURE__ */ jsx(Text, {
|
|
269
310
|
dimColor: true,
|
|
@@ -297,13 +338,7 @@ const liveRegion = (state, shape, nowMs, frame) => {
|
|
|
297
338
|
if (runningVisible < MAX_LIVE_RUNNING_ROWS) {
|
|
298
339
|
runningVisible++;
|
|
299
340
|
const counts = sumCounts(g.modules);
|
|
300
|
-
const summary =
|
|
301
|
-
name: g.name,
|
|
302
|
-
passCount: counts.passCount,
|
|
303
|
-
failCount: counts.failCount,
|
|
304
|
-
skipCount: counts.skipCount,
|
|
305
|
-
durationMs: 0
|
|
306
|
-
};
|
|
341
|
+
const summary = buildProjectSummary(g.name, counts);
|
|
307
342
|
rows.push(/* @__PURE__ */ jsx(ProjectRow, {
|
|
308
343
|
project: summary,
|
|
309
344
|
counts,
|
|
@@ -318,13 +353,7 @@ const liveRegion = (state, shape, nowMs, frame) => {
|
|
|
318
353
|
} else runningOverflow++;
|
|
319
354
|
} else {
|
|
320
355
|
const counts = sumCounts(g.modules);
|
|
321
|
-
const summary =
|
|
322
|
-
name: g.name,
|
|
323
|
-
passCount: counts.passCount,
|
|
324
|
-
failCount: counts.failCount,
|
|
325
|
-
skipCount: counts.skipCount,
|
|
326
|
-
durationMs: 0
|
|
327
|
-
};
|
|
356
|
+
const summary = buildProjectSummary(g.name, counts);
|
|
328
357
|
rows.push(/* @__PURE__ */ jsx(ProjectRow, {
|
|
329
358
|
project: summary,
|
|
330
359
|
counts,
|
|
@@ -356,6 +385,7 @@ const liveRegion = (state, shape, nowMs, frame) => {
|
|
|
356
385
|
]
|
|
357
386
|
}) : null,
|
|
358
387
|
failuresSection,
|
|
388
|
+
unhandledErrorsSection,
|
|
359
389
|
coverageItem,
|
|
360
390
|
trendItem,
|
|
361
391
|
totalsItem(nameWidth + 4)
|
|
@@ -406,6 +436,7 @@ const liveRegion = (state, shape, nowMs, frame) => {
|
|
|
406
436
|
]
|
|
407
437
|
}) : null,
|
|
408
438
|
failuresSection,
|
|
439
|
+
unhandledErrorsSection,
|
|
409
440
|
coverageItem,
|
|
410
441
|
trendItem,
|
|
411
442
|
totalsItem()
|
|
@@ -438,6 +469,7 @@ const liveRegion = (state, shape, nowMs, frame) => {
|
|
|
438
469
|
}), failure !== void 0 ? /* @__PURE__ */ jsx(InlineError, { failure }) : null]
|
|
439
470
|
}, testKey(sole.modulePath, t));
|
|
440
471
|
}),
|
|
472
|
+
unhandledErrorsSection,
|
|
441
473
|
coverageItem,
|
|
442
474
|
trendItem,
|
|
443
475
|
totalsItem()
|
|
@@ -452,16 +484,7 @@ const liveRegion = (state, shape, nowMs, frame) => {
|
|
|
452
484
|
const StreamApp = ({ state, frameIndex, nowMs }) => {
|
|
453
485
|
const now = nowMs ?? Date.now();
|
|
454
486
|
const frame = spinnerFrame(frameIndex);
|
|
455
|
-
const projects = groupByProject(state).map((g) =>
|
|
456
|
-
const c = sumCounts(g.modules);
|
|
457
|
-
return {
|
|
458
|
-
name: g.name,
|
|
459
|
-
passCount: c.passCount,
|
|
460
|
-
failCount: c.failCount,
|
|
461
|
-
skipCount: c.skipCount,
|
|
462
|
-
durationMs: 0
|
|
463
|
-
};
|
|
464
|
-
});
|
|
487
|
+
const projects = groupByProject(state).map((g) => buildProjectSummary(g.name, sumCounts(g.modules)));
|
|
465
488
|
const shape = classifyRunShape(state, projects);
|
|
466
489
|
const liveContent = liveRegion(state, shape, now, frame);
|
|
467
490
|
return /* @__PURE__ */ jsx(Box, {
|
|
@@ -471,4 +494,4 @@ const StreamApp = ({ state, frameIndex, nowMs }) => {
|
|
|
471
494
|
};
|
|
472
495
|
|
|
473
496
|
//#endregion
|
|
474
|
-
export { StreamApp };
|
|
497
|
+
export { StreamApp, buildProjectSummary };
|
package/synthesize.js
CHANGED
|
@@ -147,7 +147,10 @@ const synthesizeRunEvents = (modules, options = {}) => {
|
|
|
147
147
|
_tag: "CoverageReady",
|
|
148
148
|
metrics: cov.metrics,
|
|
149
149
|
thresholds: cov.thresholds,
|
|
150
|
-
gaps: cov.gaps
|
|
150
|
+
gaps: cov.gaps,
|
|
151
|
+
...cov.scoped !== void 0 ? { scoped: cov.scoped } : {},
|
|
152
|
+
...cov.scopedFiles !== void 0 ? { scopedFiles: cov.scopedFiles } : {},
|
|
153
|
+
...cov.totalFiles !== void 0 ? { totalFiles: cov.totalFiles } : {}
|
|
151
154
|
});
|
|
152
155
|
if (cov.violations !== void 0) for (const v of cov.violations) events.push({
|
|
153
156
|
_tag: "ThresholdViolation",
|
|
@@ -189,7 +192,7 @@ const coverageReportToBlock = (report) => {
|
|
|
189
192
|
const cov = report.coverage;
|
|
190
193
|
if (cov === void 0) return void 0;
|
|
191
194
|
const violations = [];
|
|
192
|
-
for (const metric of [
|
|
195
|
+
if (!cov.scoped) for (const metric of [
|
|
193
196
|
"lines",
|
|
194
197
|
"branches",
|
|
195
198
|
"functions",
|
|
@@ -212,7 +215,10 @@ const coverageReportToBlock = (report) => {
|
|
|
212
215
|
metrics: cov.totals,
|
|
213
216
|
thresholds: cov.thresholds.global,
|
|
214
217
|
gaps,
|
|
215
|
-
violations
|
|
218
|
+
violations,
|
|
219
|
+
...cov.scoped ? { scoped: cov.scoped } : {},
|
|
220
|
+
...cov.scopedFiles !== void 0 ? { scopedFiles: cov.scopedFiles.length } : {},
|
|
221
|
+
...cov.totalFiles !== void 0 ? { totalFiles: cov.totalFiles } : {}
|
|
216
222
|
};
|
|
217
223
|
};
|
|
218
224
|
/**
|
|
@@ -331,7 +337,10 @@ const synthesizeFromAgentReport = (report, options = {}) => {
|
|
|
331
337
|
_tag: "CoverageReady",
|
|
332
338
|
metrics: coverage.metrics,
|
|
333
339
|
thresholds: coverage.thresholds,
|
|
334
|
-
gaps: coverage.gaps
|
|
340
|
+
gaps: coverage.gaps,
|
|
341
|
+
...coverage.scoped !== void 0 ? { scoped: coverage.scoped } : {},
|
|
342
|
+
...coverage.scopedFiles !== void 0 ? { scopedFiles: coverage.scopedFiles } : {},
|
|
343
|
+
...coverage.totalFiles !== void 0 ? { totalFiles: coverage.totalFiles } : {}
|
|
335
344
|
});
|
|
336
345
|
if (coverage.violations !== void 0) for (const v of coverage.violations) events.push({
|
|
337
346
|
_tag: "ThresholdViolation",
|
|
@@ -362,7 +371,8 @@ const synthesizeFromAgentReport = (report, options = {}) => {
|
|
|
362
371
|
skipCount: report.summary.skipped,
|
|
363
372
|
durationMs: report.summary.duration,
|
|
364
373
|
...totalTimeoutCount > 0 && { timeoutCount: totalTimeoutCount },
|
|
365
|
-
...report.summary.modules !== void 0 && { collectedModules: report.summary.modules }
|
|
374
|
+
...report.summary.modules !== void 0 && { collectedModules: report.summary.modules },
|
|
375
|
+
...report.unhandledErrors.length > 0 && { unhandledErrors: report.unhandledErrors }
|
|
366
376
|
});
|
|
367
377
|
return events;
|
|
368
378
|
};
|