@elench/testkit 0.1.78 → 0.1.79
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/lib/cli/presentation/colors.mjs +21 -4
- package/lib/cli/presentation/discovery-reporter.mjs +33 -12
- package/lib/cli/presentation/run-reporter.mjs +90 -10
- package/lib/cli/presentation/run-reporter.test.mjs +3 -2
- package/lib/runner/orchestrator.mjs +1 -0
- package/node_modules/@elench/next-analysis/package.json +1 -1
- package/node_modules/@elench/testkit-bridge/package.json +2 -2
- package/node_modules/@elench/testkit-protocol/package.json +1 -1
- package/node_modules/@elench/ts-analysis/package.json +1 -1
- package/package.json +6 -5
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
import { createColors } from "picocolors";
|
|
2
|
+
import figures from "figures";
|
|
2
3
|
|
|
3
4
|
const pc = createColors(Boolean(process.stdout?.isTTY || process.env.FORCE_COLOR));
|
|
4
5
|
|
|
5
6
|
export function colorStatus(status) {
|
|
6
|
-
if (status === "PASS") return pc.green(
|
|
7
|
-
if (status === "FAIL") return pc.red(
|
|
8
|
-
if (status === "SKIP") return pc.yellow(
|
|
9
|
-
if (status === "RUN") return pc.cyan(
|
|
7
|
+
if (status === "PASS") return pc.green(figures.tick);
|
|
8
|
+
if (status === "FAIL") return pc.red(figures.cross);
|
|
9
|
+
if (status === "SKIP") return pc.yellow(figures.arrowDown);
|
|
10
|
+
if (status === "RUN") return pc.cyan(figures.play);
|
|
11
|
+
return status;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function statusLabel(status) {
|
|
15
|
+
if (status === "PASS") return pc.green(`${figures.tick} PASS`);
|
|
16
|
+
if (status === "FAIL") return pc.red(`${figures.cross} FAIL`);
|
|
17
|
+
if (status === "SKIP") return pc.yellow(`${figures.arrowDown} SKIP`);
|
|
18
|
+
if (status === "RUN") return pc.cyan(`${figures.play} RUN`);
|
|
10
19
|
return status;
|
|
11
20
|
}
|
|
12
21
|
|
|
@@ -18,6 +27,14 @@ export function bold(text) {
|
|
|
18
27
|
return pc.bold(text);
|
|
19
28
|
}
|
|
20
29
|
|
|
30
|
+
export function boldRed(text) {
|
|
31
|
+
return pc.bold(pc.red(text));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function red(text) {
|
|
35
|
+
return pc.red(text);
|
|
36
|
+
}
|
|
37
|
+
|
|
21
38
|
export function muted(text) {
|
|
22
39
|
return pc.dim(text);
|
|
23
40
|
}
|
|
@@ -5,13 +5,18 @@ import {
|
|
|
5
5
|
colorDiagnosticSeverity,
|
|
6
6
|
colorHeading,
|
|
7
7
|
colorService,
|
|
8
|
-
colorStatus,
|
|
9
8
|
colorTypeBadge,
|
|
10
9
|
muted,
|
|
10
|
+
statusLabel,
|
|
11
11
|
} from "./colors.mjs";
|
|
12
12
|
|
|
13
13
|
const TYPE_ORDER = ["int", "e2e", "scenario", "dal", "load", "pw"];
|
|
14
14
|
|
|
15
|
+
const TREE_BRANCH = "\u251C\u2500\u2500 ";
|
|
16
|
+
const TREE_LAST = "\u2514\u2500\u2500 ";
|
|
17
|
+
const TREE_PIPE = "\u2502 ";
|
|
18
|
+
const TREE_SPACE = " ";
|
|
19
|
+
|
|
15
20
|
export function buildDiscoveryReportLines(result, options = {}) {
|
|
16
21
|
const mode = options.outputMode || "compact";
|
|
17
22
|
return mode === "verbose" ? buildVerboseLines(result) : buildCompactLines(result);
|
|
@@ -20,7 +25,7 @@ export function buildDiscoveryReportLines(result, options = {}) {
|
|
|
20
25
|
function buildCompactLines(result) {
|
|
21
26
|
const lines = [];
|
|
22
27
|
lines.push(
|
|
23
|
-
`${colorHeading("Summary")} ${result.summary.files} files
|
|
28
|
+
`${colorHeading("Summary")} ${result.summary.files} files \u00B7 ${result.summary.activeFiles} active \u00B7 ${result.summary.skippedFiles} skipped \u00B7 ${result.summary.suites} suites \u00B7 ${result.summary.services} services`
|
|
24
29
|
);
|
|
25
30
|
|
|
26
31
|
if (result.history?.available) {
|
|
@@ -35,17 +40,33 @@ function buildCompactLines(result) {
|
|
|
35
40
|
for (const service of result.services) {
|
|
36
41
|
lines.push("");
|
|
37
42
|
lines.push(
|
|
38
|
-
`${colorService(service.name)} ${muted(`(${service.fileCount} files${service.dependsOn.length > 0 ? `
|
|
43
|
+
`${colorService(service.name)} ${muted(`(${service.fileCount} files${service.dependsOn.length > 0 ? ` \u00B7 depends on ${service.dependsOn.join(", ")}` : ""})`)}`
|
|
39
44
|
);
|
|
40
45
|
const typeGroups = suitesByService.get(service.name) || new Map();
|
|
41
|
-
|
|
46
|
+
const types = orderedTypes([...typeGroups.keys()]);
|
|
47
|
+
|
|
48
|
+
for (let ti = 0; ti < types.length; ti++) {
|
|
49
|
+
const type = types[ti];
|
|
50
|
+
const isLastType = ti === types.length - 1;
|
|
51
|
+
const typeConnector = isLastType ? TREE_LAST : TREE_BRANCH;
|
|
52
|
+
const typePrefix = isLastType ? TREE_SPACE : TREE_PIPE;
|
|
42
53
|
const suites = typeGroups.get(type) || [];
|
|
43
|
-
lines.push(
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
54
|
+
lines.push(`${typeConnector}${colorTypeBadge(type.toUpperCase())} ${formatSelectionTypeLabel(type)}`);
|
|
55
|
+
|
|
56
|
+
for (let si = 0; si < suites.length; si++) {
|
|
57
|
+
const suite = suites[si];
|
|
58
|
+
const isLastSuite = si === suites.length - 1;
|
|
59
|
+
const suiteConnector = isLastSuite ? TREE_LAST : TREE_BRANCH;
|
|
60
|
+
const suitePrefix = isLastSuite ? TREE_SPACE : TREE_PIPE;
|
|
61
|
+
lines.push(`${typePrefix}${suiteConnector}${bold(suite.groupLabel)} ${muted(`(${suite.fileCount} files)`)}`);
|
|
62
|
+
|
|
63
|
+
const files = filesBySuite.get(suite.id) || [];
|
|
64
|
+
for (let fi = 0; fi < files.length; fi++) {
|
|
65
|
+
const file = files[fi];
|
|
66
|
+
const isLastFile = fi === files.length - 1;
|
|
67
|
+
const fileConnector = isLastFile ? TREE_LAST : TREE_BRANCH;
|
|
68
|
+
const status = file.skipped ? `${statusLabel("SKIP")} ${file.skipReason}` : muted(buildHistoryHint(file));
|
|
69
|
+
lines.push(`${typePrefix}${suitePrefix}${fileConnector}${file.displayName}${status ? ` ${status}` : ""}`);
|
|
49
70
|
}
|
|
50
71
|
}
|
|
51
72
|
}
|
|
@@ -73,7 +94,7 @@ function buildVerboseLines(result) {
|
|
|
73
94
|
if (service.dependsOn.length > 0) {
|
|
74
95
|
lines.push(` dependsOn ${service.dependsOn.join(", ")}`);
|
|
75
96
|
}
|
|
76
|
-
lines.push(` files ${service.fileCount}
|
|
97
|
+
lines.push(` files ${service.fileCount} \u00B7 active ${service.activeFileCount} \u00B7 skipped ${service.skippedFileCount}`);
|
|
77
98
|
|
|
78
99
|
const suites = result.suites.filter((suite) => suite.service === service.name).sort(compareSuites);
|
|
79
100
|
for (const suite of suites) {
|
|
@@ -162,5 +183,5 @@ function buildHistoryHint(file) {
|
|
|
162
183
|
file.history.firstSeenAt ? `seen ${file.history.firstSeenAt.slice(0, 10)}` : null,
|
|
163
184
|
]
|
|
164
185
|
.filter(Boolean)
|
|
165
|
-
.join("
|
|
186
|
+
.join(" \u00B7 ");
|
|
166
187
|
}
|
|
@@ -1,15 +1,89 @@
|
|
|
1
1
|
import path from "path";
|
|
2
|
+
import figures from "figures";
|
|
2
3
|
import {
|
|
3
4
|
buildCompactRunSummaryLines,
|
|
4
5
|
buildDebugRunSummaryLines,
|
|
5
6
|
formatDuration,
|
|
6
7
|
} from "../../runner/formatting.mjs";
|
|
7
|
-
import {
|
|
8
|
+
import { boldRed, colorSectionLine, dim, red, statusLabel } from "./colors.mjs";
|
|
9
|
+
|
|
10
|
+
const ANSI_RE = /\x1b\[[0-9;]*m/g;
|
|
11
|
+
const DURATION_RE = /\b(\d+m\s+\d+s|\d+s)\b/g;
|
|
12
|
+
|
|
13
|
+
function stripAnsi(text) {
|
|
14
|
+
return text.replace(ANSI_RE, "");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function boxLines(lines) {
|
|
18
|
+
const nonEmpty = lines.filter((l) => stripAnsi(l).trim().length > 0);
|
|
19
|
+
if (nonEmpty.length === 0) return lines;
|
|
20
|
+
const maxWidth = lines.reduce((max, l) => Math.max(max, stripAnsi(l).length), 0);
|
|
21
|
+
const pad = 1;
|
|
22
|
+
const innerWidth = maxWidth + pad * 2;
|
|
23
|
+
const top = `${figures.lineDownRight}${figures.line.repeat(innerWidth)}${figures.lineDownLeft}`;
|
|
24
|
+
const bottom = `${figures.lineUpRight}${figures.line.repeat(innerWidth)}${figures.lineUpLeft}`;
|
|
25
|
+
const boxed = [top];
|
|
26
|
+
for (const line of lines) {
|
|
27
|
+
const visible = stripAnsi(line).length;
|
|
28
|
+
const right = innerWidth - pad - visible;
|
|
29
|
+
boxed.push(`${figures.lineVertical}${" ".repeat(pad)}${line}${" ".repeat(Math.max(0, right))}${figures.lineVertical}`);
|
|
30
|
+
}
|
|
31
|
+
boxed.push(bottom);
|
|
32
|
+
return boxed;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function colorFailureSummaryLines(lines) {
|
|
36
|
+
const result = [];
|
|
37
|
+
let inFailuresSection = false;
|
|
38
|
+
let lastWasFilePath = false;
|
|
39
|
+
for (const line of lines) {
|
|
40
|
+
const raw = stripAnsi(line);
|
|
41
|
+
if (raw === "Failures:" || raw === "Runtime Errors:") {
|
|
42
|
+
inFailuresSection = true;
|
|
43
|
+
lastWasFilePath = false;
|
|
44
|
+
result.push(line);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (/^(Summary:|Result:|Known-failure|Triage:)/.test(raw) || raw === "") {
|
|
48
|
+
inFailuresSection = false;
|
|
49
|
+
lastWasFilePath = false;
|
|
50
|
+
}
|
|
51
|
+
if (inFailuresSection) {
|
|
52
|
+
if (/^ {2}\S/.test(raw)) {
|
|
53
|
+
lastWasFilePath = true;
|
|
54
|
+
result.push(boldRed(raw));
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (/^ {4}\S/.test(raw)) {
|
|
58
|
+
if (lastWasFilePath) {
|
|
59
|
+
lastWasFilePath = false;
|
|
60
|
+
result.push(boldRed(raw));
|
|
61
|
+
} else {
|
|
62
|
+
result.push(red(raw));
|
|
63
|
+
}
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
lastWasFilePath = false;
|
|
68
|
+
result.push(line);
|
|
69
|
+
}
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function dimDurations(line) {
|
|
74
|
+
return line.replace(DURATION_RE, (match) => dim(match));
|
|
75
|
+
}
|
|
8
76
|
|
|
9
77
|
export function createRunReporter({ outputMode = "compact", stdout = process.stdout, stderr = process.stderr } = {}) {
|
|
10
78
|
const mode = outputMode || "compact";
|
|
79
|
+
let completedCount = 0;
|
|
80
|
+
let totalFileCount = 0;
|
|
81
|
+
|
|
11
82
|
return {
|
|
12
83
|
outputMode: mode,
|
|
84
|
+
setTotalFileCount(count) {
|
|
85
|
+
totalFileCount = count;
|
|
86
|
+
},
|
|
13
87
|
writeLine(line = "") {
|
|
14
88
|
stdout.write(`${line}\n`);
|
|
15
89
|
},
|
|
@@ -34,7 +108,7 @@ export function createRunReporter({ outputMode = "compact", stdout = process.std
|
|
|
34
108
|
if (operation.status === "failed") {
|
|
35
109
|
const detail = shortenMessage(operation.error || operation.summary || operation.stage);
|
|
36
110
|
stdout.write(
|
|
37
|
-
`${
|
|
111
|
+
`${statusLabel("FAIL")} ${"SETUP"} ${operation.serviceName} ${operation.stage} ${dim(duration)} ${boldRed(detail)}\n`
|
|
38
112
|
);
|
|
39
113
|
return;
|
|
40
114
|
}
|
|
@@ -44,7 +118,7 @@ export function createRunReporter({ outputMode = "compact", stdout = process.std
|
|
|
44
118
|
(operation.durationMs || 0) >= 5_000
|
|
45
119
|
) {
|
|
46
120
|
const summary = shortenMessage(operation.summary || operation.stage);
|
|
47
|
-
stdout.write(`${
|
|
121
|
+
stdout.write(`${statusLabel("RUN")} ${"SETUP"} ${operation.serviceName} ${summary} ${dim(duration)}\n`);
|
|
48
122
|
}
|
|
49
123
|
},
|
|
50
124
|
localServiceStarting(config, command) {
|
|
@@ -52,19 +126,20 @@ export function createRunReporter({ outputMode = "compact", stdout = process.std
|
|
|
52
126
|
stdout.write(`Starting ${config.runtimeLabel}:${config.name}: ${command}\n`);
|
|
53
127
|
},
|
|
54
128
|
serviceSkipped(config, reason) {
|
|
55
|
-
stdout.write(`${
|
|
129
|
+
stdout.write(`${statusLabel("SKIP")} ${config.name} ${reason}\n`);
|
|
56
130
|
},
|
|
57
131
|
plannedSkip(entry) {
|
|
58
132
|
stdout.write(
|
|
59
|
-
`${
|
|
133
|
+
`${statusLabel("SKIP")} ${entry.serviceName} ${entry.type} ${normalizePath(entry.file)} ${dim("0s")} ${shortenMessage(entry.reason || "skipped")}\n`
|
|
60
134
|
);
|
|
61
135
|
},
|
|
62
136
|
taskStarted(task, targetConfig) {
|
|
63
137
|
if (mode !== "debug") return;
|
|
64
|
-
stdout.write(`${
|
|
138
|
+
stdout.write(`${statusLabel("RUN")} ${targetConfig.name} ${task.type} ${task.file}\n`);
|
|
65
139
|
},
|
|
66
140
|
taskFinished(task, outcome) {
|
|
67
141
|
if (mode === "json") return;
|
|
142
|
+
completedCount += 1;
|
|
68
143
|
const status = outcome.status === "skipped" ? "SKIP" : outcome.failed ? "FAIL" : "PASS";
|
|
69
144
|
const duration = formatDuration(outcome.durationMs || 0);
|
|
70
145
|
const primaryFailure = firstFailureDetail(outcome);
|
|
@@ -72,12 +147,13 @@ export function createRunReporter({ outputMode = "compact", stdout = process.std
|
|
|
72
147
|
primaryFailure && isThresholdWrapperMessage(outcome.error) ? primaryFailure : outcome.error || primaryFailure;
|
|
73
148
|
const detail =
|
|
74
149
|
status === "FAIL"
|
|
75
|
-
? ` ${shortenMessage(preferredFailure || "failed")}`
|
|
150
|
+
? ` ${boldRed(shortenMessage(preferredFailure || "failed"))}`
|
|
76
151
|
: outcome.status === "not_run"
|
|
77
|
-
? ` ${shortenMessage(outcome.reason || "not run")}`
|
|
152
|
+
? ` ${dim(shortenMessage(outcome.reason || "not run"))}`
|
|
78
153
|
: "";
|
|
154
|
+
const progress = mode === "compact" && totalFileCount > 0 ? `${dim(`[${completedCount}/${totalFileCount}]`)} ` : "";
|
|
79
155
|
stdout.write(
|
|
80
|
-
`${
|
|
156
|
+
`${progress}${statusLabel(status)} ${task.serviceName} ${displayTaskType(task)} ${normalizePath(task.file)} ${dim(duration)}${detail}\n`
|
|
81
157
|
);
|
|
82
158
|
},
|
|
83
159
|
telemetry(message) {
|
|
@@ -89,7 +165,11 @@ export function createRunReporter({ outputMode = "compact", stdout = process.std
|
|
|
89
165
|
mode === "debug"
|
|
90
166
|
? buildDebugRunSummaryLines(results, durationMs, knownFailureIssueValidation)
|
|
91
167
|
: buildCompactRunSummaryLines(results, durationMs, knownFailureIssueValidation);
|
|
92
|
-
|
|
168
|
+
const colored = colorFailureSummaryLines(lines.map((line) => colorSectionLine(line)));
|
|
169
|
+
const dimmed = colored.map((line) => dimDurations(line));
|
|
170
|
+
const boxed = boxLines(dimmed);
|
|
171
|
+
stdout.write("\n");
|
|
172
|
+
for (const line of boxed) stdout.write(`${line}\n`);
|
|
93
173
|
},
|
|
94
174
|
error(message) {
|
|
95
175
|
stderr.write(`${message}\n`);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Writable } from "stream";
|
|
2
|
+
import figures from "figures";
|
|
2
3
|
import { describe, expect, it } from "vitest";
|
|
3
4
|
import { createRunReporter } from "./run-reporter.mjs";
|
|
4
5
|
|
|
@@ -24,7 +25,7 @@ describe("run reporter setup output", () => {
|
|
|
24
25
|
durationMs: 8_000,
|
|
25
26
|
});
|
|
26
27
|
|
|
27
|
-
expect(stdout).toContain(
|
|
28
|
+
expect(stdout).toContain(`${figures.play} RUN SETUP api template rebuild`);
|
|
28
29
|
expect(stdout).toContain("8s");
|
|
29
30
|
});
|
|
30
31
|
|
|
@@ -74,7 +75,7 @@ describe("run reporter setup output", () => {
|
|
|
74
75
|
error: "Command failed with exit code 1: node scripts/fail-prepare.mjs",
|
|
75
76
|
});
|
|
76
77
|
|
|
77
|
-
expect(stdout).toContain(
|
|
78
|
+
expect(stdout).toContain(`${figures.cross} FAIL SETUP api runtime:prepare`);
|
|
78
79
|
expect(stdout).toContain("Command failed with exit code 1");
|
|
79
80
|
});
|
|
80
81
|
});
|
|
@@ -121,6 +121,7 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
|
|
|
121
121
|
const timings = loadTimings(productDir);
|
|
122
122
|
const graphs = buildRuntimeGraphs(executedPlans);
|
|
123
123
|
const queue = buildTaskQueue(executedPlans, graphs, timings);
|
|
124
|
+
reporter?.setTotalFileCount?.(queue.length);
|
|
124
125
|
for (const task of queue) {
|
|
125
126
|
task.scenarioSeed = opts.scenarioSeed || null;
|
|
126
127
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elench/testkit-bridge",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.79",
|
|
4
4
|
"description": "Browser bridge helpers for testkit",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@elench/testkit-protocol": "0.1.
|
|
25
|
+
"@elench/testkit-protocol": "0.1.79"
|
|
26
26
|
},
|
|
27
27
|
"private": false
|
|
28
28
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elench/testkit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.79",
|
|
4
4
|
"description": "CLI for discovering and running local HTTP, DAL, and Playwright test suites",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"workspaces": [
|
|
@@ -81,13 +81,14 @@
|
|
|
81
81
|
},
|
|
82
82
|
"dependencies": {
|
|
83
83
|
"@babel/code-frame": "^7.29.0",
|
|
84
|
-
"@elench/next-analysis": "0.1.
|
|
85
|
-
"@elench/testkit-bridge": "0.1.
|
|
86
|
-
"@elench/testkit-protocol": "0.1.
|
|
87
|
-
"@elench/ts-analysis": "0.1.
|
|
84
|
+
"@elench/next-analysis": "0.1.79",
|
|
85
|
+
"@elench/testkit-bridge": "0.1.79",
|
|
86
|
+
"@elench/testkit-protocol": "0.1.79",
|
|
87
|
+
"@elench/ts-analysis": "0.1.79",
|
|
88
88
|
"@oclif/core": "^4.10.6",
|
|
89
89
|
"esbuild": "^0.25.11",
|
|
90
90
|
"execa": "^9.5.0",
|
|
91
|
+
"figures": "^6.1.0",
|
|
91
92
|
"ink": "^7.0.1",
|
|
92
93
|
"picocolors": "^1.1.1",
|
|
93
94
|
"react": "^19.2.5",
|