@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,453 @@
|
|
|
1
|
+
import { classifyRunShape } from "../dispatcher/classify.js";
|
|
2
|
+
import { formatDisplayDuration } from "../format-duration.js";
|
|
3
|
+
import { CountColumns } from "./CountColumns.js";
|
|
4
|
+
import { StatusIcon } from "./StatusIcon.js";
|
|
5
|
+
import { formatTagSuffix } from "./tag-suffix.js";
|
|
6
|
+
import { ProjectRow } from "./ProjectRow.js";
|
|
7
|
+
import { spinnerFrame } from "./spinner.js";
|
|
8
|
+
import { TestRow } from "./TestRow.js";
|
|
9
|
+
import { TrendLine } from "./TrendLine.js";
|
|
10
|
+
import { Box, Text } from "ink";
|
|
11
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
12
|
+
|
|
13
|
+
//#region src/render-ink/StreamApp.tsx
|
|
14
|
+
const ANONYMOUS_PROJECT = "default";
|
|
15
|
+
/** Maximum number of running rows displayed in the Live region. Beyond this,
|
|
16
|
+
* excess running units collapse into a single "… and N more running" line. */
|
|
17
|
+
const MAX_LIVE_RUNNING_ROWS = 4;
|
|
18
|
+
const groupByProject = (state) => {
|
|
19
|
+
const order = [];
|
|
20
|
+
const byName = /* @__PURE__ */ new Map();
|
|
21
|
+
for (const path of state.moduleOrder) {
|
|
22
|
+
const mod = state.modules[path];
|
|
23
|
+
if (mod === void 0) continue;
|
|
24
|
+
const name = mod.projectName ?? ANONYMOUS_PROJECT;
|
|
25
|
+
const existing = byName.get(name);
|
|
26
|
+
if (existing === void 0) {
|
|
27
|
+
byName.set(name, [mod]);
|
|
28
|
+
order.push(name);
|
|
29
|
+
} else existing.push(mod);
|
|
30
|
+
}
|
|
31
|
+
return order.map((name) => ({
|
|
32
|
+
name,
|
|
33
|
+
modules: byName.get(name) ?? []
|
|
34
|
+
}));
|
|
35
|
+
};
|
|
36
|
+
const moduleRunning = (m) => m.status !== "finished";
|
|
37
|
+
const moduleElapsedMs = (m, nowMs) => {
|
|
38
|
+
if (m.status === "finished") return m.durationMs;
|
|
39
|
+
if (m.startedAt === void 0) return 0;
|
|
40
|
+
const started = Date.parse(m.startedAt);
|
|
41
|
+
return Number.isNaN(started) ? 0 : Math.max(0, nowMs - started);
|
|
42
|
+
};
|
|
43
|
+
const sumCounts = (modules) => {
|
|
44
|
+
let passCount = 0;
|
|
45
|
+
let failCount = 0;
|
|
46
|
+
let skipCount = 0;
|
|
47
|
+
let timeoutCount = 0;
|
|
48
|
+
for (const m of modules) {
|
|
49
|
+
passCount += m.passCount;
|
|
50
|
+
failCount += m.failCount;
|
|
51
|
+
skipCount += m.skipCount;
|
|
52
|
+
timeoutCount += m.timeoutCount;
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
passCount,
|
|
56
|
+
failCount,
|
|
57
|
+
skipCount,
|
|
58
|
+
timeoutCount
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
const mergeTagCounts = (modules) => {
|
|
62
|
+
const out = {};
|
|
63
|
+
for (const m of modules) for (const [tag, count] of Object.entries(m.tagCounts ?? {})) out[tag] = (out[tag] ?? 0) + count;
|
|
64
|
+
return out;
|
|
65
|
+
};
|
|
66
|
+
const projectElapsedMs = (group, nowMs) => {
|
|
67
|
+
if (!group.modules.some(moduleRunning)) {
|
|
68
|
+
let total = 0;
|
|
69
|
+
for (const m of group.modules) total += m.durationMs;
|
|
70
|
+
return total;
|
|
71
|
+
}
|
|
72
|
+
let earliest = Number.POSITIVE_INFINITY;
|
|
73
|
+
for (const m of group.modules) {
|
|
74
|
+
if (m.startedAt === void 0) continue;
|
|
75
|
+
const started = Date.parse(m.startedAt);
|
|
76
|
+
if (!Number.isNaN(started) && started < earliest) earliest = started;
|
|
77
|
+
}
|
|
78
|
+
return earliest === Number.POSITIVE_INFINITY ? 0 : Math.max(0, nowMs - earliest);
|
|
79
|
+
};
|
|
80
|
+
const moduleIcon = (m) => {
|
|
81
|
+
if (m.failCount > 0) return "failed";
|
|
82
|
+
if (m.timeoutCount > 0) return "timed-out";
|
|
83
|
+
if (m.skipCount > 0 && m.passCount === 0) return "skipped";
|
|
84
|
+
return "passed";
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* One `single-project`-shape row: a module with the four count columns.
|
|
88
|
+
*
|
|
89
|
+
* Used in the Live region for both running rows (spinner, `running=true`)
|
|
90
|
+
* and finished rows (resolved glyph, `running=false`).
|
|
91
|
+
*/
|
|
92
|
+
const ModuleStreamRow = ({ module, nowMs, frame, nameWidth, timedOut }) => {
|
|
93
|
+
const running = moduleRunning(module);
|
|
94
|
+
const queued = module.status === "queued";
|
|
95
|
+
let glyph;
|
|
96
|
+
let notStartedSuffix = null;
|
|
97
|
+
if (timedOut && running) if (queued) {
|
|
98
|
+
glyph = /* @__PURE__ */ jsx(StatusIcon, { status: "queued" });
|
|
99
|
+
notStartedSuffix = /* @__PURE__ */ jsx(Text, {
|
|
100
|
+
dimColor: true,
|
|
101
|
+
children: " not started — killed before reaching this module"
|
|
102
|
+
});
|
|
103
|
+
} else glyph = /* @__PURE__ */ jsx(StatusIcon, { status: "timed-out" });
|
|
104
|
+
else if (running) glyph = /* @__PURE__ */ jsx(Text, {
|
|
105
|
+
color: "yellow",
|
|
106
|
+
children: frame
|
|
107
|
+
});
|
|
108
|
+
else glyph = /* @__PURE__ */ jsx(StatusIcon, { status: moduleIcon(module) });
|
|
109
|
+
return /* @__PURE__ */ jsxs(Box, { children: [
|
|
110
|
+
/* @__PURE__ */ jsx(Text, { children: " " }),
|
|
111
|
+
glyph,
|
|
112
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
113
|
+
" ",
|
|
114
|
+
module.modulePath.padEnd(nameWidth),
|
|
115
|
+
" "
|
|
116
|
+
] }),
|
|
117
|
+
/* @__PURE__ */ jsx(CountColumns, {
|
|
118
|
+
passCount: module.passCount,
|
|
119
|
+
failCount: module.failCount,
|
|
120
|
+
skipCount: module.skipCount,
|
|
121
|
+
timeoutCount: module.timeoutCount
|
|
122
|
+
}),
|
|
123
|
+
/* @__PURE__ */ jsxs(Text, {
|
|
124
|
+
dimColor: true,
|
|
125
|
+
children: [" ", formatDisplayDuration(moduleElapsedMs(module, nowMs))]
|
|
126
|
+
}),
|
|
127
|
+
formatTagSuffix(module.tagCounts).length > 0 ? /* @__PURE__ */ jsxs(Text, {
|
|
128
|
+
color: "cyan",
|
|
129
|
+
children: [" ", formatTagSuffix(module.tagCounts)]
|
|
130
|
+
}) : null,
|
|
131
|
+
notStartedSuffix
|
|
132
|
+
] });
|
|
133
|
+
};
|
|
134
|
+
const INLINE_VALUE_LIMIT = 200;
|
|
135
|
+
/** A failing test's error, expanded inline beneath a leaf row or in a failure block. */
|
|
136
|
+
const InlineError = ({ failure }) => {
|
|
137
|
+
if (failure.error?.message === void 0) return null;
|
|
138
|
+
const first = failure.error.message.split("\n", 1)[0] ?? "";
|
|
139
|
+
const expected = failure.error.expected;
|
|
140
|
+
const received = failure.error.received;
|
|
141
|
+
return /* @__PURE__ */ jsxs(Box, {
|
|
142
|
+
flexDirection: "column",
|
|
143
|
+
children: [
|
|
144
|
+
/* @__PURE__ */ jsxs(Text, {
|
|
145
|
+
dimColor: true,
|
|
146
|
+
children: [" ", first]
|
|
147
|
+
}),
|
|
148
|
+
expected !== void 0 ? /* @__PURE__ */ jsxs(Text, {
|
|
149
|
+
dimColor: true,
|
|
150
|
+
children: [
|
|
151
|
+
" ",
|
|
152
|
+
"expected: ",
|
|
153
|
+
expected.slice(0, INLINE_VALUE_LIMIT)
|
|
154
|
+
]
|
|
155
|
+
}) : null,
|
|
156
|
+
received !== void 0 ? /* @__PURE__ */ jsxs(Text, {
|
|
157
|
+
dimColor: true,
|
|
158
|
+
children: [
|
|
159
|
+
" ",
|
|
160
|
+
"received: ",
|
|
161
|
+
received.slice(0, INLINE_VALUE_LIMIT)
|
|
162
|
+
]
|
|
163
|
+
}) : null
|
|
164
|
+
]
|
|
165
|
+
});
|
|
166
|
+
};
|
|
167
|
+
const failureKey = (f) => `failure:${f.modulePath}::${f.suitePath.join("/")}::${f.testName}`;
|
|
168
|
+
const testKey = (modulePath, t) => `test:${modulePath}::${t.suitePath.join("/")}::${t.testName}`;
|
|
169
|
+
const failurePath = (f) => [
|
|
170
|
+
f.modulePath,
|
|
171
|
+
...f.suitePath,
|
|
172
|
+
f.testName
|
|
173
|
+
].join(" › ");
|
|
174
|
+
/** A failure entry rendered in the Live region. */
|
|
175
|
+
const FailureItem = ({ failure }) => /* @__PURE__ */ jsxs(Box, {
|
|
176
|
+
flexDirection: "column",
|
|
177
|
+
children: [/* @__PURE__ */ jsxs(Text, { children: [
|
|
178
|
+
" ",
|
|
179
|
+
/* @__PURE__ */ jsx(Text, {
|
|
180
|
+
color: failure.timedOut === true ? "#e09a4e" : "red",
|
|
181
|
+
children: failure.timedOut === true ? "⧖" : "✗"
|
|
182
|
+
}),
|
|
183
|
+
" ",
|
|
184
|
+
failurePath(failure),
|
|
185
|
+
failure.classification !== null ? /* @__PURE__ */ jsxs(Text, {
|
|
186
|
+
color: "#c98ae0",
|
|
187
|
+
children: [
|
|
188
|
+
" [",
|
|
189
|
+
failure.classification,
|
|
190
|
+
"]"
|
|
191
|
+
]
|
|
192
|
+
}) : null
|
|
193
|
+
] }), /* @__PURE__ */ jsx(InlineError, { failure })]
|
|
194
|
+
});
|
|
195
|
+
const TotalsLine = ({ totals }) => /* @__PURE__ */ jsxs(Text, { children: [
|
|
196
|
+
/* @__PURE__ */ jsx(Text, {
|
|
197
|
+
bold: true,
|
|
198
|
+
children: "Total:"
|
|
199
|
+
}),
|
|
200
|
+
" ",
|
|
201
|
+
/* @__PURE__ */ jsx(CountColumns, {
|
|
202
|
+
passCount: totals.passCount,
|
|
203
|
+
failCount: totals.failCount,
|
|
204
|
+
skipCount: totals.skipCount,
|
|
205
|
+
timeoutCount: totals.timeoutCount
|
|
206
|
+
}),
|
|
207
|
+
/* @__PURE__ */ jsxs(Text, {
|
|
208
|
+
dimColor: true,
|
|
209
|
+
children: [" ", formatDisplayDuration(totals.durationMs)]
|
|
210
|
+
})
|
|
211
|
+
] });
|
|
212
|
+
const CoverageItem = ({ state }) => {
|
|
213
|
+
if (state.coverage === null) return null;
|
|
214
|
+
const clean = state.coverage.violations.length === 0;
|
|
215
|
+
return /* @__PURE__ */ jsxs(Text, { children: [
|
|
216
|
+
/* @__PURE__ */ jsx(Text, {
|
|
217
|
+
bold: true,
|
|
218
|
+
children: "Coverage:"
|
|
219
|
+
}),
|
|
220
|
+
" ",
|
|
221
|
+
/* @__PURE__ */ jsx(Text, {
|
|
222
|
+
color: clean ? "green" : "yellow",
|
|
223
|
+
children: clean ? "✓" : "⚠"
|
|
224
|
+
}),
|
|
225
|
+
" ",
|
|
226
|
+
clean ? "all metrics meet thresholds" : `${state.coverage.violations.length} threshold violation(s)`
|
|
227
|
+
] });
|
|
228
|
+
};
|
|
229
|
+
const samePath = (a, b) => {
|
|
230
|
+
if (a.length !== b.length) return false;
|
|
231
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
232
|
+
return true;
|
|
233
|
+
};
|
|
234
|
+
/**
|
|
235
|
+
* Compose the Live region content for the given state + shape. Pure;
|
|
236
|
+
* recomputed every render and every clock tick. Renders the ENTIRE
|
|
237
|
+
* run picture — all rows in discovery order, with running rows capped
|
|
238
|
+
* at MAX_LIVE_RUNNING_ROWS and finished rows always shown.
|
|
239
|
+
*/
|
|
240
|
+
const liveRegion = (state, shape, nowMs, frame) => {
|
|
241
|
+
const groups = groupByProject(state);
|
|
242
|
+
const timedOut = state.phase === "timed-out";
|
|
243
|
+
const finished = state.phase === "finished" || state.phase === "timed-out";
|
|
244
|
+
const ordered = state.moduleOrder.map((p) => state.modules[p]).filter((m) => m !== void 0);
|
|
245
|
+
if (shape === "single-test") {
|
|
246
|
+
const sole = ordered[0];
|
|
247
|
+
if (sole === void 0 || sole.tests.length === 0) return /* @__PURE__ */ jsx(Text, {
|
|
248
|
+
dimColor: true,
|
|
249
|
+
children: "discovering tests…"
|
|
250
|
+
});
|
|
251
|
+
const only = sole.tests[0];
|
|
252
|
+
if (only === void 0) return /* @__PURE__ */ jsx(Text, {
|
|
253
|
+
dimColor: true,
|
|
254
|
+
children: "discovering tests…"
|
|
255
|
+
});
|
|
256
|
+
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;
|
|
257
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(TestRow, {
|
|
258
|
+
test: only,
|
|
259
|
+
indent: 0
|
|
260
|
+
}), failure !== void 0 ? /* @__PURE__ */ jsx(InlineError, { failure }) : null] });
|
|
261
|
+
}
|
|
262
|
+
if (groups.length === 0) return /* @__PURE__ */ jsx(Text, {
|
|
263
|
+
dimColor: true,
|
|
264
|
+
children: "discovering tests…"
|
|
265
|
+
});
|
|
266
|
+
const failuresSection = state.failures.length > 0 ? /* @__PURE__ */ jsx(Box, {
|
|
267
|
+
flexDirection: "column",
|
|
268
|
+
children: state.failures.map((f) => /* @__PURE__ */ jsx(FailureItem, { failure: f }, failureKey(f)))
|
|
269
|
+
}) : null;
|
|
270
|
+
const coverageItem = /* @__PURE__ */ jsx(CoverageItem, { state });
|
|
271
|
+
const trendItem = state.trend !== null ? /* @__PURE__ */ jsx(TrendLine, { trend: state.trend }) : null;
|
|
272
|
+
const totalsItem = /* @__PURE__ */ jsxs(Box, {
|
|
273
|
+
flexDirection: "column",
|
|
274
|
+
children: [finished && timedOut ? /* @__PURE__ */ jsx(Text, {
|
|
275
|
+
color: "#e09a4e",
|
|
276
|
+
bold: true,
|
|
277
|
+
children: "⧖ Run timed out"
|
|
278
|
+
}) : null, /* @__PURE__ */ jsx(TotalsLine, { totals: state.totals })]
|
|
279
|
+
});
|
|
280
|
+
if (shape === "workspace") {
|
|
281
|
+
const nameWidth = Math.max(0, ...groups.map((g) => g.name.length));
|
|
282
|
+
let runningVisible = 0;
|
|
283
|
+
let runningOverflow = 0;
|
|
284
|
+
const rows = [];
|
|
285
|
+
for (const g of groups) if (g.modules.some(moduleRunning)) if (runningVisible < MAX_LIVE_RUNNING_ROWS) {
|
|
286
|
+
runningVisible++;
|
|
287
|
+
const counts = sumCounts(g.modules);
|
|
288
|
+
const summary = {
|
|
289
|
+
name: g.name,
|
|
290
|
+
passCount: counts.passCount,
|
|
291
|
+
failCount: counts.failCount,
|
|
292
|
+
skipCount: counts.skipCount,
|
|
293
|
+
durationMs: 0
|
|
294
|
+
};
|
|
295
|
+
rows.push(/* @__PURE__ */ jsx(ProjectRow, {
|
|
296
|
+
project: summary,
|
|
297
|
+
counts,
|
|
298
|
+
running: true,
|
|
299
|
+
timedOut,
|
|
300
|
+
elapsedMs: projectElapsedMs(g, nowMs),
|
|
301
|
+
frame,
|
|
302
|
+
nameWidth,
|
|
303
|
+
tagCounts: mergeTagCounts(g.modules)
|
|
304
|
+
}, g.name));
|
|
305
|
+
} else runningOverflow++;
|
|
306
|
+
else {
|
|
307
|
+
const counts = sumCounts(g.modules);
|
|
308
|
+
const summary = {
|
|
309
|
+
name: g.name,
|
|
310
|
+
passCount: counts.passCount,
|
|
311
|
+
failCount: counts.failCount,
|
|
312
|
+
skipCount: counts.skipCount,
|
|
313
|
+
durationMs: 0
|
|
314
|
+
};
|
|
315
|
+
rows.push(/* @__PURE__ */ jsx(ProjectRow, {
|
|
316
|
+
project: summary,
|
|
317
|
+
counts,
|
|
318
|
+
running: false,
|
|
319
|
+
timedOut,
|
|
320
|
+
elapsedMs: projectElapsedMs(g, nowMs),
|
|
321
|
+
frame,
|
|
322
|
+
nameWidth,
|
|
323
|
+
tagCounts: mergeTagCounts(g.modules)
|
|
324
|
+
}, g.name));
|
|
325
|
+
}
|
|
326
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
327
|
+
/* @__PURE__ */ jsxs(Text, {
|
|
328
|
+
bold: true,
|
|
329
|
+
children: [
|
|
330
|
+
"Projects (",
|
|
331
|
+
groups.length,
|
|
332
|
+
"):"
|
|
333
|
+
]
|
|
334
|
+
}),
|
|
335
|
+
rows,
|
|
336
|
+
runningOverflow > 0 ? /* @__PURE__ */ jsxs(Text, {
|
|
337
|
+
dimColor: true,
|
|
338
|
+
children: [
|
|
339
|
+
" … and ",
|
|
340
|
+
runningOverflow,
|
|
341
|
+
" more running"
|
|
342
|
+
]
|
|
343
|
+
}) : null,
|
|
344
|
+
failuresSection,
|
|
345
|
+
coverageItem,
|
|
346
|
+
trendItem,
|
|
347
|
+
totalsItem
|
|
348
|
+
] });
|
|
349
|
+
}
|
|
350
|
+
if (shape === "single-project") {
|
|
351
|
+
const nameWidth = Math.max(0, ...ordered.map((m) => m.modulePath.length));
|
|
352
|
+
let runningVisible = 0;
|
|
353
|
+
let runningOverflow = 0;
|
|
354
|
+
const rows = [];
|
|
355
|
+
for (const m of ordered) if (moduleRunning(m)) if (runningVisible < MAX_LIVE_RUNNING_ROWS) {
|
|
356
|
+
runningVisible++;
|
|
357
|
+
rows.push(/* @__PURE__ */ jsx(ModuleStreamRow, {
|
|
358
|
+
module: m,
|
|
359
|
+
nowMs,
|
|
360
|
+
frame,
|
|
361
|
+
nameWidth,
|
|
362
|
+
timedOut
|
|
363
|
+
}, m.modulePath));
|
|
364
|
+
} else runningOverflow++;
|
|
365
|
+
else rows.push(/* @__PURE__ */ jsx(ModuleStreamRow, {
|
|
366
|
+
module: m,
|
|
367
|
+
nowMs,
|
|
368
|
+
frame,
|
|
369
|
+
nameWidth,
|
|
370
|
+
timedOut
|
|
371
|
+
}, m.modulePath));
|
|
372
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
373
|
+
/* @__PURE__ */ jsxs(Text, {
|
|
374
|
+
bold: true,
|
|
375
|
+
children: [
|
|
376
|
+
"Modules (",
|
|
377
|
+
ordered.length,
|
|
378
|
+
"):"
|
|
379
|
+
]
|
|
380
|
+
}),
|
|
381
|
+
rows,
|
|
382
|
+
runningOverflow > 0 ? /* @__PURE__ */ jsxs(Text, {
|
|
383
|
+
dimColor: true,
|
|
384
|
+
children: [
|
|
385
|
+
" … and ",
|
|
386
|
+
runningOverflow,
|
|
387
|
+
" more running"
|
|
388
|
+
]
|
|
389
|
+
}) : null,
|
|
390
|
+
failuresSection,
|
|
391
|
+
coverageItem,
|
|
392
|
+
trendItem,
|
|
393
|
+
totalsItem
|
|
394
|
+
] });
|
|
395
|
+
}
|
|
396
|
+
const sole = ordered[0];
|
|
397
|
+
if (sole === void 0) return /* @__PURE__ */ jsx(Text, {
|
|
398
|
+
dimColor: true,
|
|
399
|
+
children: "discovering tests…"
|
|
400
|
+
});
|
|
401
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
402
|
+
/* @__PURE__ */ jsxs(Text, { children: [/* @__PURE__ */ jsx(Text, {
|
|
403
|
+
bold: true,
|
|
404
|
+
children: sole.modulePath
|
|
405
|
+
}), /* @__PURE__ */ jsxs(Text, {
|
|
406
|
+
dimColor: true,
|
|
407
|
+
children: [
|
|
408
|
+
" — ",
|
|
409
|
+
sole.tests.length,
|
|
410
|
+
" tests"
|
|
411
|
+
]
|
|
412
|
+
})] }),
|
|
413
|
+
sole.tests.map((t) => {
|
|
414
|
+
const failure = t.status === "failed" || t.status === "timed-out" ? state.failures.find((f) => f.testName === t.testName && f.modulePath === sole.modulePath && samePath(f.suitePath, t.suitePath)) : void 0;
|
|
415
|
+
return /* @__PURE__ */ jsxs(Box, {
|
|
416
|
+
flexDirection: "column",
|
|
417
|
+
children: [/* @__PURE__ */ jsx(TestRow, {
|
|
418
|
+
test: t,
|
|
419
|
+
indent: 2
|
|
420
|
+
}), failure !== void 0 ? /* @__PURE__ */ jsx(InlineError, { failure }) : null]
|
|
421
|
+
}, testKey(sole.modulePath, t));
|
|
422
|
+
}),
|
|
423
|
+
coverageItem,
|
|
424
|
+
trendItem,
|
|
425
|
+
totalsItem
|
|
426
|
+
] });
|
|
427
|
+
};
|
|
428
|
+
/**
|
|
429
|
+
* Root Ink component for the `stream` live renderer. Classifies the run
|
|
430
|
+
* shape and renders the appropriate per-shape live region.
|
|
431
|
+
*
|
|
432
|
+
* @public
|
|
433
|
+
*/
|
|
434
|
+
const StreamApp = ({ state, frameIndex, nowMs }) => {
|
|
435
|
+
const now = nowMs ?? Date.now();
|
|
436
|
+
const frame = spinnerFrame(frameIndex);
|
|
437
|
+
return /* @__PURE__ */ jsx(Box, {
|
|
438
|
+
flexDirection: "column",
|
|
439
|
+
children: liveRegion(state, classifyRunShape(state, groupByProject(state).map((g) => {
|
|
440
|
+
const c = sumCounts(g.modules);
|
|
441
|
+
return {
|
|
442
|
+
name: g.name,
|
|
443
|
+
passCount: c.passCount,
|
|
444
|
+
failCount: c.failCount,
|
|
445
|
+
skipCount: c.skipCount,
|
|
446
|
+
durationMs: 0
|
|
447
|
+
};
|
|
448
|
+
})), now, frame)
|
|
449
|
+
});
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
//#endregion
|
|
453
|
+
export { StreamApp };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Box, Text } from "ink";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
|
|
4
|
+
//#region src/render-ink/SuggestedActions.tsx
|
|
5
|
+
const SEVERITY_COLOR = {
|
|
6
|
+
info: "blue",
|
|
7
|
+
warn: "yellow",
|
|
8
|
+
blocker: "red"
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Renders the suggested-actions queue as severity-prefixed rows with
|
|
12
|
+
* optional tool hints. Returns `null` when the actions list is empty.
|
|
13
|
+
*
|
|
14
|
+
* @public
|
|
15
|
+
*/
|
|
16
|
+
const SuggestedActions = ({ actions }) => {
|
|
17
|
+
if (actions.length === 0) return null;
|
|
18
|
+
return /* @__PURE__ */ jsxs(Box, {
|
|
19
|
+
flexDirection: "column",
|
|
20
|
+
children: [/* @__PURE__ */ jsx(Text, {
|
|
21
|
+
bold: true,
|
|
22
|
+
children: "Actions"
|
|
23
|
+
}), actions.map((action, idx) => /* @__PURE__ */ jsxs(Box, {
|
|
24
|
+
flexDirection: "column",
|
|
25
|
+
children: [/* @__PURE__ */ jsxs(Box, { children: [
|
|
26
|
+
/* @__PURE__ */ jsx(Text, {
|
|
27
|
+
color: SEVERITY_COLOR[action.severity],
|
|
28
|
+
bold: true,
|
|
29
|
+
children: ` ${action.severity}: `
|
|
30
|
+
}),
|
|
31
|
+
/* @__PURE__ */ jsx(Text, { children: action.title }),
|
|
32
|
+
action.targetTool !== void 0 ? /* @__PURE__ */ jsxs(Text, {
|
|
33
|
+
dimColor: true,
|
|
34
|
+
children: [
|
|
35
|
+
" (tool: ",
|
|
36
|
+
action.targetTool,
|
|
37
|
+
")"
|
|
38
|
+
]
|
|
39
|
+
}) : null
|
|
40
|
+
] }), /* @__PURE__ */ jsx(Text, {
|
|
41
|
+
dimColor: true,
|
|
42
|
+
children: ` ${action.detail}`
|
|
43
|
+
})]
|
|
44
|
+
}, `${action.severity}-${idx}-${action.title}`))]
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
//#endregion
|
|
49
|
+
export { SuggestedActions };
|
|
@@ -0,0 +1,33 @@
|
|
|
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/TestRow.tsx
|
|
7
|
+
const testGlyph = (status) => status;
|
|
8
|
+
/**
|
|
9
|
+
* Renders one test row: status glyph, optional suite prefix, test name, and duration.
|
|
10
|
+
*
|
|
11
|
+
* @public
|
|
12
|
+
*/
|
|
13
|
+
const TestRow = ({ test, indent = 2 }) => {
|
|
14
|
+
const pad = " ".repeat(indent);
|
|
15
|
+
const suite = test.suitePath.length > 0 ? `${test.suitePath.join(" > ")} > ` : "";
|
|
16
|
+
const duration = test.durationMs !== null ? ` (${formatDisplayDuration(test.durationMs)})` : "";
|
|
17
|
+
return /* @__PURE__ */ jsxs(Box, { children: [
|
|
18
|
+
/* @__PURE__ */ jsx(Text, { children: pad }),
|
|
19
|
+
/* @__PURE__ */ jsx(StatusIcon, { status: testGlyph(test.status) }),
|
|
20
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
21
|
+
" ",
|
|
22
|
+
suite,
|
|
23
|
+
test.testName
|
|
24
|
+
] }),
|
|
25
|
+
/* @__PURE__ */ jsx(Text, {
|
|
26
|
+
dimColor: true,
|
|
27
|
+
children: duration
|
|
28
|
+
})
|
|
29
|
+
] });
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
//#endregion
|
|
33
|
+
export { TestRow };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Text } from "ink";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
|
|
4
|
+
//#region src/render-ink/TrendLine.tsx
|
|
5
|
+
/**
|
|
6
|
+
* The one-line trend signal — `Trend: <direction> (<N> runs)`.
|
|
7
|
+
*/
|
|
8
|
+
const COLOR = {
|
|
9
|
+
improving: "green",
|
|
10
|
+
regressing: "red",
|
|
11
|
+
stable: "gray"
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Renders the one-line trend signal: direction (colored) and run count.
|
|
15
|
+
*
|
|
16
|
+
* @public
|
|
17
|
+
*/
|
|
18
|
+
const TrendLine = ({ trend }) => {
|
|
19
|
+
const runs = trend.runCount === 1 ? "1 run" : `${trend.runCount} runs`;
|
|
20
|
+
return /* @__PURE__ */ jsxs(Text, { children: [
|
|
21
|
+
/* @__PURE__ */ jsx(Text, {
|
|
22
|
+
bold: true,
|
|
23
|
+
children: "Trend:"
|
|
24
|
+
}),
|
|
25
|
+
" ",
|
|
26
|
+
/* @__PURE__ */ jsx(Text, {
|
|
27
|
+
color: COLOR[trend.direction],
|
|
28
|
+
children: trend.direction
|
|
29
|
+
}),
|
|
30
|
+
/* @__PURE__ */ jsxs(Text, {
|
|
31
|
+
dimColor: true,
|
|
32
|
+
children: [
|
|
33
|
+
" (",
|
|
34
|
+
runs,
|
|
35
|
+
")"
|
|
36
|
+
]
|
|
37
|
+
})
|
|
38
|
+
] });
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
//#endregion
|
|
42
|
+
export { TrendLine };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { CountColumns } from "./CountColumns.js";
|
|
2
|
+
import { CoverageBlock } from "./CoverageBlock.js";
|
|
3
|
+
import { FailureSection } from "./FailureSection.js";
|
|
4
|
+
import { FailuresSection } from "./FailuresSection.js";
|
|
5
|
+
import { StatusIcon } from "./StatusIcon.js";
|
|
6
|
+
import { ModuleHeader } from "./ModuleHeader.js";
|
|
7
|
+
import { ProjectRow } from "./ProjectRow.js";
|
|
8
|
+
import { SPINNER_FRAMES, SPINNER_FRAME_MS, spinnerFrame, spinnerFrameForTime } from "./spinner.js";
|
|
9
|
+
import { TestRow } from "./TestRow.js";
|
|
10
|
+
import { TrendLine } from "./TrendLine.js";
|
|
11
|
+
import { StreamApp } from "./StreamApp.js";
|
|
12
|
+
import { SuggestedActions } from "./SuggestedActions.js";
|
|
13
|
+
|
|
14
|
+
export { };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
//#region src/render-ink/spinner.ts
|
|
2
|
+
/**
|
|
3
|
+
* Hand-rolled Braille spinner for the `stream` live renderer.
|
|
4
|
+
*
|
|
5
|
+
* No `ink-spinner` dependency — that package is a thin wrapper over the
|
|
6
|
+
* same ten Braille characters plus a timer. The `stream` renderer
|
|
7
|
+
* already needs a frame clock for the ticking elapsed column, so the
|
|
8
|
+
* timer is shared and only the frame array lives here.
|
|
9
|
+
*
|
|
10
|
+
* The frame index is presentation state: it is derived from wall-clock
|
|
11
|
+
* time by `createLiveInk` and passed to `StreamApp` as a prop. It never
|
|
12
|
+
* enters the event-sourced `RenderState`.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* The ten Braille spinner frames, in animation order.
|
|
16
|
+
*
|
|
17
|
+
* @public
|
|
18
|
+
*/
|
|
19
|
+
const SPINNER_FRAMES = [
|
|
20
|
+
"⠋",
|
|
21
|
+
"⠙",
|
|
22
|
+
"⠹",
|
|
23
|
+
"⠸",
|
|
24
|
+
"⠼",
|
|
25
|
+
"⠴",
|
|
26
|
+
"⠦",
|
|
27
|
+
"⠧",
|
|
28
|
+
"⠇",
|
|
29
|
+
"⠏"
|
|
30
|
+
];
|
|
31
|
+
/**
|
|
32
|
+
* How long each spinner frame is held, in milliseconds.
|
|
33
|
+
*
|
|
34
|
+
* @public
|
|
35
|
+
*/
|
|
36
|
+
const SPINNER_FRAME_MS = 80;
|
|
37
|
+
/**
|
|
38
|
+
* Resolve the spinner glyph for a frame index. The index wraps modulo
|
|
39
|
+
* the frame count and tolerates negative values, so a wall-clock-derived
|
|
40
|
+
* index is always valid.
|
|
41
|
+
*
|
|
42
|
+
* @param index - the frame index (wraps modulo frame count)
|
|
43
|
+
* @returns the Braille glyph for the given frame
|
|
44
|
+
* @public
|
|
45
|
+
*/
|
|
46
|
+
const spinnerFrame = (index) => {
|
|
47
|
+
const count = SPINNER_FRAMES.length;
|
|
48
|
+
return SPINNER_FRAMES[(Math.trunc(index) % count + count) % count];
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Derive the spinner frame index from a wall-clock timestamp. Using the
|
|
52
|
+
* clock — rather than a monotonic counter — keeps the animation correct
|
|
53
|
+
* across watch-mode remounts with no extra closure state to reset.
|
|
54
|
+
*
|
|
55
|
+
* @param nowMs - current wall-clock time in milliseconds
|
|
56
|
+
* @returns the frame index for the given timestamp
|
|
57
|
+
* @public
|
|
58
|
+
*/
|
|
59
|
+
const spinnerFrameForTime = (nowMs) => Math.floor(nowMs / 80);
|
|
60
|
+
|
|
61
|
+
//#endregion
|
|
62
|
+
export { SPINNER_FRAMES, SPINNER_FRAME_MS, spinnerFrame, spinnerFrameForTime };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//#region src/render-ink/tag-suffix.ts
|
|
2
|
+
/**
|
|
3
|
+
* Format a per-row tag-count suffix — sorted `tag:count` pairs joined
|
|
4
|
+
* by two spaces. A single tag is suppressed: a project that is all one
|
|
5
|
+
* kind carries no signal worth the column, matching the agent
|
|
6
|
+
* renderer's behaviour.
|
|
7
|
+
*
|
|
8
|
+
* @packageDocumentation
|
|
9
|
+
*/
|
|
10
|
+
const formatTagSuffix = (tagCounts) => {
|
|
11
|
+
if (tagCounts === void 0) return "";
|
|
12
|
+
const entries = Object.entries(tagCounts);
|
|
13
|
+
if (entries.length <= 1) return "";
|
|
14
|
+
return [...entries].sort(([a], [b]) => a.localeCompare(b)).map(([tag, count]) => `${tag}:${count}`).join(" ");
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
//#endregion
|
|
18
|
+
export { formatTagSuffix };
|