@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.
@@ -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(status);
7
- if (status === "FAIL") return pc.red(status);
8
- if (status === "SKIP") return pc.yellow(status);
9
- if (status === "RUN") return pc.cyan(status);
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 · ${result.summary.activeFiles} active · ${result.summary.skippedFiles} skipped · ${result.summary.suites} suites · ${result.summary.services} services`
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 ? ` · depends on ${service.dependsOn.join(", ")}` : ""})`)}`
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
- for (const type of orderedTypes([...typeGroups.keys()])) {
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(` ${colorTypeBadge(type.toUpperCase())} ${formatSelectionTypeLabel(type)}`);
44
- for (const suite of suites) {
45
- lines.push(` ${bold(suite.groupLabel)} ${muted(`(${suite.fileCount} files)`)}`);
46
- for (const file of filesBySuite.get(suite.id) || []) {
47
- const status = file.skipped ? `${colorStatus("SKIP")} ${file.skipReason}` : muted(buildHistoryHint(file));
48
- lines.push(` ${file.displayName}${status ? ` ${status}` : ""}`);
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} · active ${service.activeFileCount} · skipped ${service.skippedFileCount}`);
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 { colorSectionLine, colorStatus, dim } from "./colors.mjs";
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
- `${colorStatus("FAIL")} ${"SETUP"} ${operation.serviceName} ${operation.stage} ${dim(duration)} ${detail}\n`
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(`${colorStatus("RUN")} ${"SETUP"} ${operation.serviceName} ${summary} ${dim(duration)}\n`);
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(`${colorStatus("SKIP")} ${config.name} ${reason}\n`);
129
+ stdout.write(`${statusLabel("SKIP")} ${config.name} ${reason}\n`);
56
130
  },
57
131
  plannedSkip(entry) {
58
132
  stdout.write(
59
- `${colorStatus("SKIP")} ${entry.serviceName} ${entry.type} ${normalizePath(entry.file)} ${dim("0s")} ${shortenMessage(entry.reason || "skipped")}\n`
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(`${colorStatus("RUN").padEnd(12)} ${targetConfig.name} ${task.type} ${task.file}\n`);
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
- `${colorStatus(status)} ${task.serviceName} ${displayTaskType(task)} ${normalizePath(task.file)} ${dim(duration)}${detail}\n`
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
- for (const line of lines) stdout.write(`${colorSectionLine(line)}\n`);
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("RUN SETUP api template rebuild");
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("FAIL SETUP api runtime:prepare");
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/next-analysis",
3
- "version": "0.1.78",
3
+ "version": "0.1.79",
4
4
  "description": "SWC-backed Next.js source analysis primitives for Erench tools",
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elench/testkit-bridge",
3
- "version": "0.1.78",
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.78"
25
+ "@elench/testkit-protocol": "0.1.79"
26
26
  },
27
27
  "private": false
28
28
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elench/testkit-protocol",
3
- "version": "0.1.78",
3
+ "version": "0.1.79",
4
4
  "description": "Shared browser protocol for testkit bridge and extension consumers",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elench/ts-analysis",
3
- "version": "0.1.78",
3
+ "version": "0.1.79",
4
4
  "description": "TypeScript compiler-backed source analysis primitives for Erench tools",
5
5
  "type": "module",
6
6
  "exports": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elench/testkit",
3
- "version": "0.1.78",
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.78",
85
- "@elench/testkit-bridge": "0.1.78",
86
- "@elench/testkit-protocol": "0.1.78",
87
- "@elench/ts-analysis": "0.1.78",
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",