@vitest-agent/ui 2.5.3 → 4.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/README.md +1 -2
- 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 +98 -343
- package/index.js +4 -7
- package/package.json +7 -3
- package/render-agent.js +25 -37
- 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/pubsub/Channel.js +0 -30
- package/pubsub/Publisher.js +0 -37
- package/pubsub/Subscriber.js +0 -77
- package/pubsub/index.js +0 -5
package/render-agent.js
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { formatTotalsLine } from "./counts.js";
|
|
2
|
+
import { formatFailure as formatFailure$1 } from "./dispatcher/helpers.js";
|
|
3
|
+
import { Fmt } from "@effected/cli";
|
|
2
4
|
|
|
3
5
|
//#region src/render-agent.ts
|
|
6
|
+
/**
|
|
7
|
+
* Agent-mode renderer: produces a token-efficient final-frame string
|
|
8
|
+
* from a fully-reduced `RenderState`.
|
|
9
|
+
*
|
|
10
|
+
* Output is intentionally separate from the Ink renderer — agents
|
|
11
|
+
* benefit from a different layout than humans (no ANSI, no live
|
|
12
|
+
* progress, no decoration overhead). The output is stable for stable
|
|
13
|
+
* inputs: no timestamps, no derived "time ago" phrasing, no randomized
|
|
14
|
+
* ordering.
|
|
15
|
+
*/
|
|
4
16
|
const DEFAULT_WIDTH = 80;
|
|
5
17
|
const DEFAULT_MAX_GAPS = 3;
|
|
6
18
|
const severityLabel = {
|
|
@@ -8,18 +20,11 @@ const severityLabel = {
|
|
|
8
20
|
warn: "warn",
|
|
9
21
|
blocker: "blocker"
|
|
10
22
|
};
|
|
11
|
-
const formatHeader = (state) => {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
}
|
|
21
|
-
return `Tests: ${parts.join(", ")} (${formatDisplayDuration(durationMs)})`;
|
|
22
|
-
};
|
|
23
|
+
const formatHeader = (state) => formatTotalsLine({
|
|
24
|
+
label: "Tests",
|
|
25
|
+
...state.totals,
|
|
26
|
+
unhandledErrors: state.unhandledErrors.length
|
|
27
|
+
});
|
|
23
28
|
const formatModulesSection = (state) => {
|
|
24
29
|
const modules = state.moduleOrder.map((path) => state.modules[path]).filter((m) => m !== void 0);
|
|
25
30
|
const { passCount, failCount, skipCount, timeoutCount } = state.totals;
|
|
@@ -27,9 +32,9 @@ const formatModulesSection = (state) => {
|
|
|
27
32
|
const collected = state.collectedModules ?? modules.length;
|
|
28
33
|
if (modules.length === 0) {
|
|
29
34
|
if (collected === 0) return null;
|
|
30
|
-
return `${collected
|
|
35
|
+
return `${Fmt.plural(collected, "module")} all-passed.`;
|
|
31
36
|
}
|
|
32
|
-
if (modules.filter((m) => m.failCount > 0 || m.skipCount > 0).length === 0) return `${collected
|
|
37
|
+
if (modules.filter((m) => m.failCount > 0 || m.skipCount > 0).length === 0) return `${Fmt.plural(collected, "module")} all-passed.`;
|
|
33
38
|
const lines = ["Modules:"];
|
|
34
39
|
for (const m of modules) {
|
|
35
40
|
const parts = [];
|
|
@@ -41,21 +46,9 @@ const formatModulesSection = (state) => {
|
|
|
41
46
|
}
|
|
42
47
|
return lines.join("\n");
|
|
43
48
|
};
|
|
44
|
-
const truncate = (line, max) =>
|
|
45
|
-
if (line.length <= max) return line;
|
|
46
|
-
const slice = max - 1;
|
|
47
|
-
if (slice <= 0) return "…";
|
|
48
|
-
return `${line.slice(0, slice)}…`;
|
|
49
|
-
};
|
|
49
|
+
const truncate = (line, max) => Fmt.truncate(line, max);
|
|
50
50
|
const formatFailure = (f, width, includeStack) => {
|
|
51
|
-
const
|
|
52
|
-
const classification = f.classification !== null ? ` [${f.classification}]` : "";
|
|
53
|
-
const lines = [`- ${f.modulePath} > ${suite}${f.testName}${classification}`];
|
|
54
|
-
if (f.error?.message !== void 0) {
|
|
55
|
-
const firstLine = f.error.message.split("\n", 1)[0] ?? "";
|
|
56
|
-
lines.push(` ${truncate(firstLine, Math.max(20, width - 2))}`);
|
|
57
|
-
}
|
|
58
|
-
if (f.error?.diff !== void 0) for (const diffLine of f.error.diff.split("\n")) lines.push(` ${truncate(diffLine, Math.max(20, width - 2))}`);
|
|
51
|
+
const lines = [...formatFailure$1(f, width)];
|
|
59
52
|
if (includeStack && f.error?.stack !== void 0) for (const stackLine of f.error.stack.split("\n")) lines.push(` ${truncate(stackLine, Math.max(20, width - 2))}`);
|
|
60
53
|
return lines.join("\n");
|
|
61
54
|
};
|
|
@@ -77,10 +70,8 @@ const formatUnhandledErrorsSection = (state, width, includeStack) => {
|
|
|
77
70
|
for (const err of state.unhandledErrors) lines.push(formatUnhandledError(err, width, includeStack));
|
|
78
71
|
return lines.join("\n");
|
|
79
72
|
};
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
return Number.isInteger(rounded) ? `${rounded}%` : `${rounded}%`;
|
|
83
|
-
};
|
|
73
|
+
/** Coverage metrics arrive on istanbul's 0–100 scale. */
|
|
74
|
+
const formatPercent = (n) => Fmt.percent(n, { scale: 100 });
|
|
84
75
|
const formatCoverageSection = (state, maxGaps) => {
|
|
85
76
|
const cov = state.coverage;
|
|
86
77
|
if (cov === null) return null;
|
|
@@ -108,10 +99,7 @@ const formatCoverageSection = (state, maxGaps) => {
|
|
|
108
99
|
lines.push(`- ${g.file}${detail}`);
|
|
109
100
|
}
|
|
110
101
|
const omitted = cov.gaps.length - topN.length;
|
|
111
|
-
if (omitted > 0) {
|
|
112
|
-
const noun = omitted === 1 ? "gap" : "gaps";
|
|
113
|
-
lines.push(`- (+${omitted} more ${noun})`);
|
|
114
|
-
}
|
|
102
|
+
if (omitted > 0) lines.push(`- (+${Fmt.plural(omitted, "more gap", "more gaps")})`);
|
|
115
103
|
}
|
|
116
104
|
return lines.join("\n");
|
|
117
105
|
};
|
|
@@ -1,41 +1,42 @@
|
|
|
1
|
+
import { VitestAgentTokens, inkStyle, statusGlyph, statusInkStyle } from "../theme.js";
|
|
1
2
|
import { Text } from "ink";
|
|
3
|
+
import { useGlyphs } from "@effected/cli/ui";
|
|
2
4
|
import { jsxs } from "react/jsx-runtime";
|
|
3
5
|
|
|
4
6
|
//#region src/render-ink/CountColumns.tsx
|
|
5
7
|
/**
|
|
6
|
-
* The four-outcome count columns for an aggregate row (a project or a
|
|
7
|
-
* module): pass, fail, skip, timeout. All four always render — a zero
|
|
8
|
-
* is dimmed, never omitted — so the columns line up across rows.
|
|
9
|
-
*/
|
|
10
|
-
/**
|
|
11
8
|
* Fixed width of the duration cell that follows the count columns on an
|
|
12
9
|
* aggregate row — sized for the formatter's widest common output
|
|
13
|
-
* (`
|
|
10
|
+
* (`59m 59s`). Longer values overflow their row rather than truncate.
|
|
14
11
|
*
|
|
15
12
|
* @public
|
|
16
13
|
*/
|
|
17
14
|
const DURATION_CELL_WIDTH = 7;
|
|
18
|
-
const cell = (count,
|
|
19
|
-
|
|
20
|
-
children: [String(count).padStart(4),
|
|
15
|
+
const cell = (count, status, glyphs) => /* @__PURE__ */ jsxs(Text, {
|
|
16
|
+
...count > 0 ? statusInkStyle(status) : inkStyle(VitestAgentTokens.zero),
|
|
17
|
+
children: [String(count).padStart(4), statusGlyph(status, glyphs)]
|
|
21
18
|
});
|
|
22
19
|
/**
|
|
23
20
|
* Renders four colored count columns (pass ✓, fail ✗, skip ↷, timeout ⧖)
|
|
24
21
|
* for an aggregate row. Counts render right-aligned in fixed 4-digit cells so
|
|
25
22
|
* columns align across rows; counts of 10,000+ overflow their row without
|
|
26
|
-
* truncation. Zeros are
|
|
23
|
+
* truncation. Zeros are greyed; all four columns always appear. Glyphs and
|
|
24
|
+
* colours come from `VitestAgentStatus` (skip is `muted`).
|
|
27
25
|
*
|
|
28
26
|
* @public
|
|
29
27
|
*/
|
|
30
|
-
const CountColumns = ({ passCount, failCount, skipCount, timeoutCount }) =>
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
28
|
+
const CountColumns = ({ passCount, failCount, skipCount, timeoutCount }) => {
|
|
29
|
+
const glyphs = useGlyphs();
|
|
30
|
+
return /* @__PURE__ */ jsxs(Text, { children: [
|
|
31
|
+
cell(passCount, "success", glyphs),
|
|
32
|
+
" ",
|
|
33
|
+
cell(failCount, "failure", glyphs),
|
|
34
|
+
" ",
|
|
35
|
+
cell(skipCount, "skip", glyphs),
|
|
36
|
+
" ",
|
|
37
|
+
cell(timeoutCount, "timeout", glyphs)
|
|
38
|
+
] });
|
|
39
|
+
};
|
|
39
40
|
|
|
40
41
|
//#endregion
|
|
41
42
|
export { CountColumns, DURATION_CELL_WIDTH };
|
|
@@ -1,16 +1,22 @@
|
|
|
1
|
+
import { inkStyle } from "../theme.js";
|
|
1
2
|
import { Box, Text } from "ink";
|
|
3
|
+
import { Fmt } from "@effected/cli";
|
|
2
4
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
5
|
|
|
4
6
|
//#region src/render-ink/CoverageBlock.tsx
|
|
7
|
+
/**
|
|
8
|
+
* Coverage section: per-metric percentage, threshold violations, top-N gaps.
|
|
9
|
+
*/
|
|
5
10
|
const METRIC_ORDER = [
|
|
6
11
|
"lines",
|
|
7
12
|
"branches",
|
|
8
13
|
"functions",
|
|
9
14
|
"statements"
|
|
10
15
|
];
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
16
|
+
/** Coverage metrics arrive on istanbul's 0–100 scale. */
|
|
17
|
+
const formatPercent = (n) => Fmt.percent(n, { scale: 100 });
|
|
18
|
+
/** A metric below its threshold is a coverage threshold failure: the kit's `failure` token. */
|
|
19
|
+
const FAILURE = inkStyle("failure");
|
|
14
20
|
/**
|
|
15
21
|
* Renders the coverage section: per-metric percentages, threshold
|
|
16
22
|
* violations, and top-N gap entries sorted by missing line count.
|
|
@@ -35,7 +41,7 @@ const CoverageBlock = ({ coverage, maxGaps = 3 }) => {
|
|
|
35
41
|
return /* @__PURE__ */ jsxs(Box, { children: [
|
|
36
42
|
/* @__PURE__ */ jsx(Text, { children: ` ${metric}: ` }),
|
|
37
43
|
failing ? /* @__PURE__ */ jsx(Text, {
|
|
38
|
-
|
|
44
|
+
...FAILURE,
|
|
39
45
|
children: formatPercent(actual)
|
|
40
46
|
}) : /* @__PURE__ */ jsx(Text, { children: formatPercent(actual) }),
|
|
41
47
|
threshold !== void 0 ? /* @__PURE__ */ jsxs(Text, {
|
|
@@ -51,10 +57,10 @@ const CoverageBlock = ({ coverage, maxGaps = 3 }) => {
|
|
|
51
57
|
coverage.violations.length > 0 ? /* @__PURE__ */ jsxs(Box, {
|
|
52
58
|
flexDirection: "column",
|
|
53
59
|
children: [/* @__PURE__ */ jsx(Text, {
|
|
54
|
-
|
|
60
|
+
...FAILURE,
|
|
55
61
|
children: "Violations"
|
|
56
62
|
}), coverage.violations.map((v) => /* @__PURE__ */ jsx(Text, {
|
|
57
|
-
|
|
63
|
+
...FAILURE,
|
|
58
64
|
children: ` ${v.metric}: ${formatPercent(v.actual)} < ${formatPercent(v.expected)}`
|
|
59
65
|
}, v.metric))]
|
|
60
66
|
}) : null,
|
|
@@ -68,7 +74,7 @@ const CoverageBlock = ({ coverage, maxGaps = 3 }) => {
|
|
|
68
74
|
topGaps.map((g) => /* @__PURE__ */ jsxs(Text, { children: [` ${g.file}`, g.uncoveredLines !== void 0 ? `: ${g.uncoveredLines}` : ""] }, g.file)),
|
|
69
75
|
elidedGaps > 0 ? /* @__PURE__ */ jsx(Text, {
|
|
70
76
|
dimColor: true,
|
|
71
|
-
children: ` (+${elidedGaps
|
|
77
|
+
children: ` (+${Fmt.plural(elidedGaps, "more gap", "more gaps")})`
|
|
72
78
|
}) : null
|
|
73
79
|
]
|
|
74
80
|
}) : null
|
|
@@ -1,17 +1,24 @@
|
|
|
1
|
+
import { inkStyle, statusGlyph, statusInkStyle } from "../theme.js";
|
|
1
2
|
import { Box, Text } from "ink";
|
|
3
|
+
import { useGlyphs } from "@effected/cli/ui";
|
|
2
4
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
5
|
|
|
4
6
|
//#region src/render-ink/FailureSection.tsx
|
|
7
|
+
/**
|
|
8
|
+
* Failure list: one block per failed test with classification, message,
|
|
9
|
+
* diff, and optionally stack trace.
|
|
10
|
+
*/
|
|
5
11
|
const FailureRow = ({ failure, includeStack }) => {
|
|
6
12
|
const suite = failure.suitePath.length > 0 ? `${failure.suitePath.join(" > ")} > ` : "";
|
|
7
13
|
const classification = failure.classification !== null ? ` [${failure.classification}]` : "";
|
|
14
|
+
const glyphs = useGlyphs();
|
|
8
15
|
return /* @__PURE__ */ jsxs(Box, {
|
|
9
16
|
flexDirection: "column",
|
|
10
17
|
children: [
|
|
11
18
|
/* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsxs(Text, { children: [
|
|
12
19
|
/* @__PURE__ */ jsx(Text, {
|
|
13
|
-
|
|
14
|
-
children: "
|
|
20
|
+
...statusInkStyle("failure"),
|
|
21
|
+
children: statusGlyph("failure", glyphs)
|
|
15
22
|
}),
|
|
16
23
|
` ${failure.modulePath} > ${suite}${failure.testName}`,
|
|
17
24
|
failure.classification !== null ? /* @__PURE__ */ jsx(Text, {
|
|
@@ -20,17 +27,17 @@ const FailureRow = ({ failure, includeStack }) => {
|
|
|
20
27
|
}) : null
|
|
21
28
|
] }) }),
|
|
22
29
|
failure.error?.message !== void 0 ? /* @__PURE__ */ jsx(Text, {
|
|
23
|
-
|
|
30
|
+
...inkStyle("failure"),
|
|
24
31
|
children: ` ${failure.error.message.split("\n", 1)[0] ?? ""}`
|
|
25
32
|
}) : null,
|
|
26
33
|
failure.error?.diff !== void 0 ? failure.error.diff.split("\n").map((line, idx) => {
|
|
27
34
|
const key = `diff-${idx}`;
|
|
28
35
|
if (line.startsWith("-")) return /* @__PURE__ */ jsx(Text, {
|
|
29
|
-
|
|
36
|
+
...inkStyle("failure"),
|
|
30
37
|
children: ` ${line}`
|
|
31
38
|
}, key);
|
|
32
39
|
if (line.startsWith("+")) return /* @__PURE__ */ jsx(Text, {
|
|
33
|
-
|
|
40
|
+
...inkStyle("success"),
|
|
34
41
|
children: ` ${line}`
|
|
35
42
|
}, key);
|
|
36
43
|
return /* @__PURE__ */ jsx(Text, { children: ` ${line}` }, key);
|
|
@@ -1,7 +1,18 @@
|
|
|
1
|
+
import { VitestAgentTokens, inkStyle, statusGlyph, statusInkStyle } from "../theme.js";
|
|
1
2
|
import { Box, Text } from "ink";
|
|
3
|
+
import { useGlyphs } from "@effected/cli/ui";
|
|
2
4
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
5
|
|
|
4
6
|
//#region src/render-ink/FailuresSection.tsx
|
|
7
|
+
/**
|
|
8
|
+
* The capped failures block for aggregate-row shapes (workspace,
|
|
9
|
+
* single-project) — the rows there are projects or modules and cannot
|
|
10
|
+
* expand a per-test error inline, so the failures collect here.
|
|
11
|
+
*
|
|
12
|
+
* Each entry: a glyph (⧖ for a timed-out failure, ✗ otherwise), the
|
|
13
|
+
* `module › suite › test` path, the classification tag when known, and
|
|
14
|
+
* one indented line — the first line of the error message.
|
|
15
|
+
*/
|
|
5
16
|
const pathOf = (f) => {
|
|
6
17
|
return [
|
|
7
18
|
f.modulePath,
|
|
@@ -9,6 +20,7 @@ const pathOf = (f) => {
|
|
|
9
20
|
f.testName
|
|
10
21
|
].join(" › ");
|
|
11
22
|
};
|
|
23
|
+
const failureStatus = (f) => f.timedOut === true ? "timeout" : "failure";
|
|
12
24
|
const firstLine = (message) => message.split("\n", 1)[0] ?? "";
|
|
13
25
|
const FAILURE_VALUE_LIMIT = 200;
|
|
14
26
|
/**
|
|
@@ -20,6 +32,7 @@ const FAILURE_VALUE_LIMIT = 200;
|
|
|
20
32
|
const FailuresSection = ({ failures, limit }) => {
|
|
21
33
|
const shown = failures.slice(0, limit);
|
|
22
34
|
const overflow = failures.length - shown.length;
|
|
35
|
+
const glyphs = useGlyphs();
|
|
23
36
|
return /* @__PURE__ */ jsxs(Box, {
|
|
24
37
|
flexDirection: "column",
|
|
25
38
|
children: [
|
|
@@ -37,13 +50,13 @@ const FailuresSection = ({ failures, limit }) => {
|
|
|
37
50
|
/* @__PURE__ */ jsxs(Text, { children: [
|
|
38
51
|
" ",
|
|
39
52
|
/* @__PURE__ */ jsx(Text, {
|
|
40
|
-
|
|
41
|
-
children: f
|
|
53
|
+
...statusInkStyle(failureStatus(f)),
|
|
54
|
+
children: statusGlyph(failureStatus(f), glyphs)
|
|
42
55
|
}),
|
|
43
56
|
" ",
|
|
44
57
|
pathOf(f),
|
|
45
58
|
f.classification !== null ? /* @__PURE__ */ jsxs(Text, {
|
|
46
|
-
|
|
59
|
+
...inkStyle(VitestAgentTokens.classification),
|
|
47
60
|
children: [
|
|
48
61
|
" [",
|
|
49
62
|
f.classification,
|
package/render-ink/ProjectRow.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { statusInkStyle } from "../theme.js";
|
|
1
2
|
import { formatDisplayDuration } from "../format-duration.js";
|
|
2
3
|
import { CountColumns, DURATION_CELL_WIDTH } from "./CountColumns.js";
|
|
3
4
|
import { StatusIcon } from "./StatusIcon.js";
|
|
@@ -13,7 +14,7 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
|
13
14
|
*/
|
|
14
15
|
const projectGlyph = (counts, running, timedOut, frame) => {
|
|
15
16
|
if (running && !timedOut) return /* @__PURE__ */ jsx(Text, {
|
|
16
|
-
|
|
17
|
+
...statusInkStyle("running"),
|
|
17
18
|
children: frame
|
|
18
19
|
});
|
|
19
20
|
if (counts.failCount > 0) return /* @__PURE__ */ jsx(StatusIcon, { status: "failed" });
|
package/render-ink/StatusIcon.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { statusGlyph, statusInkStyle } from "../theme.js";
|
|
1
2
|
import { Text } from "ink";
|
|
3
|
+
import { useGlyphs } from "@effected/cli/ui";
|
|
2
4
|
import { jsx } from "react/jsx-runtime";
|
|
3
5
|
|
|
4
6
|
//#region src/render-ink/StatusIcon.tsx
|
|
@@ -8,37 +10,34 @@ import { jsx } from "react/jsx-runtime";
|
|
|
8
10
|
* Ink-only primitive — no DOM-isms. Renders one colored character so
|
|
9
11
|
* the surrounding row can keep its width budget predictable.
|
|
10
12
|
*/
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
pending: "cyan",
|
|
27
|
-
running: "yellow",
|
|
28
|
-
queued: "gray",
|
|
29
|
-
finished: "green",
|
|
30
|
-
threshold: "yellow",
|
|
31
|
-
"timed-out": "#e09a4e"
|
|
13
|
+
/**
|
|
14
|
+
* Each icon kind's status in the shared vocabulary. `finished` is a pass;
|
|
15
|
+
* `threshold` is a coverage threshold failure, which the kit ranks as a
|
|
16
|
+
* `failure`, not a warning.
|
|
17
|
+
*/
|
|
18
|
+
const STATUS = {
|
|
19
|
+
passed: "success",
|
|
20
|
+
failed: "failure",
|
|
21
|
+
skipped: "skip",
|
|
22
|
+
pending: "pending",
|
|
23
|
+
running: "running",
|
|
24
|
+
queued: "queued",
|
|
25
|
+
finished: "success",
|
|
26
|
+
threshold: "failure",
|
|
27
|
+
"timed-out": "timeout"
|
|
32
28
|
};
|
|
33
29
|
/**
|
|
34
30
|
* Renders a single colored status glyph for a test, module, or run.
|
|
35
31
|
*
|
|
36
32
|
* @public
|
|
37
33
|
*/
|
|
38
|
-
const StatusIcon = ({ status }) =>
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
34
|
+
const StatusIcon = ({ status }) => {
|
|
35
|
+
const glyphs = useGlyphs();
|
|
36
|
+
return /* @__PURE__ */ jsx(Text, {
|
|
37
|
+
...statusInkStyle(STATUS[status]),
|
|
38
|
+
children: statusGlyph(STATUS[status], glyphs)
|
|
39
|
+
});
|
|
40
|
+
};
|
|
42
41
|
|
|
43
42
|
//#endregion
|
|
44
43
|
export { StatusIcon };
|
package/render-ink/StreamApp.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { classifyRunShape } from "../dispatcher/classify.js";
|
|
2
|
+
import { VitestAgentTokens, inkStyle, statusGlyph, statusInkStyle } from "../theme.js";
|
|
2
3
|
import { formatDisplayDuration } from "../format-duration.js";
|
|
3
4
|
import { CountColumns, DURATION_CELL_WIDTH } from "./CountColumns.js";
|
|
4
5
|
import { StatusIcon } from "./StatusIcon.js";
|
|
@@ -8,6 +9,8 @@ import { spinnerFrame } from "./spinner.js";
|
|
|
8
9
|
import { TestRow } from "./TestRow.js";
|
|
9
10
|
import { TrendLine } from "./TrendLine.js";
|
|
10
11
|
import { Box, Text } from "ink";
|
|
12
|
+
import { Fmt } from "@effected/cli";
|
|
13
|
+
import { useGlyphs } from "@effected/cli/ui";
|
|
11
14
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
12
15
|
|
|
13
16
|
//#region src/render-ink/StreamApp.tsx
|
|
@@ -118,7 +121,7 @@ const ModuleStreamRow = ({ module, nowMs, frame, nameWidth, timedOut, tagUnion:
|
|
|
118
121
|
});
|
|
119
122
|
} else glyph = /* @__PURE__ */ jsx(StatusIcon, { status: "timed-out" });
|
|
120
123
|
} else if (running) glyph = /* @__PURE__ */ jsx(Text, {
|
|
121
|
-
|
|
124
|
+
...statusInkStyle("running"),
|
|
122
125
|
children: frame
|
|
123
126
|
});
|
|
124
127
|
else glyph = /* @__PURE__ */ jsx(StatusIcon, { status: moduleIcon(module) });
|
|
@@ -182,47 +185,54 @@ const InlineError = ({ failure }) => {
|
|
|
182
185
|
};
|
|
183
186
|
const failureKey = (f) => `failure:${f.modulePath}::${f.suitePath.join("/")}::${f.testName}`;
|
|
184
187
|
/** A process-level unhandled error, rendered in the aggregate Unhandled errors block. */
|
|
185
|
-
const UnhandledErrorItem = ({ error }) =>
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
"
|
|
189
|
-
/* @__PURE__ */
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
188
|
+
const UnhandledErrorItem = ({ error }) => {
|
|
189
|
+
const glyphs = useGlyphs();
|
|
190
|
+
return /* @__PURE__ */ jsxs(Box, {
|
|
191
|
+
flexDirection: "column",
|
|
192
|
+
children: [/* @__PURE__ */ jsxs(Text, { children: [
|
|
193
|
+
" ",
|
|
194
|
+
/* @__PURE__ */ jsx(Text, {
|
|
195
|
+
...statusInkStyle("failure"),
|
|
196
|
+
children: statusGlyph("failure", glyphs)
|
|
197
|
+
}),
|
|
198
|
+
" unhandled error"
|
|
199
|
+
] }), /* @__PURE__ */ jsxs(Text, {
|
|
200
|
+
dimColor: true,
|
|
201
|
+
children: [" ", error.message.split("\n", 1)[0] ?? ""]
|
|
202
|
+
})]
|
|
203
|
+
});
|
|
204
|
+
};
|
|
199
205
|
const testKey = (modulePath, t) => `test:${modulePath}::${t.suitePath.join("/")}::${t.testName}`;
|
|
200
206
|
const failurePath = (f) => [
|
|
201
207
|
f.modulePath,
|
|
202
208
|
...f.suitePath,
|
|
203
209
|
f.testName
|
|
204
210
|
].join(" › ");
|
|
211
|
+
const failureStatus = (f) => f.timedOut === true ? "timeout" : "failure";
|
|
205
212
|
/** A failure entry rendered in the Live region. */
|
|
206
|
-
const FailureItem = ({ failure }) =>
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
"
|
|
210
|
-
/* @__PURE__ */
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
})
|
|
213
|
+
const FailureItem = ({ failure }) => {
|
|
214
|
+
const glyphs = useGlyphs();
|
|
215
|
+
return /* @__PURE__ */ jsxs(Box, {
|
|
216
|
+
flexDirection: "column",
|
|
217
|
+
children: [/* @__PURE__ */ jsxs(Text, { children: [
|
|
218
|
+
" ",
|
|
219
|
+
/* @__PURE__ */ jsx(Text, {
|
|
220
|
+
...statusInkStyle(failureStatus(failure)),
|
|
221
|
+
children: statusGlyph(failureStatus(failure), glyphs)
|
|
222
|
+
}),
|
|
223
|
+
" ",
|
|
224
|
+
failurePath(failure),
|
|
225
|
+
failure.classification !== null ? /* @__PURE__ */ jsxs(Text, {
|
|
226
|
+
...inkStyle(VitestAgentTokens.classification),
|
|
227
|
+
children: [
|
|
228
|
+
" [",
|
|
229
|
+
failure.classification,
|
|
230
|
+
"]"
|
|
231
|
+
]
|
|
232
|
+
}) : null
|
|
233
|
+
] }), /* @__PURE__ */ jsx(InlineError, { failure })]
|
|
234
|
+
});
|
|
235
|
+
};
|
|
226
236
|
/**
|
|
227
237
|
* The bottom `Total:` rollup line. In the workspace view `labelWidth`
|
|
228
238
|
* pads the label to the project-row prefix width so the count columns
|
|
@@ -245,9 +255,15 @@ const TotalsLine = ({ totals, labelWidth }) => /* @__PURE__ */ jsxs(Text, { chil
|
|
|
245
255
|
children: [" ", formatDisplayDuration(totals.durationMs).padStart(7)]
|
|
246
256
|
})
|
|
247
257
|
] });
|
|
258
|
+
/**
|
|
259
|
+
* The coverage judgment. A threshold violation is a `failure` (the kit's
|
|
260
|
+
* decision: threshold failure is ✗, only a target shortfall is ⚠).
|
|
261
|
+
*/
|
|
248
262
|
const CoverageItem = ({ state }) => {
|
|
249
263
|
if (state.coverage === null) return null;
|
|
250
|
-
const
|
|
264
|
+
const violations = state.coverage.violations.length;
|
|
265
|
+
const status = violations === 0 ? "success" : "failure";
|
|
266
|
+
const glyphs = useGlyphs();
|
|
251
267
|
return /* @__PURE__ */ jsxs(Text, { children: [
|
|
252
268
|
/* @__PURE__ */ jsx(Text, {
|
|
253
269
|
bold: true,
|
|
@@ -255,11 +271,11 @@ const CoverageItem = ({ state }) => {
|
|
|
255
271
|
}),
|
|
256
272
|
" ",
|
|
257
273
|
/* @__PURE__ */ jsx(Text, {
|
|
258
|
-
|
|
259
|
-
children:
|
|
274
|
+
...statusInkStyle(status),
|
|
275
|
+
children: statusGlyph(status, glyphs)
|
|
260
276
|
}),
|
|
261
277
|
" ",
|
|
262
|
-
|
|
278
|
+
violations === 0 ? "all metrics meet thresholds" : Fmt.plural(violations, "threshold violation")
|
|
263
279
|
] });
|
|
264
280
|
};
|
|
265
281
|
const samePath = (a, b) => {
|
|
@@ -273,7 +289,7 @@ const samePath = (a, b) => {
|
|
|
273
289
|
* run picture — all rows in discovery order, with running rows capped
|
|
274
290
|
* at MAX_LIVE_RUNNING_ROWS and finished rows always shown.
|
|
275
291
|
*/
|
|
276
|
-
const liveRegion = (state, shape, nowMs, frame) => {
|
|
292
|
+
const liveRegion = (state, shape, nowMs, frame, glyphs) => {
|
|
277
293
|
const groups = groupByProject(state);
|
|
278
294
|
const timedOut = state.phase === "timed-out";
|
|
279
295
|
const finished = state.phase === "finished" || state.phase === "timed-out";
|
|
@@ -287,14 +303,14 @@ const liveRegion = (state, shape, nowMs, frame) => {
|
|
|
287
303
|
}) : null;
|
|
288
304
|
if (shape === "single-test") {
|
|
289
305
|
const sole = ordered[0];
|
|
290
|
-
if (sole === void 0 || sole.tests.length === 0) return /* @__PURE__ */
|
|
306
|
+
if (sole === void 0 || sole.tests.length === 0) return /* @__PURE__ */ jsxs(Text, {
|
|
291
307
|
dimColor: true,
|
|
292
|
-
children: "discovering tests
|
|
308
|
+
children: ["discovering tests", glyphs.ellipsis]
|
|
293
309
|
});
|
|
294
310
|
const only = sole.tests[0];
|
|
295
|
-
if (only === void 0) return /* @__PURE__ */
|
|
311
|
+
if (only === void 0) return /* @__PURE__ */ jsxs(Text, {
|
|
296
312
|
dimColor: true,
|
|
297
|
-
children: "discovering tests
|
|
313
|
+
children: ["discovering tests", glyphs.ellipsis]
|
|
298
314
|
});
|
|
299
315
|
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;
|
|
300
316
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
@@ -306,9 +322,9 @@ const liveRegion = (state, shape, nowMs, frame) => {
|
|
|
306
322
|
unhandledErrorsSection
|
|
307
323
|
] });
|
|
308
324
|
}
|
|
309
|
-
if (groups.length === 0) return /* @__PURE__ */
|
|
325
|
+
if (groups.length === 0) return /* @__PURE__ */ jsxs(Text, {
|
|
310
326
|
dimColor: true,
|
|
311
|
-
children: "discovering tests
|
|
327
|
+
children: ["discovering tests", glyphs.ellipsis]
|
|
312
328
|
});
|
|
313
329
|
const failuresSection = state.failures.length > 0 ? /* @__PURE__ */ jsx(Box, {
|
|
314
330
|
flexDirection: "column",
|
|
@@ -318,10 +334,10 @@ const liveRegion = (state, shape, nowMs, frame) => {
|
|
|
318
334
|
const trendItem = state.trend !== null ? /* @__PURE__ */ jsx(TrendLine, { trend: state.trend }) : null;
|
|
319
335
|
const totalsItem = (labelWidth) => /* @__PURE__ */ jsxs(Box, {
|
|
320
336
|
flexDirection: "column",
|
|
321
|
-
children: [finished && timedOut ? /* @__PURE__ */
|
|
322
|
-
|
|
337
|
+
children: [finished && timedOut ? /* @__PURE__ */ jsxs(Text, {
|
|
338
|
+
...statusInkStyle("timeout"),
|
|
323
339
|
bold: true,
|
|
324
|
-
children: "
|
|
340
|
+
children: [statusGlyph("timeout", glyphs), " Run timed out"]
|
|
325
341
|
}) : null, /* @__PURE__ */ jsx(TotalsLine, {
|
|
326
342
|
totals: state.totals,
|
|
327
343
|
labelWidth
|
|
@@ -379,7 +395,9 @@ const liveRegion = (state, shape, nowMs, frame) => {
|
|
|
379
395
|
runningOverflow > 0 ? /* @__PURE__ */ jsxs(Text, {
|
|
380
396
|
dimColor: true,
|
|
381
397
|
children: [
|
|
382
|
-
"
|
|
398
|
+
" ",
|
|
399
|
+
glyphs.ellipsis,
|
|
400
|
+
" and ",
|
|
383
401
|
runningOverflow,
|
|
384
402
|
" more running"
|
|
385
403
|
]
|
|
@@ -430,7 +448,9 @@ const liveRegion = (state, shape, nowMs, frame) => {
|
|
|
430
448
|
runningOverflow > 0 ? /* @__PURE__ */ jsxs(Text, {
|
|
431
449
|
dimColor: true,
|
|
432
450
|
children: [
|
|
433
|
-
"
|
|
451
|
+
" ",
|
|
452
|
+
glyphs.ellipsis,
|
|
453
|
+
" and ",
|
|
434
454
|
runningOverflow,
|
|
435
455
|
" more running"
|
|
436
456
|
]
|
|
@@ -443,9 +463,9 @@ const liveRegion = (state, shape, nowMs, frame) => {
|
|
|
443
463
|
] });
|
|
444
464
|
}
|
|
445
465
|
const sole = ordered[0];
|
|
446
|
-
if (sole === void 0) return /* @__PURE__ */
|
|
466
|
+
if (sole === void 0) return /* @__PURE__ */ jsxs(Text, {
|
|
447
467
|
dimColor: true,
|
|
448
|
-
children: "discovering tests
|
|
468
|
+
children: ["discovering tests", glyphs.ellipsis]
|
|
449
469
|
});
|
|
450
470
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
451
471
|
/* @__PURE__ */ jsxs(Text, { children: [/* @__PURE__ */ jsx(Text, {
|
|
@@ -483,10 +503,11 @@ const liveRegion = (state, shape, nowMs, frame) => {
|
|
|
483
503
|
*/
|
|
484
504
|
const StreamApp = ({ state, frameIndex, nowMs }) => {
|
|
485
505
|
const now = nowMs ?? Date.now();
|
|
486
|
-
const
|
|
506
|
+
const glyphs = useGlyphs();
|
|
507
|
+
const frame = spinnerFrame(frameIndex, glyphs);
|
|
487
508
|
const projects = groupByProject(state).map((g) => buildProjectSummary(g.name, sumCounts(g.modules)));
|
|
488
509
|
const shape = classifyRunShape(state, projects);
|
|
489
|
-
const liveContent = liveRegion(state, shape, now, frame);
|
|
510
|
+
const liveContent = liveRegion(state, shape, now, frame, glyphs);
|
|
490
511
|
return /* @__PURE__ */ jsx(Box, {
|
|
491
512
|
flexDirection: "column",
|
|
492
513
|
children: liveContent
|