@vitest-agent/ui 2.5.3 → 3.0.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/counts.js +61 -0
- package/dispatcher/cells/single-file-pass.js +2 -1
- package/dispatcher/cells/single-file-threshold.js +2 -1
- package/dispatcher/cells/single-project-pass.js +2 -1
- package/dispatcher/cells/single-test-fail.js +2 -1
- package/dispatcher/cells/single-test-pass.js +2 -1
- package/dispatcher/helpers.js +38 -49
- package/dispatcher/ink-helpers.js +32 -56
- package/format-duration.js +10 -10
- package/index.d.ts +97 -37
- package/index.js +4 -3
- package/package.json +7 -3
- package/render-agent.js +23 -29
- package/render-ink/CountColumns.js +20 -19
- package/render-ink/CoverageBlock.js +13 -7
- package/render-ink/FailureSection.js +12 -5
- package/render-ink/FailuresSection.js +16 -3
- package/render-ink/ProjectRow.js +2 -1
- package/render-ink/StatusIcon.js +24 -25
- package/render-ink/StreamApp.js +76 -55
- package/render-ink/SuggestedActions.js +7 -5
- package/render-ink/TagColumns.js +2 -1
- package/render-ink/TrendLine.js +9 -9
- package/render-ink/index.js +2 -2
- package/render-ink/spinner.js +14 -43
- package/theme.js +82 -0
package/counts.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { VitestAgentStatus } from "./theme.js";
|
|
2
|
+
import { Doc, Render } from "@effected/cli";
|
|
3
|
+
|
|
4
|
+
//#region src/counts.ts
|
|
5
|
+
/** The test counters a totals line folds into its `n/total`. */
|
|
6
|
+
const TEST_KEYS = /* @__PURE__ */ new Set([
|
|
7
|
+
"pass",
|
|
8
|
+
"fail",
|
|
9
|
+
"timeout",
|
|
10
|
+
"skip"
|
|
11
|
+
]);
|
|
12
|
+
/** An agent is always escape-free; colour none, no links, no width. */
|
|
13
|
+
const AGENT_CONTEXT = Render.contextOf({ audience: "agent" });
|
|
14
|
+
const sumTests = (counters) => counters.reduce((sum, c) => TEST_KEYS.has(c.key) ? sum + c.n : sum, 0);
|
|
15
|
+
/**
|
|
16
|
+
* Render one totals line as plain agent text.
|
|
17
|
+
*
|
|
18
|
+
* @internal
|
|
19
|
+
*/
|
|
20
|
+
const formatTotalsLine = (input) => {
|
|
21
|
+
const unhandled = input.unhandledErrors ?? 0;
|
|
22
|
+
const counters = [
|
|
23
|
+
Doc.counter(VitestAgentStatus, "success", {
|
|
24
|
+
key: "pass",
|
|
25
|
+
label: "passed",
|
|
26
|
+
n: input.passCount,
|
|
27
|
+
showZero: true
|
|
28
|
+
}),
|
|
29
|
+
Doc.counter(VitestAgentStatus, "failure", {
|
|
30
|
+
key: "fail",
|
|
31
|
+
label: "failed",
|
|
32
|
+
n: input.failCount
|
|
33
|
+
}),
|
|
34
|
+
Doc.counter(VitestAgentStatus, "timeout", {
|
|
35
|
+
key: "timeout",
|
|
36
|
+
label: "timed out",
|
|
37
|
+
n: input.timeoutCount
|
|
38
|
+
}),
|
|
39
|
+
Doc.counter(VitestAgentStatus, "skip", {
|
|
40
|
+
key: "skip",
|
|
41
|
+
label: "skipped",
|
|
42
|
+
n: input.skipCount
|
|
43
|
+
}),
|
|
44
|
+
Doc.counter(VitestAgentStatus, "failure", {
|
|
45
|
+
key: "unhandled",
|
|
46
|
+
label: unhandled === 1 ? "unhandled error" : "unhandled errors",
|
|
47
|
+
n: unhandled
|
|
48
|
+
})
|
|
49
|
+
];
|
|
50
|
+
return Render.plain([Doc.counts({
|
|
51
|
+
label: input.label,
|
|
52
|
+
counters,
|
|
53
|
+
total: sumTests,
|
|
54
|
+
durationMs: input.durationMs,
|
|
55
|
+
layout: "inline",
|
|
56
|
+
...input.suffix !== void 0 ? { suffix: input.suffix } : {}
|
|
57
|
+
})], AGENT_CONTEXT);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
//#endregion
|
|
61
|
+
export { formatTotalsLine };
|
|
@@ -2,13 +2,14 @@ import { buildFooter } from "../footer.js";
|
|
|
2
2
|
import { formatDisplayDuration } from "../../format-duration.js";
|
|
3
3
|
import { soleModulePath } from "../helpers.js";
|
|
4
4
|
import { renderAgentStringAsInk } from "../ink-helpers.js";
|
|
5
|
+
import { Fmt } from "@effected/cli";
|
|
5
6
|
|
|
6
7
|
//#region src/dispatcher/cells/single-file-pass.ts
|
|
7
8
|
const renderAgent = (inputs) => {
|
|
8
9
|
const modulePath = soleModulePath(inputs.state);
|
|
9
10
|
if (modulePath === void 0) return "";
|
|
10
11
|
const { passCount, durationMs } = inputs.state.totals;
|
|
11
|
-
return `${modulePath}: ${passCount
|
|
12
|
+
return `${modulePath}: ${Fmt.plural(passCount, "test")} passed (${formatDisplayDuration(durationMs)})\n${buildFooter(inputs)}`;
|
|
12
13
|
};
|
|
13
14
|
const renderSingleFilePass = {
|
|
14
15
|
agent: renderAgent,
|
|
@@ -2,13 +2,14 @@ import { buildFooter } from "../footer.js";
|
|
|
2
2
|
import { formatDisplayDuration } from "../../format-duration.js";
|
|
3
3
|
import { formatCoverageJudgmentLine, soleModulePath } from "../helpers.js";
|
|
4
4
|
import { renderAgentStringAsInk } from "../ink-helpers.js";
|
|
5
|
+
import { Fmt } from "@effected/cli";
|
|
5
6
|
|
|
6
7
|
//#region src/dispatcher/cells/single-file-threshold.ts
|
|
7
8
|
const renderAgent = (inputs) => {
|
|
8
9
|
const modulePath = soleModulePath(inputs.state);
|
|
9
10
|
if (modulePath === void 0) return "";
|
|
10
11
|
const { passCount, durationMs } = inputs.state.totals;
|
|
11
|
-
const lines = [`${modulePath}: ${passCount
|
|
12
|
+
const lines = [`${modulePath}: ${Fmt.plural(passCount, "test")} passed (${formatDisplayDuration(durationMs)})`];
|
|
12
13
|
const coverage = formatCoverageJudgmentLine(inputs.state);
|
|
13
14
|
if (coverage !== null) lines.push(coverage);
|
|
14
15
|
return `${lines.join("\n")}\n${buildFooter(inputs)}`;
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { buildFooter } from "../footer.js";
|
|
2
2
|
import { formatTotals } from "../helpers.js";
|
|
3
3
|
import { renderAgentStringAsInk } from "../ink-helpers.js";
|
|
4
|
+
import { Fmt } from "@effected/cli";
|
|
4
5
|
|
|
5
6
|
//#region src/dispatcher/cells/single-project-pass.ts
|
|
6
7
|
const renderAgent = (inputs) => {
|
|
7
8
|
const { passCount, failCount, skipCount, timeoutCount } = inputs.state.totals;
|
|
8
9
|
if (passCount + failCount + skipCount + timeoutCount === 0) return `${[formatTotals(inputs.state), "0 tests collected. A zero-test run usually means a wrong working directory, a filter that matched nothing, or a load-time error — verify before trusting it."].join("\n\n")}\n${buildFooter(inputs)}`;
|
|
9
10
|
const collected = inputs.state.collectedModules ?? inputs.state.moduleOrder.length;
|
|
10
|
-
return `${(collected === 0 ? [formatTotals(inputs.state)] : [formatTotals(inputs.state), `${collected
|
|
11
|
+
return `${(collected === 0 ? [formatTotals(inputs.state)] : [formatTotals(inputs.state), `${Fmt.plural(collected, "module")} all-passed.`]).join("\n\n")}\n${buildFooter(inputs)}`;
|
|
11
12
|
};
|
|
12
13
|
const renderSingleProjectPass = {
|
|
13
14
|
agent: renderAgent,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { buildFooter } from "../footer.js";
|
|
2
|
+
import { statusGlyph } from "../../theme.js";
|
|
2
3
|
import { formatDisplayDuration } from "../../format-duration.js";
|
|
3
4
|
import { formatFailure, formatTestName, soleTest } from "../helpers.js";
|
|
4
5
|
import { renderAgentStringAsInk } from "../ink-helpers.js";
|
|
@@ -9,7 +10,7 @@ const renderAgent = (inputs) => {
|
|
|
9
10
|
const test = soleTest(inputs.state);
|
|
10
11
|
if (test === void 0) return "";
|
|
11
12
|
const duration = test.durationMs ?? 0;
|
|
12
|
-
const header =
|
|
13
|
+
const header = `${statusGlyph("failure")} ${formatTestName(test)} (${formatDisplayDuration(duration)})`;
|
|
13
14
|
const failure = inputs.state.failures[0];
|
|
14
15
|
return `${failure === void 0 ? `${header}\n` : `${header}\n${formatFailure(failure, DEFAULT_WIDTH).join("\n")}\n`}${buildFooter(inputs)}`;
|
|
15
16
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { buildFooter } from "../footer.js";
|
|
2
|
+
import { statusGlyph } from "../../theme.js";
|
|
2
3
|
import { formatDisplayDuration } from "../../format-duration.js";
|
|
3
4
|
import { formatTestName, soleTest } from "../helpers.js";
|
|
4
5
|
import { renderAgentStringAsInk } from "../ink-helpers.js";
|
|
@@ -8,7 +9,7 @@ const renderAgent = (inputs) => {
|
|
|
8
9
|
const test = soleTest(inputs.state);
|
|
9
10
|
if (test === void 0) return "";
|
|
10
11
|
const duration = test.durationMs ?? 0;
|
|
11
|
-
return
|
|
12
|
+
return `${statusGlyph("success")} ${formatTestName(test)} (${formatDisplayDuration(duration)})\n${buildFooter(inputs)}`;
|
|
12
13
|
};
|
|
13
14
|
const renderSingleTestPass = {
|
|
14
15
|
agent: renderAgent,
|
package/dispatcher/helpers.js
CHANGED
|
@@ -1,29 +1,22 @@
|
|
|
1
|
+
import { statusGlyph } from "../theme.js";
|
|
2
|
+
import { formatTotalsLine } from "../counts.js";
|
|
1
3
|
import { formatDisplayDuration } from "../format-duration.js";
|
|
4
|
+
import { Doc, Fmt, Render } from "@effected/cli";
|
|
2
5
|
|
|
3
6
|
//#region src/dispatcher/helpers.ts
|
|
4
7
|
/**
|
|
5
|
-
* Truncate a line to a maximum width with an ellipsis suffix.
|
|
6
|
-
*/
|
|
7
|
-
const truncate = (line, max) => {
|
|
8
|
-
if (line.length <= max) return line;
|
|
9
|
-
const slice = max - 1;
|
|
10
|
-
if (slice <= 0) return "…";
|
|
11
|
-
return `${line.slice(0, slice)}…`;
|
|
12
|
-
};
|
|
13
|
-
/**
|
|
14
8
|
* Format the `Tests:` header line —
|
|
15
9
|
* `<pass>/<total> passed[, <fail> failed][, <timeout> timed out][, <skip> skipped] (Xms)`.
|
|
16
10
|
* `total` folds in `timeoutCount` — a timed-out test is a real collected
|
|
17
11
|
* test, not a pass (issue #224).
|
|
18
12
|
*/
|
|
19
13
|
const formatTotals = (state) => {
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
return `Tests: ${parts.join(", ")} (${formatDisplayDuration(durationMs)})${suffix}`;
|
|
14
|
+
const modules = state.collectedModules;
|
|
15
|
+
return formatTotalsLine({
|
|
16
|
+
label: "Tests",
|
|
17
|
+
...state.totals,
|
|
18
|
+
...modules !== void 0 && modules > 0 ? { suffix: `across ${modules} files` } : {}
|
|
19
|
+
});
|
|
27
20
|
};
|
|
28
21
|
/**
|
|
29
22
|
* Locate the sole {@link TestRecord} in a `single-test` shape state.
|
|
@@ -56,17 +49,25 @@ const formatTestName = (test) => {
|
|
|
56
49
|
* Render one failure block — `- <path > suite > name> [classification]`
|
|
57
50
|
* followed by the indented message and diff. Stack traces are omitted
|
|
58
51
|
* by default to keep the agent string compact.
|
|
52
|
+
*
|
|
53
|
+
* @remarks
|
|
54
|
+
* A kit document rendered plain for the agent audience: one compact list
|
|
55
|
+
* item whose title is verbatim (never wrapped), whose first message line is
|
|
56
|
+
* a truncated `Doc.line`, and whose diff is `Doc.diffText` with `truncate`.
|
|
57
|
+
* Each content line is cut to `width - 2` (floor 20) after the item indent.
|
|
58
|
+
* The kit sanitizes the text, so a tab becomes a space and an escape
|
|
59
|
+
* sequence is removed.
|
|
59
60
|
*/
|
|
60
61
|
const formatFailure = (f, width) => {
|
|
61
62
|
const suite = f.suitePath.length > 0 ? `${f.suitePath.join(" > ")} > ` : "";
|
|
62
63
|
const classification = f.classification !== null ? ` [${f.classification}]` : "";
|
|
63
|
-
const
|
|
64
|
-
if (f.error?.message !== void 0) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
64
|
+
const parts = [Doc.verbatim(`${f.modulePath} > ${suite}${f.testName}${classification}`)];
|
|
65
|
+
if (f.error?.message !== void 0) parts.push(Doc.line(f.error.message.split("\n", 1)[0] ?? "", { truncate: true }));
|
|
66
|
+
if (f.error?.diff !== void 0) parts.push(Doc.diffText(f.error.diff, { truncate: true }));
|
|
67
|
+
return Render.plain([Doc.list([Doc.section(void 0, parts)], { compact: true })], Render.contextOf({
|
|
68
|
+
audience: "agent",
|
|
69
|
+
width: Math.max(22, width)
|
|
70
|
+
})).split("\n");
|
|
70
71
|
};
|
|
71
72
|
/**
|
|
72
73
|
* One coverage judgment line — `Coverage: ✓ all metrics meet thresholds`
|
|
@@ -76,10 +77,10 @@ const formatFailure = (f, width) => {
|
|
|
76
77
|
const formatCoverageJudgmentLine = (state) => {
|
|
77
78
|
const cov = state.coverage;
|
|
78
79
|
if (cov === null) return null;
|
|
79
|
-
if (cov.violations.length === 0) return
|
|
80
|
+
if (cov.violations.length === 0) return `Coverage: ${statusGlyph("success")} all metrics meet thresholds`;
|
|
80
81
|
const metrics = cov.violations.map((v) => v.metric).join(", ");
|
|
81
82
|
const fileCount = countLowCoverageFiles(cov.gaps);
|
|
82
|
-
return `Coverage:
|
|
83
|
+
return `Coverage: ${statusGlyph("failure")} ${Fmt.plural(fileCount, "file")} below minimum thresholds (${metrics})`;
|
|
83
84
|
};
|
|
84
85
|
const countLowCoverageFiles = (gaps) => {
|
|
85
86
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -92,8 +93,7 @@ const countLowCoverageFiles = (gaps) => {
|
|
|
92
93
|
*/
|
|
93
94
|
const formatTrendLine = (trend) => {
|
|
94
95
|
if (trend === null) return null;
|
|
95
|
-
|
|
96
|
-
return `Trend: ${trend.direction} (${runs})`;
|
|
96
|
+
return `Trend: ${trend.direction} (${Fmt.plural(trend.runCount, "run")})`;
|
|
97
97
|
};
|
|
98
98
|
/**
|
|
99
99
|
* Format a compact projects-table line for one project. Pads the name
|
|
@@ -104,7 +104,7 @@ const formatTrendLine = (trend) => {
|
|
|
104
104
|
const formatProjectRow = (project, nameWidth) => {
|
|
105
105
|
const timeoutCount = project.timeoutCount ?? 0;
|
|
106
106
|
const total = project.passCount + project.failCount + project.skipCount + timeoutCount;
|
|
107
|
-
const glyph = project.failCount > 0 || timeoutCount > 0 ? "
|
|
107
|
+
const glyph = statusGlyph(project.failCount > 0 || timeoutCount > 0 ? "failure" : "success");
|
|
108
108
|
const countParts = [`${project.passCount}/${total} passed`];
|
|
109
109
|
if (project.failCount > 0) countParts.push(`${project.failCount} failed`);
|
|
110
110
|
if (timeoutCount > 0) countParts.push(`${timeoutCount} timed out`);
|
|
@@ -132,26 +132,15 @@ const formatProjectsTable = (projects) => {
|
|
|
132
132
|
/**
|
|
133
133
|
* Format the `Total:` footer for a workspace run.
|
|
134
134
|
*/
|
|
135
|
-
const formatWorkspaceTotal = (projects) => {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
skip += p.skipCount;
|
|
145
|
-
timeout += p.timeoutCount ?? 0;
|
|
146
|
-
durationMs += p.durationMs;
|
|
147
|
-
}
|
|
148
|
-
const total = pass + fail + skip + timeout;
|
|
149
|
-
const parts = [`${pass}/${total} passed`];
|
|
150
|
-
if (fail > 0) parts.push(`${fail} failed`);
|
|
151
|
-
if (timeout > 0) parts.push(`${timeout} timed out`);
|
|
152
|
-
if (skip > 0) parts.push(`${skip} skipped`);
|
|
153
|
-
return `Total: ${parts.join(", ")} (${formatDisplayDuration(durationMs)})`;
|
|
154
|
-
};
|
|
135
|
+
const formatWorkspaceTotal = (projects) => formatTotalsLine({
|
|
136
|
+
label: "Total",
|
|
137
|
+
passCount: sumOf(projects, (p) => p.passCount),
|
|
138
|
+
failCount: sumOf(projects, (p) => p.failCount),
|
|
139
|
+
timeoutCount: sumOf(projects, (p) => p.timeoutCount ?? 0),
|
|
140
|
+
skipCount: sumOf(projects, (p) => p.skipCount),
|
|
141
|
+
durationMs: sumOf(projects, (p) => p.durationMs)
|
|
142
|
+
});
|
|
143
|
+
const sumOf = (projects, pick) => projects.reduce((sum, p) => sum + pick(p), 0);
|
|
155
144
|
const TABLE_COL_FILE_MIN = 60;
|
|
156
145
|
/**
|
|
157
146
|
* Format the `Files below aspirational target:` block — a pipe-delimited
|
|
@@ -202,4 +191,4 @@ const pctCell = (n) => {
|
|
|
202
191
|
};
|
|
203
192
|
|
|
204
193
|
//#endregion
|
|
205
|
-
export { formatBelowTargetTable, formatCoverageJudgmentLine, formatDisplayDuration, formatFailure, formatProjectRow, formatProjectsTable, formatTestName, formatTotals, formatTrendLine, formatWorkspaceTotal, soleModulePath, soleTest
|
|
194
|
+
export { formatBelowTargetTable, formatCoverageJudgmentLine, formatDisplayDuration, formatFailure, formatProjectRow, formatProjectsTable, formatTestName, formatTotals, formatTrendLine, formatWorkspaceTotal, soleModulePath, soleTest };
|
|
@@ -1,18 +1,9 @@
|
|
|
1
|
+
import { inkStyle, statusGlyph, statusInkStyle } from "../theme.js";
|
|
1
2
|
import { Box, Text } from "ink";
|
|
2
3
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
4
|
|
|
4
5
|
//#region src/dispatcher/ink-helpers.tsx
|
|
5
6
|
/**
|
|
6
|
-
* Ink rendering helpers shared by the dispatcher cells.
|
|
7
|
-
*
|
|
8
|
-
* Most cells re-express their agent-half string in a simple
|
|
9
|
-
* `<Box flexDirection="column">` with one `<Text>` per line plus
|
|
10
|
-
* targeted color coding on glyph characters (✓ green, ✗ red,
|
|
11
|
-
* Trend regressing/improving in matching colors).
|
|
12
|
-
*
|
|
13
|
-
* @packageDocumentation
|
|
14
|
-
*/
|
|
15
|
-
/**
|
|
16
7
|
* Render an agent-string output as a column of Ink Text rows, applying
|
|
17
8
|
* color to the leading status glyph on each line.
|
|
18
9
|
*/
|
|
@@ -23,54 +14,39 @@ const renderAgentStringAsInk = (agentString) => {
|
|
|
23
14
|
children: lines.map((line, idx) => /* @__PURE__ */ jsx(Text, { children: colorize(line) }, `${idx}-${line}`))
|
|
24
15
|
});
|
|
25
16
|
};
|
|
26
|
-
const
|
|
27
|
-
const
|
|
17
|
+
const SUCCESS_GLYPH = statusGlyph("success");
|
|
18
|
+
const FAILURE_GLYPH = statusGlyph("failure");
|
|
19
|
+
/** Paint the leading status glyph of a line in its vocabulary style. */
|
|
20
|
+
const glyphLine = (line, glyph, status) => {
|
|
21
|
+
const idx = line.indexOf(glyph);
|
|
22
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
23
|
+
line.slice(0, idx),
|
|
24
|
+
/* @__PURE__ */ jsx(Text, {
|
|
25
|
+
...statusInkStyle(status),
|
|
26
|
+
children: glyph
|
|
27
|
+
}),
|
|
28
|
+
line.slice(idx + glyph.length)
|
|
29
|
+
] });
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Whole-line styles keyed on the agent string's line prefixes. A
|
|
33
|
+
* regressing trend is a `warning`; a coverage threshold failure is a
|
|
34
|
+
* `failure` (the kit's drift decisions).
|
|
35
|
+
*/
|
|
36
|
+
const LINE_STYLES = [
|
|
37
|
+
["Trend: regressing", "warning"],
|
|
38
|
+
["Trend: improving", "success"],
|
|
39
|
+
[`Coverage: ${SUCCESS_GLYPH}`, "success"],
|
|
40
|
+
[`Coverage: ${FAILURE_GLYPH}`, "failure"],
|
|
41
|
+
["Failures:", "emphasis"],
|
|
42
|
+
["Use `", "muted"]
|
|
43
|
+
];
|
|
28
44
|
const colorize = (line) => {
|
|
29
45
|
const trimmed = line.trimStart();
|
|
30
|
-
if (trimmed.startsWith(
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
/* @__PURE__ */ jsx(Text, {
|
|
35
|
-
color: "green",
|
|
36
|
-
children: PASS_GLYPH
|
|
37
|
-
}),
|
|
38
|
-
line.slice(idx + 1)
|
|
39
|
-
] });
|
|
40
|
-
}
|
|
41
|
-
if (trimmed.startsWith(FAIL_GLYPH)) {
|
|
42
|
-
const idx = line.indexOf(FAIL_GLYPH);
|
|
43
|
-
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
44
|
-
line.slice(0, idx),
|
|
45
|
-
/* @__PURE__ */ jsx(Text, {
|
|
46
|
-
color: "red",
|
|
47
|
-
children: FAIL_GLYPH
|
|
48
|
-
}),
|
|
49
|
-
line.slice(idx + 1)
|
|
50
|
-
] });
|
|
51
|
-
}
|
|
52
|
-
if (line.startsWith("Trend: regressing")) return /* @__PURE__ */ jsx(Text, {
|
|
53
|
-
color: "yellow",
|
|
54
|
-
children: line
|
|
55
|
-
});
|
|
56
|
-
if (line.startsWith("Trend: improving")) return /* @__PURE__ */ jsx(Text, {
|
|
57
|
-
color: "green",
|
|
58
|
-
children: line
|
|
59
|
-
});
|
|
60
|
-
if (line.startsWith("Coverage: ✓")) return /* @__PURE__ */ jsx(Text, {
|
|
61
|
-
color: "green",
|
|
62
|
-
children: line
|
|
63
|
-
});
|
|
64
|
-
if (line.startsWith("Coverage: ✗")) return /* @__PURE__ */ jsx(Text, {
|
|
65
|
-
color: "red",
|
|
66
|
-
children: line
|
|
67
|
-
});
|
|
68
|
-
if (line.startsWith("Failures:")) return /* @__PURE__ */ jsx(Text, {
|
|
69
|
-
bold: true,
|
|
70
|
-
children: line
|
|
71
|
-
});
|
|
72
|
-
if (line.startsWith("Use `")) return /* @__PURE__ */ jsx(Text, {
|
|
73
|
-
dimColor: true,
|
|
46
|
+
if (trimmed.startsWith(SUCCESS_GLYPH)) return glyphLine(line, SUCCESS_GLYPH, "success");
|
|
47
|
+
if (trimmed.startsWith(FAILURE_GLYPH)) return glyphLine(line, FAILURE_GLYPH, "failure");
|
|
48
|
+
for (const [prefix, token] of LINE_STYLES) if (line.startsWith(prefix)) return /* @__PURE__ */ jsx(Text, {
|
|
49
|
+
...inkStyle(token),
|
|
74
50
|
children: line
|
|
75
51
|
});
|
|
76
52
|
return line;
|
package/format-duration.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { Fmt } from "@effected/cli";
|
|
2
|
+
|
|
1
3
|
//#region src/format-duration.ts
|
|
2
4
|
/**
|
|
3
5
|
* Shared display formatter for run / module / test durations.
|
|
@@ -6,29 +8,27 @@
|
|
|
6
8
|
* (`14.87745800000016`). Rendering those verbatim is noise. This is
|
|
7
9
|
* the single formatter every render path calls — `render-agent.ts`,
|
|
8
10
|
* the `render-ink/` components, the dispatcher cells, and `StreamApp`
|
|
9
|
-
* — so a duration looks the same wherever it appears.
|
|
11
|
+
* — so a duration looks the same wherever it appears. It delegates to
|
|
12
|
+
* `@effected/cli`'s `Fmt.duration`, so it also matches every other kit
|
|
13
|
+
* CLI's output.
|
|
10
14
|
*
|
|
11
15
|
* Display only. Full-precision durations continue to persist to the
|
|
12
16
|
* database unchanged; nothing in the trend / baseline / classification
|
|
13
17
|
* pipeline reads a duration through a formatter.
|
|
14
18
|
*/
|
|
15
|
-
const SECOND_MS = 1e3;
|
|
16
19
|
/**
|
|
17
20
|
* Format a duration in milliseconds for display.
|
|
18
21
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
22
|
+
* Whole milliseconds under a second (`250ms`), seconds to one decimal
|
|
23
|
+
* under a minute with a trailing `.0` dropped (`1.2s`, `2s`), then
|
|
24
|
+
* `1m 3s` and `1h 2m`. A value that rounds up to the next unit is
|
|
25
|
+
* written in that unit (`999.6` → `1s`).
|
|
23
26
|
*
|
|
24
27
|
* @param ms - duration in milliseconds
|
|
25
28
|
* @returns formatted duration string
|
|
26
29
|
* @public
|
|
27
30
|
*/
|
|
28
|
-
const formatDisplayDuration = (ms) =>
|
|
29
|
-
if (ms < SECOND_MS) return `${Math.round(ms * 10) / 10}ms`;
|
|
30
|
-
return `${Math.round(ms / SECOND_MS * 10) / 10}s`;
|
|
31
|
-
};
|
|
31
|
+
const formatDisplayDuration = (ms) => Fmt.duration(ms);
|
|
32
32
|
|
|
33
33
|
//#endregion
|
|
34
34
|
export { formatDisplayDuration };
|
package/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Context, Effect, Layer, PubSub, Schema, Scope, Stream } from "effect";
|
|
2
2
|
import { FC, ReactElement } from "react";
|
|
3
|
+
import { CoreStatusName, GlyphSet, Status, Style, TokenName } from "@effected/cli";
|
|
3
4
|
//#region ../sdk/dist/dev/pkg/index.d.ts
|
|
4
5
|
//#region src/schemas/Coverage.d.ts
|
|
5
6
|
/**
|
|
@@ -1170,7 +1171,9 @@ export declare const buildFooter: (inputs: DispatchInputs) => string;
|
|
|
1170
1171
|
* (`14.87745800000016`). Rendering those verbatim is noise. This is
|
|
1171
1172
|
* the single formatter every render path calls — `render-agent.ts`,
|
|
1172
1173
|
* the `render-ink/` components, the dispatcher cells, and `StreamApp`
|
|
1173
|
-
* — so a duration looks the same wherever it appears.
|
|
1174
|
+
* — so a duration looks the same wherever it appears. It delegates to
|
|
1175
|
+
* `@effected/cli`'s `Fmt.duration`, so it also matches every other kit
|
|
1176
|
+
* CLI's output.
|
|
1174
1177
|
*
|
|
1175
1178
|
* Display only. Full-precision durations continue to persist to the
|
|
1176
1179
|
* database unchanged; nothing in the trend / baseline / classification
|
|
@@ -1179,10 +1182,10 @@ export declare const buildFooter: (inputs: DispatchInputs) => string;
|
|
|
1179
1182
|
/**
|
|
1180
1183
|
* Format a duration in milliseconds for display.
|
|
1181
1184
|
*
|
|
1182
|
-
*
|
|
1183
|
-
*
|
|
1184
|
-
*
|
|
1185
|
-
*
|
|
1185
|
+
* Whole milliseconds under a second (`250ms`), seconds to one decimal
|
|
1186
|
+
* under a minute with a trailing `.0` dropped (`1.2s`, `2s`), then
|
|
1187
|
+
* `1m 3s` and `1h 2m`. A value that rounds up to the next unit is
|
|
1188
|
+
* written in that unit (`999.6` → `1s`).
|
|
1186
1189
|
*
|
|
1187
1190
|
* @param ms - duration in milliseconds
|
|
1188
1191
|
* @returns formatted duration string
|
|
@@ -1588,7 +1591,7 @@ interface CountColumnsProps {
|
|
|
1588
1591
|
/**
|
|
1589
1592
|
* Fixed width of the duration cell that follows the count columns on an
|
|
1590
1593
|
* aggregate row — sized for the formatter's widest common output
|
|
1591
|
-
* (`
|
|
1594
|
+
* (`59m 59s`). Longer values overflow their row rather than truncate.
|
|
1592
1595
|
*
|
|
1593
1596
|
* @public
|
|
1594
1597
|
*/
|
|
@@ -1597,7 +1600,8 @@ export declare const DURATION_CELL_WIDTH = 7;
|
|
|
1597
1600
|
* Renders four colored count columns (pass ✓, fail ✗, skip ↷, timeout ⧖)
|
|
1598
1601
|
* for an aggregate row. Counts render right-aligned in fixed 4-digit cells so
|
|
1599
1602
|
* columns align across rows; counts of 10,000+ overflow their row without
|
|
1600
|
-
* truncation. Zeros are
|
|
1603
|
+
* truncation. Zeros are greyed; all four columns always appear. Glyphs and
|
|
1604
|
+
* colours come from `VitestAgentStatus` (skip is `muted`).
|
|
1601
1605
|
*
|
|
1602
1606
|
* @public
|
|
1603
1607
|
*/
|
|
@@ -1759,7 +1763,7 @@ export declare const StatusIcon: FC<StatusIconProps>;
|
|
|
1759
1763
|
interface StreamAppProps {
|
|
1760
1764
|
/** The current reduced render state to display. */
|
|
1761
1765
|
readonly state: RenderState;
|
|
1762
|
-
/** Current spinner frame index
|
|
1766
|
+
/** Current spinner frame index (the live view's `frame`: wall-clock ticks of `SPINNER_FRAME_MS`). */
|
|
1763
1767
|
readonly frameIndex: number;
|
|
1764
1768
|
/** Current wall-clock time in milliseconds; defaults to `Date.now()`. */
|
|
1765
1769
|
readonly nowMs?: number;
|
|
@@ -1792,49 +1796,29 @@ export declare const SuggestedActions: FC<SuggestedActionsProps>;
|
|
|
1792
1796
|
//#endregion
|
|
1793
1797
|
//#region src/render-ink/spinner.d.ts
|
|
1794
1798
|
/**
|
|
1795
|
-
*
|
|
1796
|
-
*
|
|
1797
|
-
* No `ink-spinner` dependency — that package is a thin wrapper over the
|
|
1798
|
-
* same ten Braille characters plus a timer. The `stream` renderer
|
|
1799
|
-
* already needs a frame clock for the ticking elapsed column, so the
|
|
1800
|
-
* timer is shared and only the frame array lives here.
|
|
1801
|
-
*
|
|
1802
|
-
* The frame index is presentation state: it is derived from wall-clock
|
|
1803
|
-
* time by `createLiveInk` and passed to `StreamApp` as a prop. It never
|
|
1804
|
-
* enters the event-sourced `RenderState`.
|
|
1805
|
-
*/
|
|
1806
|
-
/**
|
|
1807
|
-
* The ten Braille spinner frames, in animation order.
|
|
1799
|
+
* The Unicode spinner frames, in animation order (`Glyphs.unicode.spinner`).
|
|
1808
1800
|
*
|
|
1809
1801
|
* @public
|
|
1810
1802
|
*/
|
|
1811
|
-
export declare const SPINNER_FRAMES:
|
|
1803
|
+
export declare const SPINNER_FRAMES: ReadonlyArray<string>;
|
|
1812
1804
|
/**
|
|
1813
|
-
* How long each spinner frame is held, in milliseconds
|
|
1805
|
+
* How long each spinner frame is held, in milliseconds
|
|
1806
|
+
* (`Glyphs.unicode.spinnerIntervalMs`).
|
|
1814
1807
|
*
|
|
1815
1808
|
* @public
|
|
1816
1809
|
*/
|
|
1817
|
-
export declare const SPINNER_FRAME_MS
|
|
1810
|
+
export declare const SPINNER_FRAME_MS: number;
|
|
1818
1811
|
/**
|
|
1819
1812
|
* Resolve the spinner glyph for a frame index. The index wraps modulo
|
|
1820
1813
|
* the frame count and tolerates negative values, so a wall-clock-derived
|
|
1821
1814
|
* index is always valid.
|
|
1822
1815
|
*
|
|
1823
1816
|
* @param index - the frame index (wraps modulo frame count)
|
|
1824
|
-
* @
|
|
1817
|
+
* @param glyphs - the glyph set whose frames to draw; Unicode by default
|
|
1818
|
+
* @returns the spinner glyph for the given frame
|
|
1825
1819
|
* @public
|
|
1826
1820
|
*/
|
|
1827
|
-
export declare const spinnerFrame: (index: number) => string;
|
|
1828
|
-
/**
|
|
1829
|
-
* Derive the spinner frame index from a wall-clock timestamp. Using the
|
|
1830
|
-
* clock — rather than a monotonic counter — keeps the animation correct
|
|
1831
|
-
* across watch-mode remounts with no extra closure state to reset.
|
|
1832
|
-
*
|
|
1833
|
-
* @param nowMs - current wall-clock time in milliseconds
|
|
1834
|
-
* @returns the frame index for the given timestamp
|
|
1835
|
-
* @public
|
|
1836
|
-
*/
|
|
1837
|
-
export declare const spinnerFrameForTime: (nowMs: number) => number;
|
|
1821
|
+
export declare const spinnerFrame: (index: number, glyphs?: GlyphSet) => string;
|
|
1838
1822
|
//#endregion
|
|
1839
1823
|
//#region src/render-ink/TagColumns.d.ts
|
|
1840
1824
|
/**
|
|
@@ -2003,6 +1987,82 @@ interface SynthesizeFromAgentReportOptions {
|
|
|
2003
1987
|
*/
|
|
2004
1988
|
export declare const synthesizeFromAgentReport: (report: AgentReport, options?: SynthesizeFromAgentReportOptions) => RunEvent[];
|
|
2005
1989
|
//#endregion
|
|
1990
|
+
//#region src/theme.d.ts
|
|
1991
|
+
/**
|
|
1992
|
+
* The status vocabulary every vitest-agent render path draws from.
|
|
1993
|
+
*
|
|
1994
|
+
* @remarks
|
|
1995
|
+
* Ranks slot the run statuses into the core ladder: `queued` (25) and
|
|
1996
|
+
* `running` (35) sit among the not-yet-decided statuses, and `timeout` (85)
|
|
1997
|
+
* is just under `failure` (90), so `Status.worst` over a project's
|
|
1998
|
+
* outcomes reads failure, then timeout, then warning.
|
|
1999
|
+
*
|
|
2000
|
+
* @public
|
|
2001
|
+
*/
|
|
2002
|
+
export declare const VitestAgentStatus: Status<VitestAgentStatusName>;
|
|
2003
|
+
/**
|
|
2004
|
+
* A status name in {@link VitestAgentStatus}.
|
|
2005
|
+
*
|
|
2006
|
+
* @public
|
|
2007
|
+
*/
|
|
2008
|
+
type VitestAgentStatusName = CoreStatusName | "timeout" | "running" | "queued";
|
|
2009
|
+
/**
|
|
2010
|
+
* Named accents that are not statuses.
|
|
2011
|
+
*
|
|
2012
|
+
* @public
|
|
2013
|
+
*/
|
|
2014
|
+
export declare const VitestAgentTokens: {
|
|
2015
|
+
/** The `[flaky]` / `[new-failure]` classification tag on a failure line. */
|
|
2016
|
+
readonly classification: Style;
|
|
2017
|
+
/** A zero in a count column: present for alignment, not signal. */
|
|
2018
|
+
readonly zero: Style;
|
|
2019
|
+
/** The `stable` trend direction. */
|
|
2020
|
+
readonly stable: Style;
|
|
2021
|
+
/** A non-zero tag count. */
|
|
2022
|
+
readonly tag: Style;
|
|
2023
|
+
};
|
|
2024
|
+
/**
|
|
2025
|
+
* The subset of Ink's `<Text>` props a `Style` maps onto.
|
|
2026
|
+
*
|
|
2027
|
+
* @public
|
|
2028
|
+
*/
|
|
2029
|
+
interface InkTextStyle {
|
|
2030
|
+
readonly color?: string;
|
|
2031
|
+
readonly bold?: boolean;
|
|
2032
|
+
readonly dimColor?: boolean;
|
|
2033
|
+
readonly italic?: boolean;
|
|
2034
|
+
readonly underline?: boolean;
|
|
2035
|
+
}
|
|
2036
|
+
/**
|
|
2037
|
+
* Ink `<Text>` props for a semantic token or an explicit style.
|
|
2038
|
+
*
|
|
2039
|
+
* @remarks
|
|
2040
|
+
* Resolves the token through the kit's pure `Token.resolve` and maps the
|
|
2041
|
+
* `Style` with the kit's `inkProps`. No colour level reaches this call
|
|
2042
|
+
* site, so `inkProps` with no level emits every prop and Ink's own chalk
|
|
2043
|
+
* level, set by `CliUi.live` or `CliUi.context`, gates what is drawn.
|
|
2044
|
+
*
|
|
2045
|
+
* @param token - a token name or a `Style`
|
|
2046
|
+
* @public
|
|
2047
|
+
*/
|
|
2048
|
+
export declare const inkStyle: (token: TokenName | Style) => InkTextStyle;
|
|
2049
|
+
/**
|
|
2050
|
+
* The glyph of a status in a glyph set: its Unicode glyph, or its ASCII
|
|
2051
|
+
* fallback when `glyphs` is the kit's ASCII set (the kit's `Status.glyph`).
|
|
2052
|
+
*
|
|
2053
|
+
* @param name - a status in {@link VitestAgentStatus}
|
|
2054
|
+
* @param glyphs - the glyph set, from `Glyphs.select`; Unicode by default
|
|
2055
|
+
* @public
|
|
2056
|
+
*/
|
|
2057
|
+
export declare const statusGlyph: (name: VitestAgentStatusName, glyphs?: GlyphSet) => string;
|
|
2058
|
+
/**
|
|
2059
|
+
* Ink `<Text>` props for a status's glyph.
|
|
2060
|
+
*
|
|
2061
|
+
* @param name - a status in {@link VitestAgentStatus}
|
|
2062
|
+
* @public
|
|
2063
|
+
*/
|
|
2064
|
+
export declare const statusInkStyle: (name: VitestAgentStatusName) => InkTextStyle;
|
|
2065
|
+
//#endregion
|
|
2006
2066
|
//#region src/index.d.ts
|
|
2007
2067
|
/**
|
|
2008
2068
|
* The version of this package, inlined at build time from
|
|
@@ -2013,5 +2073,5 @@ export declare const synthesizeFromAgentReport: (report: AgentReport, options?:
|
|
|
2013
2073
|
*/
|
|
2014
2074
|
export declare const CURRENT_UI_VERSION: string;
|
|
2015
2075
|
//#endregion
|
|
2016
|
-
export { type AgentCellFn, type AgentReport, type Cell, type CellOptions, type CountColumnsProps, type CoverageBlockProps, FileCoverageReport as CoverageFile, type CoverageTotals, type DispatchInputs, type FailureSectionProps, type FailuresSectionProps, type InkCellFn, type MetricThresholds, type ModuleHeaderProps, type ProjectRowProps, type ProjectSummary, type RenderAgentOptions, type RunEventByTag, type RunOutcome, type RunShape, type StatusIconKind, type StatusIconProps, type StreamAppProps, type SuggestedActionsProps, type SynthesizeFromAgentReportOptions, type SynthesizeOptions, type SynthesizedCoverage, type TagColumnsProps, type TestClassification, type TestRowProps, type TrendLineProps, type TrendSummary, type VitestModuleDiagnostic, type VitestModuleError, type VitestParsedStack, type VitestTestCase, type VitestTestDiagnostic, type VitestTestError, type VitestTestModule, type VitestTestResult, type VitestTestSuite };
|
|
2076
|
+
export { type AgentCellFn, type AgentReport, type Cell, type CellOptions, type CountColumnsProps, type CoverageBlockProps, FileCoverageReport as CoverageFile, type CoverageTotals, type DispatchInputs, type FailureSectionProps, type FailuresSectionProps, type InkCellFn, type InkTextStyle, type MetricThresholds, type ModuleHeaderProps, type ProjectRowProps, type ProjectSummary, type RenderAgentOptions, type RunEventByTag, type RunOutcome, type RunShape, type StatusIconKind, type StatusIconProps, type StreamAppProps, type SuggestedActionsProps, type SynthesizeFromAgentReportOptions, type SynthesizeOptions, type SynthesizedCoverage, type TagColumnsProps, type TestClassification, type TestRowProps, type TrendLineProps, type TrendSummary, type VitestAgentStatusName, type VitestModuleDiagnostic, type VitestModuleError, type VitestParsedStack, type VitestTestCase, type VitestTestDiagnostic, type VitestTestError, type VitestTestModule, type VitestTestResult, type VitestTestSuite };
|
|
2017
2077
|
//# sourceMappingURL=index.d.ts.map
|