@quenty/cli-output-helpers 1.10.0 → 1.11.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/CHANGELOG.md +28 -96
- package/dist/outputHelper.d.ts +5 -0
- package/dist/outputHelper.d.ts.map +1 -1
- package/dist/outputHelper.js +10 -1
- package/dist/outputHelper.js.map +1 -1
- package/dist/reporting/build-result-reporter.d.ts +33 -0
- package/dist/reporting/build-result-reporter.d.ts.map +1 -0
- package/dist/reporting/build-result-reporter.js +33 -0
- package/dist/reporting/build-result-reporter.js.map +1 -0
- package/dist/reporting/build-result-reporter.test.d.ts +2 -0
- package/dist/reporting/build-result-reporter.test.d.ts.map +1 -0
- package/dist/reporting/build-result-reporter.test.js +42 -0
- package/dist/reporting/build-result-reporter.test.js.map +1 -0
- package/dist/reporting/composite-reporter.d.ts.map +1 -1
- package/dist/reporting/composite-reporter.js.map +1 -1
- package/dist/reporting/composite-result-reporter.d.ts +15 -0
- package/dist/reporting/composite-result-reporter.d.ts.map +1 -0
- package/dist/reporting/composite-result-reporter.js +43 -0
- package/dist/reporting/composite-result-reporter.js.map +1 -0
- package/dist/reporting/composite-result-reporter.test.d.ts +2 -0
- package/dist/reporting/composite-result-reporter.test.d.ts.map +1 -0
- package/dist/reporting/composite-result-reporter.test.js +61 -0
- package/dist/reporting/composite-result-reporter.test.js.map +1 -0
- package/dist/reporting/file-result-reporter.d.ts +29 -0
- package/dist/reporting/file-result-reporter.d.ts.map +1 -0
- package/dist/reporting/file-result-reporter.js +61 -0
- package/dist/reporting/file-result-reporter.js.map +1 -0
- package/dist/reporting/file-result-reporter.test.d.ts +2 -0
- package/dist/reporting/file-result-reporter.test.d.ts.map +1 -0
- package/dist/reporting/file-result-reporter.test.js +78 -0
- package/dist/reporting/file-result-reporter.test.js.map +1 -0
- package/dist/reporting/format-json.d.ts +9 -0
- package/dist/reporting/format-json.d.ts.map +1 -0
- package/dist/reporting/format-json.js +12 -0
- package/dist/reporting/format-json.js.map +1 -0
- package/dist/reporting/format-json.test.d.ts +2 -0
- package/dist/reporting/format-json.test.d.ts.map +1 -0
- package/dist/reporting/format-json.test.js +29 -0
- package/dist/reporting/format-json.test.js.map +1 -0
- package/dist/reporting/format-table.d.ts +18 -0
- package/dist/reporting/format-table.d.ts.map +1 -0
- package/dist/reporting/format-table.js +61 -0
- package/dist/reporting/format-table.js.map +1 -0
- package/dist/reporting/format-table.test.d.ts +2 -0
- package/dist/reporting/format-table.test.d.ts.map +1 -0
- package/dist/reporting/format-table.test.js +90 -0
- package/dist/reporting/format-table.test.js.map +1 -0
- package/dist/reporting/github/annotations.d.ts.map +1 -1
- package/dist/reporting/github/annotations.js +1 -4
- package/dist/reporting/github/annotations.js.map +1 -1
- package/dist/reporting/github/annotations.test.js.map +1 -1
- package/dist/reporting/github/comment-table-reporter.d.ts.map +1 -1
- package/dist/reporting/github/comment-table-reporter.js.map +1 -1
- package/dist/reporting/github/formatting.d.ts.map +1 -1
- package/dist/reporting/github/formatting.js +9 -14
- package/dist/reporting/github/formatting.js.map +1 -1
- package/dist/reporting/github/job-summary-reporter.d.ts.map +1 -1
- package/dist/reporting/github/job-summary-reporter.js.map +1 -1
- package/dist/reporting/grouped-reporter.d.ts.map +1 -1
- package/dist/reporting/grouped-reporter.js +6 -2
- package/dist/reporting/grouped-reporter.js.map +1 -1
- package/dist/reporting/index.d.ts +9 -0
- package/dist/reporting/index.d.ts.map +1 -1
- package/dist/reporting/index.js +12 -1
- package/dist/reporting/index.js.map +1 -1
- package/dist/reporting/json-file-reporter.d.ts.map +1 -1
- package/dist/reporting/json-file-reporter.js +2 -1
- package/dist/reporting/json-file-reporter.js.map +1 -1
- package/dist/reporting/progress-format.d.ts.map +1 -1
- package/dist/reporting/progress-format.js.map +1 -1
- package/dist/reporting/reporter.d.ts.map +1 -1
- package/dist/reporting/reporter.js.map +1 -1
- package/dist/reporting/result-reporter.d.ts +37 -0
- package/dist/reporting/result-reporter.d.ts.map +1 -0
- package/dist/reporting/result-reporter.js +22 -0
- package/dist/reporting/result-reporter.js.map +1 -0
- package/dist/reporting/simple-reporter.d.ts.map +1 -1
- package/dist/reporting/simple-reporter.js +3 -1
- package/dist/reporting/simple-reporter.js.map +1 -1
- package/dist/reporting/spinner-reporter.d.ts.map +1 -1
- package/dist/reporting/spinner-reporter.js +4 -4
- package/dist/reporting/spinner-reporter.js.map +1 -1
- package/dist/reporting/spinner-reporter.test.js.map +1 -1
- package/dist/reporting/state/live-state-tracker.d.ts.map +1 -1
- package/dist/reporting/state/live-state-tracker.js +1 -1
- package/dist/reporting/state/live-state-tracker.js.map +1 -1
- package/dist/reporting/state/loaded-state-tracker.d.ts.map +1 -1
- package/dist/reporting/state/loaded-state-tracker.js.map +1 -1
- package/dist/reporting/state/state-tracker.d.ts.map +1 -1
- package/dist/reporting/stdout-result-reporter.d.ts +14 -0
- package/dist/reporting/stdout-result-reporter.d.ts.map +1 -0
- package/dist/reporting/stdout-result-reporter.js +16 -0
- package/dist/reporting/stdout-result-reporter.js.map +1 -0
- package/dist/reporting/stdout-result-reporter.test.d.ts +2 -0
- package/dist/reporting/stdout-result-reporter.test.d.ts.map +1 -0
- package/dist/reporting/stdout-result-reporter.test.js +28 -0
- package/dist/reporting/stdout-result-reporter.test.js.map +1 -0
- package/dist/reporting/summary-table-reporter.d.ts +2 -0
- package/dist/reporting/summary-table-reporter.d.ts.map +1 -1
- package/dist/reporting/summary-table-reporter.js +44 -33
- package/dist/reporting/summary-table-reporter.js.map +1 -1
- package/dist/reporting/watch-renderer.d.ts +16 -0
- package/dist/reporting/watch-renderer.d.ts.map +1 -0
- package/dist/reporting/watch-renderer.js +110 -0
- package/dist/reporting/watch-renderer.js.map +1 -0
- package/dist/reporting/watch-renderer.test.d.ts +2 -0
- package/dist/reporting/watch-renderer.test.d.ts.map +1 -0
- package/dist/reporting/watch-renderer.test.js +103 -0
- package/dist/reporting/watch-renderer.test.js.map +1 -0
- package/dist/reporting/watch-result-reporter.d.ts +21 -0
- package/dist/reporting/watch-result-reporter.d.ts.map +1 -0
- package/dist/reporting/watch-result-reporter.js +34 -0
- package/dist/reporting/watch-result-reporter.js.map +1 -0
- package/dist/reporting/watch-result-reporter.test.d.ts +2 -0
- package/dist/reporting/watch-result-reporter.test.d.ts.map +1 -0
- package/dist/reporting/watch-result-reporter.test.js +37 -0
- package/dist/reporting/watch-result-reporter.test.js.map +1 -0
- package/package.json +2 -2
- package/src/outputHelper.ts +12 -3
- package/src/reporting/build-result-reporter.test.ts +48 -0
- package/src/reporting/build-result-reporter.ts +58 -0
- package/src/reporting/composite-reporter.ts +10 -2
- package/src/reporting/composite-result-reporter.test.ts +71 -0
- package/src/reporting/composite-result-reporter.ts +58 -0
- package/src/reporting/file-result-reporter.test.ts +112 -0
- package/src/reporting/file-result-reporter.ts +75 -0
- package/src/reporting/format-json.test.ts +34 -0
- package/src/reporting/format-json.ts +16 -0
- package/src/reporting/format-table.test.ts +113 -0
- package/src/reporting/format-table.ts +97 -0
- package/src/reporting/github/annotations.test.ts +1 -3
- package/src/reporting/github/annotations.ts +13 -12
- package/src/reporting/github/comment-table-reporter.ts +5 -5
- package/src/reporting/github/formatting.ts +18 -16
- package/src/reporting/github/job-summary-reporter.ts +3 -1
- package/src/reporting/grouped-reporter.ts +6 -2
- package/src/reporting/index.ts +34 -1
- package/src/reporting/json-file-reporter.ts +2 -1
- package/src/reporting/progress-format.ts +5 -2
- package/src/reporting/reporter.ts +15 -2
- package/src/reporting/result-reporter.ts +40 -0
- package/src/reporting/simple-reporter.ts +3 -1
- package/src/reporting/spinner-reporter.test.ts +7 -6
- package/src/reporting/spinner-reporter.ts +20 -8
- package/src/reporting/state/live-state-tracker.ts +12 -6
- package/src/reporting/state/loaded-state-tracker.ts +2 -7
- package/src/reporting/state/state-tracker.ts +5 -1
- package/src/reporting/stdout-result-reporter.test.ts +34 -0
- package/src/reporting/stdout-result-reporter.ts +23 -0
- package/src/reporting/summary-table-reporter.ts +51 -36
- package/src/reporting/watch-renderer.test.ts +127 -0
- package/src/reporting/watch-renderer.ts +132 -0
- package/src/reporting/watch-result-reporter.test.ts +44 -0
- package/src/reporting/watch-result-reporter.ts +45 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -52,10 +52,7 @@ function _escapeProperty(value: string): string {
|
|
|
52
52
|
|
|
53
53
|
/** Escape a workflow command message (data portion). */
|
|
54
54
|
function _escapeMessage(value: string): string {
|
|
55
|
-
return value
|
|
56
|
-
.replace(/%/g, '%25')
|
|
57
|
-
.replace(/\r/g, '%0D')
|
|
58
|
-
.replace(/\n/g, '%0A');
|
|
55
|
+
return value.replace(/%/g, '%25').replace(/\r/g, '%0D').replace(/\n/g, '%0A');
|
|
59
56
|
}
|
|
60
57
|
|
|
61
58
|
// ── Annotation emission ─────────────────────────────────────────────────────
|
|
@@ -153,12 +150,12 @@ export function formatAnnotationSummaryMarkdown(
|
|
|
153
150
|
);
|
|
154
151
|
}
|
|
155
152
|
if (summary.notices > 0) {
|
|
156
|
-
parts.push(
|
|
157
|
-
`${summary.notices} notice${summary.notices !== 1 ? 's' : ''}`
|
|
158
|
-
);
|
|
153
|
+
parts.push(`${summary.notices} notice${summary.notices !== 1 ? 's' : ''}`);
|
|
159
154
|
}
|
|
160
155
|
|
|
161
|
-
md += `**${summary.total} issue${summary.total !== 1 ? 's' : ''}** across ${
|
|
156
|
+
md += `**${summary.total} issue${summary.total !== 1 ? 's' : ''}** across ${
|
|
157
|
+
summary.fileCount
|
|
158
|
+
} file${summary.fileCount !== 1 ? 's' : ''}: ${parts.join(', ')}\n\n`;
|
|
162
159
|
|
|
163
160
|
// Group diagnostics by file
|
|
164
161
|
const byFile = new Map<string, Diagnostic[]>();
|
|
@@ -189,14 +186,16 @@ export function formatAnnotationSummaryMarkdown(
|
|
|
189
186
|
d.severity === 'error'
|
|
190
187
|
? '`error`'
|
|
191
188
|
: d.severity === 'warning'
|
|
192
|
-
|
|
193
|
-
|
|
189
|
+
? '`warning`'
|
|
190
|
+
: '`notice`';
|
|
194
191
|
const escapedMsg = d.message.replace(/\|/g, '\\|').replace(/\n/g, ' ');
|
|
195
192
|
md += `| ${d.line} | ${sev} | ${escapedMsg} |\n`;
|
|
196
193
|
}
|
|
197
194
|
|
|
198
195
|
if (diags.length > MAX_PER_FILE) {
|
|
199
|
-
md += `\n_... and ${
|
|
196
|
+
md += `\n_... and ${
|
|
197
|
+
diags.length - MAX_PER_FILE
|
|
198
|
+
} more issue(s) in this file_\n`;
|
|
200
199
|
}
|
|
201
200
|
|
|
202
201
|
md += '\n</details>\n\n';
|
|
@@ -226,7 +225,9 @@ export async function writeAnnotationSummaryAsync(
|
|
|
226
225
|
OutputHelper.info('Written lint results to GitHub job summary.');
|
|
227
226
|
} catch (err) {
|
|
228
227
|
OutputHelper.warn(
|
|
229
|
-
`Failed to write job summary: ${
|
|
228
|
+
`Failed to write job summary: ${
|
|
229
|
+
err instanceof Error ? err.message : String(err)
|
|
230
|
+
}`
|
|
230
231
|
);
|
|
231
232
|
}
|
|
232
233
|
}
|
|
@@ -80,14 +80,14 @@ export class GithubCommentTableReporter extends BaseReporter {
|
|
|
80
80
|
this._scheduleUpdate();
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
override onPackagePhaseChange(
|
|
84
|
-
_name: string,
|
|
85
|
-
_phase: PackageStatus
|
|
86
|
-
): void {
|
|
83
|
+
override onPackagePhaseChange(_name: string, _phase: PackageStatus): void {
|
|
87
84
|
this._scheduleUpdate();
|
|
88
85
|
}
|
|
89
86
|
|
|
90
|
-
override onPackageProgressUpdate(
|
|
87
|
+
override onPackageProgressUpdate(
|
|
88
|
+
_name: string,
|
|
89
|
+
_progress: ProgressSummary
|
|
90
|
+
): void {
|
|
91
91
|
this._scheduleUpdate();
|
|
92
92
|
}
|
|
93
93
|
|
|
@@ -16,7 +16,11 @@ import {
|
|
|
16
16
|
type IStateTracker,
|
|
17
17
|
type PackageState,
|
|
18
18
|
} from '../state/state-tracker.js';
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
formatProgressInline,
|
|
21
|
+
formatProgressResult,
|
|
22
|
+
isEmptyTestRun,
|
|
23
|
+
} from '../progress-format.js';
|
|
20
24
|
|
|
21
25
|
// ── Public types ────────────────────────────────────────────────────────────
|
|
22
26
|
|
|
@@ -111,7 +115,10 @@ const RUNNING_PHASE_LABELS: Record<string, string> = {
|
|
|
111
115
|
executing: '🔄 Executing...',
|
|
112
116
|
};
|
|
113
117
|
|
|
114
|
-
export function formatRunningStatus(
|
|
118
|
+
export function formatRunningStatus(
|
|
119
|
+
phase: PackageStatus,
|
|
120
|
+
progress?: ProgressSummary
|
|
121
|
+
): string {
|
|
115
122
|
const label = RUNNING_PHASE_LABELS[phase] ?? '🔄 Running...';
|
|
116
123
|
if (progress) {
|
|
117
124
|
const progressText = formatProgressInline(progress);
|
|
@@ -130,10 +137,10 @@ export function formatResultStatus(
|
|
|
130
137
|
const empty = isEmptyTestRun(pkg.progressSummary);
|
|
131
138
|
|
|
132
139
|
if (pkg.success) {
|
|
133
|
-
const label = progressText
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
140
|
+
const label = progressText
|
|
141
|
+
? `${successLabel} ${progressText}`
|
|
142
|
+
: successLabel;
|
|
143
|
+
return empty ? `⚠️ ${label} (${duration})` : `✅ ${label} (${duration})`;
|
|
137
144
|
}
|
|
138
145
|
|
|
139
146
|
const failedPhase = pkg.failedPhase;
|
|
@@ -305,9 +312,7 @@ export function formatGithubTableBody(
|
|
|
305
312
|
).length;
|
|
306
313
|
const running = packages.filter(
|
|
307
314
|
(p) =>
|
|
308
|
-
p.status !== 'pending' &&
|
|
309
|
-
p.status !== 'passed' &&
|
|
310
|
-
p.status !== 'failed'
|
|
315
|
+
p.status !== 'pending' && p.status !== 'passed' && p.status !== 'failed'
|
|
311
316
|
).length;
|
|
312
317
|
const pending = packages.filter((p) => p.status === 'pending').length;
|
|
313
318
|
const parts: string[] = [];
|
|
@@ -330,14 +335,11 @@ export function formatGithubNoTestsBody(
|
|
|
330
335
|
const actionsRunUrl = getActionsRunUrl();
|
|
331
336
|
const heading = config.heading;
|
|
332
337
|
|
|
333
|
-
|
|
334
|
-
body += `## ${heading}\n\n`;
|
|
335
|
-
body += `ℹ️ **No tests to run**\n\n`;
|
|
336
|
-
body += `${message}\n`;
|
|
338
|
+
const logsPart = actionsRunUrl ? ` · [View logs](${actionsRunUrl})` : '';
|
|
337
339
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
}
|
|
340
|
+
let body = config.commentMarker + '\n';
|
|
341
|
+
body += `## ${heading}\n`;
|
|
342
|
+
body += `ℹ️ ${message}${logsPart}\n`;
|
|
341
343
|
|
|
342
344
|
return body;
|
|
343
345
|
}
|
|
@@ -79,7 +79,9 @@ export class GithubJobSummaryReporter extends BaseReporter {
|
|
|
79
79
|
OutputHelper.info('Written results to GitHub job summary.');
|
|
80
80
|
} catch (err) {
|
|
81
81
|
OutputHelper.warn(
|
|
82
|
-
`Failed to write job summary: ${
|
|
82
|
+
`Failed to write job summary: ${
|
|
83
|
+
err instanceof Error ? err.message : String(err)
|
|
84
|
+
}`
|
|
83
85
|
);
|
|
84
86
|
}
|
|
85
87
|
}
|
|
@@ -77,11 +77,15 @@ export class GroupedReporter extends BaseReporter {
|
|
|
77
77
|
const empty = isEmptyTestRun(result.progressSummary);
|
|
78
78
|
|
|
79
79
|
if (result.success) {
|
|
80
|
-
const label = progressText
|
|
80
|
+
const label = progressText
|
|
81
|
+
? `${successLabel} ${progressText}`
|
|
82
|
+
: successLabel;
|
|
81
83
|
const formatted = empty
|
|
82
84
|
? OutputHelper.formatWarning(`${label} ⚠`)
|
|
83
85
|
: OutputHelper.formatSuccess(label);
|
|
84
|
-
const icon = empty
|
|
86
|
+
const icon = empty
|
|
87
|
+
? OutputHelper.formatWarning('⚠')
|
|
88
|
+
: OutputHelper.formatSuccess('✓');
|
|
85
89
|
console.log(
|
|
86
90
|
` ${icon} ${formatted} ${OutputHelper.formatDim(`(${duration})`)}`
|
|
87
91
|
);
|
package/src/reporting/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Core types and base class
|
|
1
|
+
// Core types and base class — batch lifecycle (multi-package, phases, progress).
|
|
2
2
|
export {
|
|
3
3
|
BaseReporter,
|
|
4
4
|
type Reporter,
|
|
@@ -12,6 +12,39 @@ export {
|
|
|
12
12
|
type StepProgress,
|
|
13
13
|
} from './reporter.js';
|
|
14
14
|
|
|
15
|
+
// Single-result reporter — for one-shot or polled command output.
|
|
16
|
+
export { BaseResultReporter, type ResultReporter } from './result-reporter.js';
|
|
17
|
+
export {
|
|
18
|
+
StdoutResultReporter,
|
|
19
|
+
type StdoutResultReporterOptions,
|
|
20
|
+
} from './stdout-result-reporter.js';
|
|
21
|
+
export {
|
|
22
|
+
FileResultReporter,
|
|
23
|
+
type FileResultReporterOptions,
|
|
24
|
+
} from './file-result-reporter.js';
|
|
25
|
+
export {
|
|
26
|
+
WatchResultReporter,
|
|
27
|
+
type WatchResultReporterOptions,
|
|
28
|
+
} from './watch-result-reporter.js';
|
|
29
|
+
export { CompositeResultReporter } from './composite-result-reporter.js';
|
|
30
|
+
export {
|
|
31
|
+
buildResultReporter,
|
|
32
|
+
type BuildResultReporterOptions,
|
|
33
|
+
} from './build-result-reporter.js';
|
|
34
|
+
|
|
35
|
+
// Output formatting primitives.
|
|
36
|
+
export {
|
|
37
|
+
formatTable,
|
|
38
|
+
type TableColumn,
|
|
39
|
+
type TableOptions,
|
|
40
|
+
} from './format-table.js';
|
|
41
|
+
export { formatJson, type JsonOutputOptions } from './format-json.js';
|
|
42
|
+
export {
|
|
43
|
+
createWatchRenderer,
|
|
44
|
+
type WatchRenderer,
|
|
45
|
+
type WatchRendererOptions,
|
|
46
|
+
} from './watch-renderer.js';
|
|
47
|
+
|
|
15
48
|
// Progress formatting helpers
|
|
16
49
|
export {
|
|
17
50
|
formatProgressInline,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from 'fs/promises';
|
|
2
2
|
import { OutputHelper } from '../outputHelper.js';
|
|
3
3
|
import { BaseReporter } from './reporter.js';
|
|
4
|
+
import { formatJson } from './format-json.js';
|
|
4
5
|
import { type IStateTracker } from './state/state-tracker.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -32,7 +33,7 @@ export class JsonFileReporter extends BaseReporter {
|
|
|
32
33
|
},
|
|
33
34
|
};
|
|
34
35
|
|
|
35
|
-
await fs.writeFile(this._outputPath,
|
|
36
|
+
await fs.writeFile(this._outputPath, formatJson(summary, { pretty: true }));
|
|
36
37
|
OutputHelper.info(`Results written to ${this._outputPath}`);
|
|
37
38
|
}
|
|
38
39
|
}
|
|
@@ -20,7 +20,9 @@ export function formatProgressInline(progress?: ProgressSummary): string {
|
|
|
20
20
|
return `(${progress.passed}/${progress.total})`;
|
|
21
21
|
case 'bytes':
|
|
22
22
|
if (progress.totalBytes > 0 && progress.transferredBytes > 0) {
|
|
23
|
-
return `(${_formatBytes(progress.transferredBytes)}/${_formatBytes(
|
|
23
|
+
return `(${_formatBytes(progress.transferredBytes)}/${_formatBytes(
|
|
24
|
+
progress.totalBytes
|
|
25
|
+
)})`;
|
|
24
26
|
}
|
|
25
27
|
return `(${_formatBytes(progress.totalBytes)})`;
|
|
26
28
|
case 'steps':
|
|
@@ -79,7 +81,8 @@ export function summarizeFailure(
|
|
|
79
81
|
|
|
80
82
|
if (error) {
|
|
81
83
|
const firstLine = error.split('\n')[0];
|
|
82
|
-
const short =
|
|
84
|
+
const short =
|
|
85
|
+
firstLine.length > 60 ? firstLine.slice(0, 57) + '...' : firstLine;
|
|
83
86
|
if (parts.length > 0) {
|
|
84
87
|
parts.push(`: ${short}`);
|
|
85
88
|
} else {
|
|
@@ -6,7 +6,17 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
/** Execution phases a package can move through. */
|
|
9
|
-
export type JobPhase =
|
|
9
|
+
export type JobPhase =
|
|
10
|
+
| 'waiting'
|
|
11
|
+
| 'building'
|
|
12
|
+
| 'downloading'
|
|
13
|
+
| 'merging'
|
|
14
|
+
| 'combining'
|
|
15
|
+
| 'uploading'
|
|
16
|
+
| 'scheduling'
|
|
17
|
+
| 'launching'
|
|
18
|
+
| 'connecting'
|
|
19
|
+
| 'executing';
|
|
10
20
|
|
|
11
21
|
/** Unified status for a package moving through the job lifecycle. */
|
|
12
22
|
export type PackageStatus = 'pending' | JobPhase | 'passed' | 'failed';
|
|
@@ -94,7 +104,10 @@ export class BaseReporter implements Reporter {
|
|
|
94
104
|
async startAsync(): Promise<void> {}
|
|
95
105
|
onPackageStart(_packageName: string): void {}
|
|
96
106
|
onPackagePhaseChange(_packageName: string, _phase: JobPhase): void {}
|
|
97
|
-
onPackageProgressUpdate(
|
|
107
|
+
onPackageProgressUpdate(
|
|
108
|
+
_packageName: string,
|
|
109
|
+
_progress: ProgressSummary
|
|
110
|
+
): void {}
|
|
98
111
|
onPackageResult(_result: PackageResult, _bufferedOutput?: string[]): void {}
|
|
99
112
|
async stopAsync(): Promise<void> {}
|
|
100
113
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single-result reporter — for commands that produce one result (or a polled
|
|
3
|
+
* series of results), as opposed to batch jobs with per-package lifecycle.
|
|
4
|
+
*
|
|
5
|
+
* Use this for any CLI command that:
|
|
6
|
+
* - Runs once and prints a result to stdout
|
|
7
|
+
* - Writes a result to a file via --output
|
|
8
|
+
* - Polls a result on an interval and redraws (--watch)
|
|
9
|
+
*
|
|
10
|
+
* For batch jobs (multi-package, lifecycle phases, progress events), use the
|
|
11
|
+
* Reporter interface in reporter.ts instead.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Lifecycle hooks for a single-result reporter. Implementations decide how
|
|
16
|
+
* to render the result (stdout, file, watch redraw, etc).
|
|
17
|
+
*/
|
|
18
|
+
export interface ResultReporter<T = unknown> {
|
|
19
|
+
/** Called once before any results are reported. */
|
|
20
|
+
startAsync(): Promise<void>;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Called when a result is available. May be called multiple times for
|
|
24
|
+
* watch mode (each tick produces a fresh result).
|
|
25
|
+
*/
|
|
26
|
+
onResult(result: T): void;
|
|
27
|
+
|
|
28
|
+
/** Called once after the final result. */
|
|
29
|
+
stopAsync(): Promise<void>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Base class with no-op defaults. Reporters extend this and override only
|
|
34
|
+
* the hooks they need.
|
|
35
|
+
*/
|
|
36
|
+
export class BaseResultReporter<T = unknown> implements ResultReporter<T> {
|
|
37
|
+
async startAsync(): Promise<void> {}
|
|
38
|
+
onResult(_result: T): void {}
|
|
39
|
+
async stopAsync(): Promise<void> {}
|
|
40
|
+
}
|
|
@@ -40,7 +40,9 @@ export class SimpleReporter extends BaseReporter {
|
|
|
40
40
|
|
|
41
41
|
const progressText = formatProgressResult(result.progressSummary);
|
|
42
42
|
if (result.success) {
|
|
43
|
-
const msg = progressText
|
|
43
|
+
const msg = progressText
|
|
44
|
+
? `${this._successMessage} ${progressText}`
|
|
45
|
+
: this._successMessage;
|
|
44
46
|
OutputHelper.info(msg);
|
|
45
47
|
} else {
|
|
46
48
|
OutputHelper.error(this._failureMessage);
|
|
@@ -13,12 +13,13 @@ function setup() {
|
|
|
13
13
|
// Capture everything written to stdout
|
|
14
14
|
const writes: string[] = [];
|
|
15
15
|
const realWrite = process.stdout.write.bind(process.stdout);
|
|
16
|
-
vi.spyOn(process.stdout, 'write').mockImplementation(
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
vi.spyOn(process.stdout, 'write').mockImplementation(((
|
|
17
|
+
chunk: any,
|
|
18
|
+
...args: any[]
|
|
19
|
+
) => {
|
|
20
|
+
writes.push(typeof chunk === 'string' ? chunk : chunk.toString());
|
|
21
|
+
return true;
|
|
22
|
+
}) as any);
|
|
22
23
|
|
|
23
24
|
// Suppress console.log (used by startAsync header)
|
|
24
25
|
vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
@@ -2,7 +2,11 @@ import { OutputHelper } from '../outputHelper.js';
|
|
|
2
2
|
import { formatDurationMs } from '../cli-utils.js';
|
|
3
3
|
import { type PackageResult, BaseReporter } from './reporter.js';
|
|
4
4
|
import { type IStateTracker } from './state/state-tracker.js';
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
formatProgressInline,
|
|
7
|
+
formatProgressResult,
|
|
8
|
+
isEmptyTestRun,
|
|
9
|
+
} from './progress-format.js';
|
|
6
10
|
|
|
7
11
|
export interface SpinnerReporterOptions {
|
|
8
12
|
showLogs: boolean;
|
|
@@ -122,8 +126,8 @@ export class SpinnerReporter extends BaseReporter {
|
|
|
122
126
|
? OutputHelper.formatSuccess('✓')
|
|
123
127
|
: OutputHelper.formatError('✗');
|
|
124
128
|
const status = result.success
|
|
125
|
-
?
|
|
126
|
-
:
|
|
129
|
+
? this._options.successLabel ?? 'Passed'
|
|
130
|
+
: this._options.failureLabel ?? 'FAILED';
|
|
127
131
|
const formatted = result.success
|
|
128
132
|
? OutputHelper.formatSuccess(status)
|
|
129
133
|
: OutputHelper.formatError(status);
|
|
@@ -172,10 +176,14 @@ export class SpinnerReporter extends BaseReporter {
|
|
|
172
176
|
? `${phaseLabel} ${progressText}`
|
|
173
177
|
: phaseLabel;
|
|
174
178
|
const statusText = OutputHelper.formatInfo(plain.padEnd(22));
|
|
175
|
-
line = ` ${icon} ${state.name.padEnd(
|
|
179
|
+
line = ` ${icon} ${state.name.padEnd(
|
|
180
|
+
30
|
|
181
|
+
)} ${statusText} ${OutputHelper.formatDim(time)}`;
|
|
176
182
|
} else if (state.status === 'passed') {
|
|
177
183
|
const icon = OutputHelper.formatSuccess('✓');
|
|
178
|
-
const progressText = formatProgressResult(
|
|
184
|
+
const progressText = formatProgressResult(
|
|
185
|
+
state.result?.progressSummary
|
|
186
|
+
);
|
|
179
187
|
const label = this._options.successLabel ?? 'Passed';
|
|
180
188
|
const empty = isEmptyTestRun(state.result?.progressSummary);
|
|
181
189
|
let plain = progressText ? `${label} ${progressText}` : label;
|
|
@@ -183,15 +191,19 @@ export class SpinnerReporter extends BaseReporter {
|
|
|
183
191
|
const statusText = empty
|
|
184
192
|
? OutputHelper.formatWarning(plain.padEnd(22))
|
|
185
193
|
: OutputHelper.formatSuccess(plain.padEnd(22));
|
|
186
|
-
line = ` ${icon} ${state.name.padEnd(
|
|
194
|
+
line = ` ${icon} ${state.name.padEnd(
|
|
195
|
+
30
|
|
196
|
+
)} ${statusText} ${OutputHelper.formatDim(time)}`;
|
|
187
197
|
} else {
|
|
188
198
|
const icon = OutputHelper.formatError('✗');
|
|
189
199
|
const failedPhase = state.result?.failedPhase;
|
|
190
200
|
const plain = failedPhase
|
|
191
201
|
? `${this._options.failureLabel ?? 'FAILED'} at ${failedPhase}`
|
|
192
|
-
:
|
|
202
|
+
: this._options.failureLabel ?? 'FAILED';
|
|
193
203
|
const statusText = OutputHelper.formatError(plain.padEnd(22));
|
|
194
|
-
line = ` ${icon} ${state.name.padEnd(
|
|
204
|
+
line = ` ${icon} ${state.name.padEnd(
|
|
205
|
+
30
|
|
206
|
+
)} ${statusText} ${OutputHelper.formatDim(time)}`;
|
|
195
207
|
}
|
|
196
208
|
|
|
197
209
|
lines.push(line);
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type PackageResult,
|
|
3
|
+
type PackageStatus,
|
|
4
|
+
type JobPhase,
|
|
5
|
+
type ProgressSummary,
|
|
6
|
+
BaseReporter,
|
|
7
|
+
} from '../reporter.js';
|
|
2
8
|
import { type IStateTracker, type PackageState } from './state-tracker.js';
|
|
3
9
|
|
|
4
10
|
export type { PackageState } from './state-tracker.js';
|
|
@@ -8,10 +14,7 @@ export type { PackageState } from './state-tracker.js';
|
|
|
8
14
|
* Extends BaseReporter to receive lifecycle hooks and mutate state.
|
|
9
15
|
* Reporters read from it via the IStateTracker interface.
|
|
10
16
|
*/
|
|
11
|
-
export class LiveStateTracker
|
|
12
|
-
extends BaseReporter
|
|
13
|
-
implements IStateTracker
|
|
14
|
-
{
|
|
17
|
+
export class LiveStateTracker extends BaseReporter implements IStateTracker {
|
|
15
18
|
private _packages: Map<string, PackageState>;
|
|
16
19
|
private _startTimeMs = 0;
|
|
17
20
|
private _completed = 0;
|
|
@@ -77,7 +80,10 @@ export class LiveStateTracker
|
|
|
77
80
|
state.progress = undefined; // clear progress on phase transition
|
|
78
81
|
}
|
|
79
82
|
|
|
80
|
-
override onPackageProgressUpdate(
|
|
83
|
+
override onPackageProgressUpdate(
|
|
84
|
+
name: string,
|
|
85
|
+
progress: ProgressSummary
|
|
86
|
+
): void {
|
|
81
87
|
const state = this._packages.get(name);
|
|
82
88
|
if (!state) return;
|
|
83
89
|
state.progress = progress;
|
|
@@ -4,10 +4,7 @@ import {
|
|
|
4
4
|
type PackageStatus,
|
|
5
5
|
type BatchSummary,
|
|
6
6
|
} from '../reporter.js';
|
|
7
|
-
import {
|
|
8
|
-
type IStateTracker,
|
|
9
|
-
type PackageState,
|
|
10
|
-
} from './state-tracker.js';
|
|
7
|
+
import { type IStateTracker, type PackageState } from './state-tracker.js';
|
|
11
8
|
|
|
12
9
|
/**
|
|
13
10
|
* Batch state loaded from a previously-saved BatchSummary JSON file.
|
|
@@ -31,9 +28,7 @@ export class LoadedStateTracker implements IStateTracker {
|
|
|
31
28
|
this._startTimeMs = startTimeMs;
|
|
32
29
|
}
|
|
33
30
|
|
|
34
|
-
static async fromFileAsync(
|
|
35
|
-
filePath: string
|
|
36
|
-
): Promise<LoadedStateTracker> {
|
|
31
|
+
static async fromFileAsync(filePath: string): Promise<LoadedStateTracker> {
|
|
37
32
|
const raw = await fs.readFile(filePath, 'utf-8');
|
|
38
33
|
const summary = JSON.parse(raw) as BatchSummary;
|
|
39
34
|
return LoadedStateTracker.fromSummary(summary);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { StdoutResultReporter } from './stdout-result-reporter.js';
|
|
3
|
+
|
|
4
|
+
describe('StdoutResultReporter', () => {
|
|
5
|
+
let logSpy: ReturnType<typeof vi.spyOn>;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
logSpy.mockRestore();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('writes rendered result to stdout on each onResult', () => {
|
|
16
|
+
const reporter = new StdoutResultReporter<{ name: string }>({
|
|
17
|
+
render: (r) => `name=${r.name}`,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
reporter.onResult({ name: 'first' });
|
|
21
|
+
reporter.onResult({ name: 'second' });
|
|
22
|
+
|
|
23
|
+
expect(logSpy).toHaveBeenCalledTimes(2);
|
|
24
|
+
expect(logSpy).toHaveBeenNthCalledWith(1, 'name=first');
|
|
25
|
+
expect(logSpy).toHaveBeenNthCalledWith(2, 'name=second');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('startAsync and stopAsync are no-ops', async () => {
|
|
29
|
+
const reporter = new StdoutResultReporter<unknown>({ render: () => '' });
|
|
30
|
+
await reporter.startAsync();
|
|
31
|
+
await reporter.stopAsync();
|
|
32
|
+
expect(logSpy).not.toHaveBeenCalled();
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Writes a rendered result to stdout. One-shot — calls render(result) and
|
|
3
|
+
* console.log(...) on each onResult.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { BaseResultReporter } from './result-reporter.js';
|
|
7
|
+
|
|
8
|
+
export interface StdoutResultReporterOptions<T> {
|
|
9
|
+
render: (result: T) => string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class StdoutResultReporter<T = unknown> extends BaseResultReporter<T> {
|
|
13
|
+
private _render: (result: T) => string;
|
|
14
|
+
|
|
15
|
+
constructor(options: StdoutResultReporterOptions<T>) {
|
|
16
|
+
super();
|
|
17
|
+
this._render = options.render;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
override onResult(result: T): void {
|
|
21
|
+
console.log(this._render(result));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { OutputHelper } from '../outputHelper.js';
|
|
2
2
|
import { formatDurationMs } from '../cli-utils.js';
|
|
3
|
-
import { BaseReporter } from './reporter.js';
|
|
3
|
+
import { BaseReporter, type PackageResult } from './reporter.js';
|
|
4
|
+
import { formatTable, type TableColumn } from './format-table.js';
|
|
4
5
|
import { type IStateTracker } from './state/state-tracker.js';
|
|
5
6
|
import { formatProgressResult, isEmptyTestRun } from './progress-format.js';
|
|
6
7
|
|
|
@@ -39,44 +40,30 @@ export class SummaryTableReporter extends BaseReporter {
|
|
|
39
40
|
const passed = results.length - failures.length;
|
|
40
41
|
const durationMs = Date.now() - this._state.startTimeMs;
|
|
41
42
|
|
|
42
|
-
const STATUS_WIDTH = 26;
|
|
43
|
-
|
|
44
|
-
console.log('');
|
|
45
|
-
console.log('Package'.padEnd(40) + 'Status'.padEnd(STATUS_WIDTH) + 'Duration');
|
|
46
|
-
console.log('─'.repeat(40 + STATUS_WIDTH + 8));
|
|
47
|
-
|
|
48
43
|
let emptyRunCount = 0;
|
|
49
|
-
for (const result of results) {
|
|
50
|
-
const progressText = formatProgressResult(result.progressSummary);
|
|
51
|
-
const empty = isEmptyTestRun(result.progressSummary);
|
|
52
|
-
if (empty) emptyRunCount++;
|
|
53
44
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
45
|
+
const columns: TableColumn<PackageResult>[] = [
|
|
46
|
+
{
|
|
47
|
+
header: 'Package',
|
|
48
|
+
value: (r) => r.packageName,
|
|
49
|
+
minWidth: 40,
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
header: 'Status',
|
|
53
|
+
value: (r) => this._statusLabel(r),
|
|
54
|
+
format: (label, r) =>
|
|
55
|
+
this._colorStatus(label, r, () => emptyRunCount++),
|
|
56
|
+
minWidth: 26,
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
header: 'Duration',
|
|
60
|
+
value: (r) => formatDurationMs(r.durationMs),
|
|
61
|
+
format: (v) => OutputHelper.formatDim(v),
|
|
62
|
+
},
|
|
63
|
+
];
|
|
63
64
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
let status: string;
|
|
67
|
-
if (result.success) {
|
|
68
|
-
status = empty
|
|
69
|
-
? OutputHelper.formatWarning(paddedLabel)
|
|
70
|
-
: OutputHelper.formatSuccess(paddedLabel);
|
|
71
|
-
} else {
|
|
72
|
-
status = OutputHelper.formatError(paddedLabel);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const duration = OutputHelper.formatDim(
|
|
76
|
-
formatDurationMs(result.durationMs)
|
|
77
|
-
);
|
|
78
|
-
console.log(result.packageName.padEnd(40) + status + duration);
|
|
79
|
-
}
|
|
65
|
+
console.log('');
|
|
66
|
+
console.log(formatTable(results, columns));
|
|
80
67
|
|
|
81
68
|
console.log('');
|
|
82
69
|
const passedText = OutputHelper.formatSuccess(`${passed} passed`);
|
|
@@ -99,4 +86,32 @@ export class SummaryTableReporter extends BaseReporter {
|
|
|
99
86
|
);
|
|
100
87
|
}
|
|
101
88
|
}
|
|
89
|
+
|
|
90
|
+
private _statusLabel(result: PackageResult): string {
|
|
91
|
+
if (result.success) {
|
|
92
|
+
const progressText = formatProgressResult(result.progressSummary);
|
|
93
|
+
return progressText
|
|
94
|
+
? `${this._successLabel} ${progressText}`
|
|
95
|
+
: this._successLabel;
|
|
96
|
+
}
|
|
97
|
+
const failedPhase = result.failedPhase;
|
|
98
|
+
return failedPhase
|
|
99
|
+
? `${this._failureLabel} at ${failedPhase}`
|
|
100
|
+
: this._failureLabel;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private _colorStatus(
|
|
104
|
+
label: string,
|
|
105
|
+
result: PackageResult,
|
|
106
|
+
countEmpty: () => void
|
|
107
|
+
): string {
|
|
108
|
+
if (result.success) {
|
|
109
|
+
const empty = isEmptyTestRun(result.progressSummary);
|
|
110
|
+
if (empty) countEmpty();
|
|
111
|
+
return empty
|
|
112
|
+
? OutputHelper.formatWarning(label)
|
|
113
|
+
: OutputHelper.formatSuccess(label);
|
|
114
|
+
}
|
|
115
|
+
return OutputHelper.formatError(label);
|
|
116
|
+
}
|
|
102
117
|
}
|