@vitest-agent/ui 1.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/LICENSE +21 -0
- package/README.md +53 -0
- package/dispatcher/cells/single-file-fail.js +28 -0
- package/dispatcher/cells/single-file-pass.js +19 -0
- package/dispatcher/cells/single-file-threshold.js +22 -0
- package/dispatcher/cells/single-project-fail.js +34 -0
- package/dispatcher/cells/single-project-pass.js +17 -0
- package/dispatcher/cells/single-project-threshold.js +25 -0
- package/dispatcher/cells/single-test-fail.js +22 -0
- package/dispatcher/cells/single-test-pass.js +19 -0
- package/dispatcher/cells/single-test-threshold.js +12 -0
- package/dispatcher/cells/workspace-fail.js +27 -0
- package/dispatcher/cells/workspace-pass.js +25 -0
- package/dispatcher/cells/workspace-threshold.js +28 -0
- package/dispatcher/classify.js +48 -0
- package/dispatcher/dispatch.js +73 -0
- package/dispatcher/footer.js +55 -0
- package/dispatcher/helpers.js +186 -0
- package/dispatcher/ink-helpers.js +79 -0
- package/format-duration.js +34 -0
- package/index.d.ts +1843 -0
- package/index.js +40 -0
- package/package.json +59 -0
- package/pubsub/Channel.js +30 -0
- package/pubsub/Publisher.js +37 -0
- package/pubsub/Subscriber.js +77 -0
- package/pubsub/index.js +5 -0
- package/reducer.js +260 -0
- package/render-agent.js +137 -0
- package/render-ink/CountColumns.js +34 -0
- package/render-ink/CoverageBlock.js +80 -0
- package/render-ink/FailureSection.js +69 -0
- package/render-ink/FailuresSection.js +85 -0
- package/render-ink/ModuleHeader.js +43 -0
- package/render-ink/ProjectRow.js +56 -0
- package/render-ink/StatusIcon.js +44 -0
- package/render-ink/StreamApp.js +453 -0
- package/render-ink/SuggestedActions.js +49 -0
- package/render-ink/TestRow.js +33 -0
- package/render-ink/TrendLine.js +42 -0
- package/render-ink/index.js +14 -0
- package/render-ink/spinner.js +62 -0
- package/render-ink/tag-suffix.js +18 -0
- package/synthesize.js +335 -0
- package/tsdoc-metadata.json +11 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Box, Text } from "ink";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
|
|
4
|
+
//#region src/render-ink/CoverageBlock.tsx
|
|
5
|
+
const METRIC_ORDER = [
|
|
6
|
+
"lines",
|
|
7
|
+
"branches",
|
|
8
|
+
"functions",
|
|
9
|
+
"statements"
|
|
10
|
+
];
|
|
11
|
+
const formatPercent = (n) => {
|
|
12
|
+
return `${Math.round(n * 10) / 10}%`;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Renders the coverage section: per-metric percentages, threshold
|
|
16
|
+
* violations, and top-N gap entries sorted by missing line count.
|
|
17
|
+
*
|
|
18
|
+
* @public
|
|
19
|
+
*/
|
|
20
|
+
const CoverageBlock = ({ coverage, maxGaps = 3 }) => {
|
|
21
|
+
const sortedGaps = [...coverage.gaps].sort((a, b) => b.missing.lines - a.missing.lines);
|
|
22
|
+
const topGaps = maxGaps > 0 ? sortedGaps.slice(0, maxGaps) : [];
|
|
23
|
+
const elidedGaps = maxGaps > 0 ? coverage.gaps.length - topGaps.length : 0;
|
|
24
|
+
return /* @__PURE__ */ jsxs(Box, {
|
|
25
|
+
flexDirection: "column",
|
|
26
|
+
children: [
|
|
27
|
+
/* @__PURE__ */ jsx(Text, {
|
|
28
|
+
bold: true,
|
|
29
|
+
children: "Coverage"
|
|
30
|
+
}),
|
|
31
|
+
METRIC_ORDER.map((metric) => {
|
|
32
|
+
const actual = coverage.metrics[metric];
|
|
33
|
+
const threshold = coverage.thresholds[metric];
|
|
34
|
+
const failing = threshold !== void 0 && actual < threshold;
|
|
35
|
+
return /* @__PURE__ */ jsxs(Box, { children: [
|
|
36
|
+
/* @__PURE__ */ jsx(Text, { children: ` ${metric}: ` }),
|
|
37
|
+
failing ? /* @__PURE__ */ jsx(Text, {
|
|
38
|
+
color: "red",
|
|
39
|
+
children: formatPercent(actual)
|
|
40
|
+
}) : /* @__PURE__ */ jsx(Text, { children: formatPercent(actual) }),
|
|
41
|
+
threshold !== void 0 ? /* @__PURE__ */ jsxs(Text, {
|
|
42
|
+
dimColor: true,
|
|
43
|
+
children: [
|
|
44
|
+
" (threshold ",
|
|
45
|
+
formatPercent(threshold),
|
|
46
|
+
")"
|
|
47
|
+
]
|
|
48
|
+
}) : null
|
|
49
|
+
] }, metric);
|
|
50
|
+
}),
|
|
51
|
+
coverage.violations.length > 0 ? /* @__PURE__ */ jsxs(Box, {
|
|
52
|
+
flexDirection: "column",
|
|
53
|
+
children: [/* @__PURE__ */ jsx(Text, {
|
|
54
|
+
color: "red",
|
|
55
|
+
children: "Violations"
|
|
56
|
+
}), coverage.violations.map((v) => /* @__PURE__ */ jsx(Text, {
|
|
57
|
+
color: "red",
|
|
58
|
+
children: ` ${v.metric}: ${formatPercent(v.actual)} < ${formatPercent(v.expected)}`
|
|
59
|
+
}, v.metric))]
|
|
60
|
+
}) : null,
|
|
61
|
+
topGaps.length > 0 ? /* @__PURE__ */ jsxs(Box, {
|
|
62
|
+
flexDirection: "column",
|
|
63
|
+
children: [
|
|
64
|
+
/* @__PURE__ */ jsx(Text, {
|
|
65
|
+
bold: true,
|
|
66
|
+
children: "Gaps"
|
|
67
|
+
}),
|
|
68
|
+
topGaps.map((g) => /* @__PURE__ */ jsxs(Text, { children: [` ${g.file}`, g.uncoveredLines !== void 0 ? `: ${g.uncoveredLines}` : ""] }, g.file)),
|
|
69
|
+
elidedGaps > 0 ? /* @__PURE__ */ jsx(Text, {
|
|
70
|
+
dimColor: true,
|
|
71
|
+
children: ` (+${elidedGaps} more ${elidedGaps === 1 ? "gap" : "gaps"})`
|
|
72
|
+
}) : null
|
|
73
|
+
]
|
|
74
|
+
}) : null
|
|
75
|
+
]
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
//#endregion
|
|
80
|
+
export { CoverageBlock };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Box, Text } from "ink";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
|
|
4
|
+
//#region src/render-ink/FailureSection.tsx
|
|
5
|
+
const FailureRow = ({ failure, includeStack }) => {
|
|
6
|
+
const suite = failure.suitePath.length > 0 ? `${failure.suitePath.join(" > ")} > ` : "";
|
|
7
|
+
const classification = failure.classification !== null ? ` [${failure.classification}]` : "";
|
|
8
|
+
return /* @__PURE__ */ jsxs(Box, {
|
|
9
|
+
flexDirection: "column",
|
|
10
|
+
children: [
|
|
11
|
+
/* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsxs(Text, { children: [
|
|
12
|
+
/* @__PURE__ */ jsx(Text, {
|
|
13
|
+
color: "red",
|
|
14
|
+
children: "✗"
|
|
15
|
+
}),
|
|
16
|
+
` ${failure.modulePath} > ${suite}${failure.testName}`,
|
|
17
|
+
failure.classification !== null ? /* @__PURE__ */ jsx(Text, {
|
|
18
|
+
color: "yellow",
|
|
19
|
+
children: classification
|
|
20
|
+
}) : null
|
|
21
|
+
] }) }),
|
|
22
|
+
failure.error?.message !== void 0 ? /* @__PURE__ */ jsx(Text, {
|
|
23
|
+
color: "red",
|
|
24
|
+
children: ` ${failure.error.message.split("\n", 1)[0] ?? ""}`
|
|
25
|
+
}) : null,
|
|
26
|
+
failure.error?.diff !== void 0 ? failure.error.diff.split("\n").map((line, idx) => {
|
|
27
|
+
const key = `diff-${idx}`;
|
|
28
|
+
if (line.startsWith("-")) return /* @__PURE__ */ jsx(Text, {
|
|
29
|
+
color: "red",
|
|
30
|
+
children: ` ${line}`
|
|
31
|
+
}, key);
|
|
32
|
+
if (line.startsWith("+")) return /* @__PURE__ */ jsx(Text, {
|
|
33
|
+
color: "green",
|
|
34
|
+
children: ` ${line}`
|
|
35
|
+
}, key);
|
|
36
|
+
return /* @__PURE__ */ jsx(Text, { children: ` ${line}` }, key);
|
|
37
|
+
}) : null,
|
|
38
|
+
includeStack && failure.error?.stack !== void 0 ? failure.error.stack.split("\n").map((line, idx) => {
|
|
39
|
+
const key = `stack-${idx}`;
|
|
40
|
+
return /* @__PURE__ */ jsx(Text, {
|
|
41
|
+
dimColor: true,
|
|
42
|
+
children: ` ${line}`
|
|
43
|
+
}, key);
|
|
44
|
+
}) : null
|
|
45
|
+
]
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Renders one block per failed test: glyph, path, classification tag,
|
|
50
|
+
* error message, diff lines, and optionally the stack trace.
|
|
51
|
+
*
|
|
52
|
+
* @public
|
|
53
|
+
*/
|
|
54
|
+
const FailureSection = ({ failures, includeStack = false }) => {
|
|
55
|
+
if (failures.length === 0) return null;
|
|
56
|
+
return /* @__PURE__ */ jsxs(Box, {
|
|
57
|
+
flexDirection: "column",
|
|
58
|
+
children: [/* @__PURE__ */ jsx(Text, {
|
|
59
|
+
bold: true,
|
|
60
|
+
children: "Failures"
|
|
61
|
+
}), failures.map((failure) => /* @__PURE__ */ jsx(FailureRow, {
|
|
62
|
+
failure,
|
|
63
|
+
includeStack
|
|
64
|
+
}, `${failure.modulePath}::${failure.suitePath.join("/")}::${failure.testName}`))]
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
//#endregion
|
|
69
|
+
export { FailureSection };
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { Box, Text } from "ink";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
|
|
4
|
+
//#region src/render-ink/FailuresSection.tsx
|
|
5
|
+
const pathOf = (f) => {
|
|
6
|
+
return [
|
|
7
|
+
f.modulePath,
|
|
8
|
+
...f.suitePath,
|
|
9
|
+
f.testName
|
|
10
|
+
].join(" › ");
|
|
11
|
+
};
|
|
12
|
+
const firstLine = (message) => message.split("\n", 1)[0] ?? "";
|
|
13
|
+
const FAILURE_VALUE_LIMIT = 200;
|
|
14
|
+
/**
|
|
15
|
+
* Renders the capped failures block for aggregate-row shapes.
|
|
16
|
+
* Entries beyond `limit` collapse into an overflow line.
|
|
17
|
+
*
|
|
18
|
+
* @public
|
|
19
|
+
*/
|
|
20
|
+
const FailuresSection = ({ failures, limit }) => {
|
|
21
|
+
const shown = failures.slice(0, limit);
|
|
22
|
+
const overflow = failures.length - shown.length;
|
|
23
|
+
return /* @__PURE__ */ jsxs(Box, {
|
|
24
|
+
flexDirection: "column",
|
|
25
|
+
children: [
|
|
26
|
+
/* @__PURE__ */ jsxs(Text, {
|
|
27
|
+
bold: true,
|
|
28
|
+
children: [
|
|
29
|
+
"Failures (",
|
|
30
|
+
failures.length,
|
|
31
|
+
"):"
|
|
32
|
+
]
|
|
33
|
+
}),
|
|
34
|
+
shown.map((f, i) => /* @__PURE__ */ jsxs(Box, {
|
|
35
|
+
flexDirection: "column",
|
|
36
|
+
children: [
|
|
37
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
38
|
+
" ",
|
|
39
|
+
/* @__PURE__ */ jsx(Text, {
|
|
40
|
+
color: f.timedOut === true ? "#e09a4e" : "red",
|
|
41
|
+
children: f.timedOut === true ? "⧖" : "✗"
|
|
42
|
+
}),
|
|
43
|
+
" ",
|
|
44
|
+
pathOf(f),
|
|
45
|
+
f.classification !== null ? /* @__PURE__ */ jsxs(Text, {
|
|
46
|
+
color: "#c98ae0",
|
|
47
|
+
children: [
|
|
48
|
+
" [",
|
|
49
|
+
f.classification,
|
|
50
|
+
"]"
|
|
51
|
+
]
|
|
52
|
+
}) : null
|
|
53
|
+
] }),
|
|
54
|
+
f.error?.message !== void 0 ? /* @__PURE__ */ jsxs(Text, {
|
|
55
|
+
dimColor: true,
|
|
56
|
+
children: [" ", firstLine(f.error.message)]
|
|
57
|
+
}) : null,
|
|
58
|
+
f.error?.expected !== void 0 ? /* @__PURE__ */ jsxs(Text, {
|
|
59
|
+
dimColor: true,
|
|
60
|
+
children: [
|
|
61
|
+
" ",
|
|
62
|
+
"expected: ",
|
|
63
|
+
f.error.expected.slice(0, FAILURE_VALUE_LIMIT)
|
|
64
|
+
]
|
|
65
|
+
}) : null,
|
|
66
|
+
f.error?.received !== void 0 ? /* @__PURE__ */ jsxs(Text, {
|
|
67
|
+
dimColor: true,
|
|
68
|
+
children: [
|
|
69
|
+
" ",
|
|
70
|
+
"received: ",
|
|
71
|
+
f.error.received.slice(0, FAILURE_VALUE_LIMIT)
|
|
72
|
+
]
|
|
73
|
+
}) : null
|
|
74
|
+
]
|
|
75
|
+
}, `${f.modulePath}:${f.testName}:${i}`)),
|
|
76
|
+
overflow > 0 ? /* @__PURE__ */ jsx(Text, {
|
|
77
|
+
dimColor: true,
|
|
78
|
+
children: ` … ${overflow} more`
|
|
79
|
+
}) : null
|
|
80
|
+
]
|
|
81
|
+
});
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
//#endregion
|
|
85
|
+
export { FailuresSection };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { formatDisplayDuration } from "../format-duration.js";
|
|
2
|
+
import { StatusIcon } from "./StatusIcon.js";
|
|
3
|
+
import { Box, Text } from "ink";
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
|
|
6
|
+
//#region src/render-ink/ModuleHeader.tsx
|
|
7
|
+
const moduleGlyph = (m) => {
|
|
8
|
+
if (m.status === "queued") return "queued";
|
|
9
|
+
if (m.status === "running") return "running";
|
|
10
|
+
if (m.failCount > 0) return "failed";
|
|
11
|
+
return "passed";
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Renders the per-module title line: a status glyph, the module path,
|
|
15
|
+
* and a dimmed inline summary of pass/fail/skip counts and duration.
|
|
16
|
+
*
|
|
17
|
+
* @public
|
|
18
|
+
*/
|
|
19
|
+
const ModuleHeader = ({ module }) => {
|
|
20
|
+
const parts = [];
|
|
21
|
+
if (module.passCount > 0) parts.push(`${module.passCount} passed`);
|
|
22
|
+
if (module.failCount > 0) parts.push(`${module.failCount} failed`);
|
|
23
|
+
if (module.skipCount > 0) parts.push(`${module.skipCount} skipped`);
|
|
24
|
+
const summary = parts.length > 0 ? parts.join(", ") : module.status;
|
|
25
|
+
return /* @__PURE__ */ jsxs(Box, { children: [
|
|
26
|
+
/* @__PURE__ */ jsx(StatusIcon, { status: moduleGlyph(module) }),
|
|
27
|
+
/* @__PURE__ */ jsxs(Text, { children: [" ", module.modulePath] }),
|
|
28
|
+
/* @__PURE__ */ jsxs(Text, {
|
|
29
|
+
dimColor: true,
|
|
30
|
+
children: [
|
|
31
|
+
" ",
|
|
32
|
+
"(",
|
|
33
|
+
summary,
|
|
34
|
+
", ",
|
|
35
|
+
formatDisplayDuration(module.durationMs),
|
|
36
|
+
")"
|
|
37
|
+
]
|
|
38
|
+
})
|
|
39
|
+
] });
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
//#endregion
|
|
43
|
+
export { ModuleHeader };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { formatDisplayDuration } from "../format-duration.js";
|
|
2
|
+
import { CountColumns } from "./CountColumns.js";
|
|
3
|
+
import { StatusIcon } from "./StatusIcon.js";
|
|
4
|
+
import { formatTagSuffix } from "./tag-suffix.js";
|
|
5
|
+
import { Box, Text } from "ink";
|
|
6
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
|
|
8
|
+
//#region src/render-ink/ProjectRow.tsx
|
|
9
|
+
/**
|
|
10
|
+
* Pick the glyph element for the project row using the spec's severity
|
|
11
|
+
* precedence (`✗ > ⧖ > ⚠ > ↷ > ✓`). Skip-only projects (no passes, no
|
|
12
|
+
* failures, no timeouts) resolve to `↷`, not a false-positive `✓`.
|
|
13
|
+
*/
|
|
14
|
+
const projectGlyph = (counts, running, timedOut, frame) => {
|
|
15
|
+
if (running && !timedOut) return /* @__PURE__ */ jsx(Text, {
|
|
16
|
+
color: "yellow",
|
|
17
|
+
children: frame
|
|
18
|
+
});
|
|
19
|
+
if (counts.failCount > 0) return /* @__PURE__ */ jsx(StatusIcon, { status: "failed" });
|
|
20
|
+
if (counts.timeoutCount > 0 || running && timedOut) return /* @__PURE__ */ jsx(StatusIcon, { status: "timed-out" });
|
|
21
|
+
if (counts.skipCount > 0 && counts.passCount === 0) return /* @__PURE__ */ jsx(StatusIcon, { status: "skipped" });
|
|
22
|
+
return /* @__PURE__ */ jsx(StatusIcon, { status: "passed" });
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Renders one per-project row in the `workspace`-shape live view: status
|
|
26
|
+
* glyph (animated spinner while running), project name, count columns,
|
|
27
|
+
* elapsed duration, and optional tag counts.
|
|
28
|
+
*
|
|
29
|
+
* @public
|
|
30
|
+
*/
|
|
31
|
+
const ProjectRow = ({ project, counts, running, timedOut = false, elapsedMs, frame, nameWidth, tagCounts }) => /* @__PURE__ */ jsxs(Box, { children: [
|
|
32
|
+
/* @__PURE__ */ jsx(Text, { children: " " }),
|
|
33
|
+
projectGlyph(counts, running, timedOut, frame),
|
|
34
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
35
|
+
" ",
|
|
36
|
+
project.name.padEnd(nameWidth),
|
|
37
|
+
" "
|
|
38
|
+
] }),
|
|
39
|
+
/* @__PURE__ */ jsx(CountColumns, {
|
|
40
|
+
passCount: counts.passCount,
|
|
41
|
+
failCount: counts.failCount,
|
|
42
|
+
skipCount: counts.skipCount,
|
|
43
|
+
timeoutCount: counts.timeoutCount
|
|
44
|
+
}),
|
|
45
|
+
/* @__PURE__ */ jsxs(Text, {
|
|
46
|
+
dimColor: true,
|
|
47
|
+
children: [" ", formatDisplayDuration(elapsedMs)]
|
|
48
|
+
}),
|
|
49
|
+
formatTagSuffix(tagCounts).length > 0 ? /* @__PURE__ */ jsxs(Text, {
|
|
50
|
+
color: "cyan",
|
|
51
|
+
children: [" ", formatTagSuffix(tagCounts)]
|
|
52
|
+
}) : null
|
|
53
|
+
] });
|
|
54
|
+
|
|
55
|
+
//#endregion
|
|
56
|
+
export { ProjectRow };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Text } from "ink";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
|
|
4
|
+
//#region src/render-ink/StatusIcon.tsx
|
|
5
|
+
/**
|
|
6
|
+
* Single-character status glyph for tests, modules, and the run itself.
|
|
7
|
+
*
|
|
8
|
+
* Ink-only primitive — no DOM-isms. Renders one colored character so
|
|
9
|
+
* the surrounding row can keep its width budget predictable.
|
|
10
|
+
*/
|
|
11
|
+
const GLYPH = {
|
|
12
|
+
passed: "✓",
|
|
13
|
+
failed: "✗",
|
|
14
|
+
skipped: "↷",
|
|
15
|
+
pending: "◯",
|
|
16
|
+
running: "…",
|
|
17
|
+
queued: "·",
|
|
18
|
+
finished: "✓",
|
|
19
|
+
threshold: "⚠",
|
|
20
|
+
"timed-out": "⧖"
|
|
21
|
+
};
|
|
22
|
+
const COLOR = {
|
|
23
|
+
passed: "green",
|
|
24
|
+
failed: "red",
|
|
25
|
+
skipped: "gray",
|
|
26
|
+
pending: "cyan",
|
|
27
|
+
running: "yellow",
|
|
28
|
+
queued: "gray",
|
|
29
|
+
finished: "green",
|
|
30
|
+
threshold: "yellow",
|
|
31
|
+
"timed-out": "#e09a4e"
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Renders a single colored status glyph for a test, module, or run.
|
|
35
|
+
*
|
|
36
|
+
* @public
|
|
37
|
+
*/
|
|
38
|
+
const StatusIcon = ({ status }) => /* @__PURE__ */ jsx(Text, {
|
|
39
|
+
color: COLOR[status],
|
|
40
|
+
children: GLYPH[status]
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
//#endregion
|
|
44
|
+
export { StatusIcon };
|