@mandujs/core 0.31.0 → 0.33.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/package.json +7 -1
- package/src/bundler/build.ts +29 -1
- package/src/bundler/generate-static-params.ts +302 -290
- package/src/bundler/prerender.ts +446 -368
- package/src/bundler/types.ts +20 -0
- package/src/config/mandu.ts +58 -1
- package/src/config/validate.ts +119 -1
- package/src/diagnose/__tests__/checks.test.ts +378 -0
- package/src/diagnose/checks.ts +599 -0
- package/src/diagnose/index.ts +15 -0
- package/src/diagnose/run.ts +87 -0
- package/src/diagnose/types.ts +53 -0
- package/src/filling/context.ts +60 -0
- package/src/guard/check.ts +225 -1
- package/src/guard/define-rule.ts +243 -0
- package/src/guard/graph.ts +898 -0
- package/src/guard/index.ts +40 -0
- package/src/guard/rule-presets.ts +379 -0
- package/src/i18n/define.ts +126 -0
- package/src/i18n/index.ts +52 -0
- package/src/i18n/locale-resolver.ts +214 -0
- package/src/i18n/message-registry.ts +173 -0
- package/src/i18n/types.ts +112 -0
- package/src/plugins/__tests__/lifecycle-integration.test.ts +272 -0
- package/src/plugins/__tests__/runner.test.ts +409 -0
- package/src/plugins/define.ts +124 -0
- package/src/plugins/examples/dep-check-plugin.ts +80 -0
- package/src/plugins/examples/prerender-cache-plugin.ts +111 -0
- package/src/plugins/examples/sitemap-plugin.ts +65 -0
- package/src/plugins/hooks.ts +297 -64
- package/src/plugins/index.ts +80 -41
- package/src/plugins/runner.ts +361 -0
- package/src/router/fs-routes.ts +64 -1
- package/src/router/fs-scanner.ts +101 -0
- package/src/router/index.ts +7 -1
- package/src/runtime/server.ts +409 -10
- package/src/runtime/ssr.ts +9 -0
- package/src/spec/schema.ts +25 -0
- package/src/testing/__tests__/reporter.test.ts +454 -0
- package/src/testing/index.ts +29 -0
- package/src/testing/reporter.ts +676 -0
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 18.σ — Regression tests for the unified test reporter.
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - `summarizeReport` counter math
|
|
6
|
+
* - `formatHuman` — color-on/color-off parity, grouping, error body
|
|
7
|
+
* - `formatJson` — schema version + round-trip stability
|
|
8
|
+
* - `formatJunit` — root element + per-suite buckets + XML escape
|
|
9
|
+
* - `formatLcov` — body passthrough + synthetic fallback
|
|
10
|
+
* - `mergeReports` — empty / single / multi-report shapes
|
|
11
|
+
* - `checkCoverageThresholds` — pass / fail / no-config / missing metric
|
|
12
|
+
* - `formatThresholdFailure` — per-metric breakdown
|
|
13
|
+
* - `parseLcovSummary` — LF/LH/BRF/BRH/FNF/FNH aggregation
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { describe, it, expect } from "bun:test";
|
|
17
|
+
import {
|
|
18
|
+
formatReport,
|
|
19
|
+
formatHuman,
|
|
20
|
+
formatJson,
|
|
21
|
+
formatJunit,
|
|
22
|
+
formatLcov,
|
|
23
|
+
mergeReports,
|
|
24
|
+
summarizeReport,
|
|
25
|
+
checkCoverageThresholds,
|
|
26
|
+
formatThresholdFailure,
|
|
27
|
+
parseLcovSummary,
|
|
28
|
+
emptyReport,
|
|
29
|
+
type TestReport,
|
|
30
|
+
} from "../reporter";
|
|
31
|
+
|
|
32
|
+
function makeReport(overrides: Partial<TestReport> = {}): TestReport {
|
|
33
|
+
return {
|
|
34
|
+
suite: "unit",
|
|
35
|
+
kind: "unit",
|
|
36
|
+
tests: [
|
|
37
|
+
{ name: "adds 1+1", status: "passed", durationMs: 2, suite: "math" },
|
|
38
|
+
{
|
|
39
|
+
name: "divides by zero",
|
|
40
|
+
status: "failed",
|
|
41
|
+
durationMs: 5,
|
|
42
|
+
suite: "math",
|
|
43
|
+
error: { message: "Infinity, not NaN" },
|
|
44
|
+
},
|
|
45
|
+
{ name: "draft", status: "skipped", durationMs: 0, suite: "misc" },
|
|
46
|
+
],
|
|
47
|
+
durationMs: 7,
|
|
48
|
+
timestamp: "2026-04-20T00:00:00.000Z",
|
|
49
|
+
...overrides,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
54
|
+
// summarizeReport
|
|
55
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
56
|
+
|
|
57
|
+
describe("summarizeReport", () => {
|
|
58
|
+
it("counts each status bucket correctly", () => {
|
|
59
|
+
const s = summarizeReport(makeReport());
|
|
60
|
+
expect(s.total).toBe(3);
|
|
61
|
+
expect(s.passed).toBe(1);
|
|
62
|
+
expect(s.failed).toBe(1);
|
|
63
|
+
expect(s.skipped).toBe(1);
|
|
64
|
+
expect(s.todo).toBe(0);
|
|
65
|
+
expect(s.durationMs).toBe(7);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("handles an empty report without NaNs", () => {
|
|
69
|
+
const s = summarizeReport(emptyReport("unit", "unit"));
|
|
70
|
+
expect(s.total).toBe(0);
|
|
71
|
+
expect(s.passed).toBe(0);
|
|
72
|
+
expect(s.durationMs).toBe(0);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
77
|
+
// formatHuman
|
|
78
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
79
|
+
|
|
80
|
+
describe("formatHuman", () => {
|
|
81
|
+
it("emits the suite heading and every test line", () => {
|
|
82
|
+
const out = formatHuman(makeReport(), { noColor: true });
|
|
83
|
+
expect(out).toContain("mandu test · unit");
|
|
84
|
+
expect(out).toContain("adds 1+1");
|
|
85
|
+
expect(out).toContain("divides by zero");
|
|
86
|
+
expect(out).toContain("draft");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("prints the failure message beneath failed tests", () => {
|
|
90
|
+
const out = formatHuman(makeReport(), { noColor: true });
|
|
91
|
+
expect(out).toContain("Infinity, not NaN");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("emits summary counters on the final summary line", () => {
|
|
95
|
+
const out = formatHuman(makeReport(), { noColor: true });
|
|
96
|
+
expect(out).toContain("1 passed");
|
|
97
|
+
expect(out).toContain("1 failed");
|
|
98
|
+
expect(out).toContain("1 skipped");
|
|
99
|
+
expect(out).toContain("3 total");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("prints the coverage block when present", () => {
|
|
103
|
+
const out = formatHuman(
|
|
104
|
+
makeReport({
|
|
105
|
+
coverage: {
|
|
106
|
+
lines: { hit: 80, found: 100, pct: 80 },
|
|
107
|
+
lcovPath: ".mandu/coverage/lcov.info",
|
|
108
|
+
},
|
|
109
|
+
}),
|
|
110
|
+
{ noColor: true },
|
|
111
|
+
);
|
|
112
|
+
expect(out).toContain("Coverage");
|
|
113
|
+
expect(out).toContain("80.00%");
|
|
114
|
+
expect(out).toContain(".mandu/coverage/lcov.info");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("produces identical text content with and without color", () => {
|
|
118
|
+
const colored = formatHuman(makeReport(), { noColor: false });
|
|
119
|
+
const plain = formatHuman(makeReport(), { noColor: true });
|
|
120
|
+
// Strip ANSI — the visible text should match.
|
|
121
|
+
// eslint-disable-next-line no-control-regex
|
|
122
|
+
const stripped = colored.replace(/\x1b\[[0-9;]*m/g, "");
|
|
123
|
+
expect(stripped).toBe(plain);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
128
|
+
// formatJson
|
|
129
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
130
|
+
|
|
131
|
+
describe("formatJson", () => {
|
|
132
|
+
it("emits a parseable document with the schema version", () => {
|
|
133
|
+
const out = formatJson(makeReport());
|
|
134
|
+
const parsed = JSON.parse(out);
|
|
135
|
+
expect(parsed.schema).toBe("mandu-test-report/v1");
|
|
136
|
+
expect(parsed.suite).toBe("unit");
|
|
137
|
+
expect(parsed.kind).toBe("unit");
|
|
138
|
+
expect(parsed.summary.total).toBe(3);
|
|
139
|
+
expect(Array.isArray(parsed.tests)).toBe(true);
|
|
140
|
+
expect(parsed.tests).toHaveLength(3);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("includes structured failure data", () => {
|
|
144
|
+
const parsed = JSON.parse(formatJson(makeReport()));
|
|
145
|
+
const failed = parsed.tests.find((t: { status: string }) => t.status === "failed");
|
|
146
|
+
expect(failed).toBeDefined();
|
|
147
|
+
expect(failed.error.message).toBe("Infinity, not NaN");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("preserves coverage metrics when present", () => {
|
|
151
|
+
const parsed = JSON.parse(
|
|
152
|
+
formatJson(
|
|
153
|
+
makeReport({
|
|
154
|
+
coverage: {
|
|
155
|
+
lines: { hit: 80, found: 100, pct: 80 },
|
|
156
|
+
branches: { hit: 10, found: 20, pct: 50 },
|
|
157
|
+
},
|
|
158
|
+
}),
|
|
159
|
+
),
|
|
160
|
+
);
|
|
161
|
+
expect(parsed.coverage.lines).toEqual({ hit: 80, found: 100, pct: 80 });
|
|
162
|
+
expect(parsed.coverage.branches.pct).toBe(50);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
167
|
+
// formatJunit
|
|
168
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
169
|
+
|
|
170
|
+
describe("formatJunit", () => {
|
|
171
|
+
it("emits an XML document with the testsuites root", () => {
|
|
172
|
+
const out = formatJunit(makeReport());
|
|
173
|
+
expect(out.startsWith("<?xml")).toBe(true);
|
|
174
|
+
expect(out).toContain(`<testsuites name="unit"`);
|
|
175
|
+
expect(out).toContain(`tests="3"`);
|
|
176
|
+
expect(out).toContain(`failures="1"`);
|
|
177
|
+
expect(out).toContain(`skipped="1"`);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("buckets cases into per-suite <testsuite> blocks", () => {
|
|
181
|
+
const out = formatJunit(makeReport());
|
|
182
|
+
expect(out).toContain(`<testsuite name="math"`);
|
|
183
|
+
expect(out).toContain(`<testsuite name="misc"`);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("emits <failure> for failed tests with the error message", () => {
|
|
187
|
+
const out = formatJunit(makeReport());
|
|
188
|
+
expect(out).toContain(
|
|
189
|
+
`<failure message="Infinity, not NaN" type="AssertionError">`,
|
|
190
|
+
);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("escapes XML-dangerous characters in names and messages", () => {
|
|
194
|
+
const report = makeReport({
|
|
195
|
+
tests: [
|
|
196
|
+
{
|
|
197
|
+
name: `weird <name> & "quoted"`,
|
|
198
|
+
status: "failed",
|
|
199
|
+
durationMs: 1,
|
|
200
|
+
error: { message: `fail <body> & "q"` },
|
|
201
|
+
},
|
|
202
|
+
],
|
|
203
|
+
suite: "x",
|
|
204
|
+
kind: "unit",
|
|
205
|
+
});
|
|
206
|
+
const out = formatJunit(report);
|
|
207
|
+
expect(out).not.toContain(`weird <name>`);
|
|
208
|
+
expect(out).toContain(`<name>`);
|
|
209
|
+
expect(out).toContain(`"quoted"`);
|
|
210
|
+
expect(out).toContain(`fail <body>`);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("emits <skipped/> for skipped and todo tests", () => {
|
|
214
|
+
const out = formatJunit(makeReport());
|
|
215
|
+
expect(out).toContain(`<skipped/>`);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
220
|
+
// formatLcov
|
|
221
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
222
|
+
|
|
223
|
+
describe("formatLcov", () => {
|
|
224
|
+
it("returns empty string when no coverage is attached", () => {
|
|
225
|
+
expect(formatLcov(makeReport())).toBe("");
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("passes through lcovBody when present", () => {
|
|
229
|
+
const body = "SF:x.ts\nLF:10\nLH:7\nend_of_record\n";
|
|
230
|
+
const out = formatLcov(
|
|
231
|
+
makeReport({
|
|
232
|
+
coverage: { lines: { hit: 7, found: 10, pct: 70 }, lcovBody: body },
|
|
233
|
+
}),
|
|
234
|
+
);
|
|
235
|
+
expect(out).toBe(body);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("synthesizes a minimal summary when lcovBody is missing", () => {
|
|
239
|
+
const out = formatLcov(
|
|
240
|
+
makeReport({
|
|
241
|
+
coverage: { lines: { hit: 7, found: 10, pct: 70 } },
|
|
242
|
+
}),
|
|
243
|
+
);
|
|
244
|
+
expect(out).toContain("LF:10");
|
|
245
|
+
expect(out).toContain("LH:7");
|
|
246
|
+
expect(out).toContain("end_of_record");
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
251
|
+
// formatReport dispatch
|
|
252
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
253
|
+
|
|
254
|
+
describe("formatReport", () => {
|
|
255
|
+
it("dispatches to the correct formatter", () => {
|
|
256
|
+
const r = makeReport();
|
|
257
|
+
expect(formatReport(r, "human", { noColor: true })).toBe(
|
|
258
|
+
formatHuman(r, { noColor: true }),
|
|
259
|
+
);
|
|
260
|
+
expect(formatReport(r, "json")).toBe(formatJson(r));
|
|
261
|
+
expect(formatReport(r, "junit")).toBe(formatJunit(r));
|
|
262
|
+
expect(formatReport(r, "lcov")).toBe(formatLcov(r));
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
267
|
+
// mergeReports
|
|
268
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
269
|
+
|
|
270
|
+
describe("mergeReports", () => {
|
|
271
|
+
it("returns a synthetic empty report for zero inputs", () => {
|
|
272
|
+
const r = mergeReports();
|
|
273
|
+
expect(r.suite).toBe("combined");
|
|
274
|
+
expect(r.kind).toBe("combined");
|
|
275
|
+
expect(r.tests).toHaveLength(0);
|
|
276
|
+
expect(r.durationMs).toBe(0);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it("returns the input verbatim when called with one report", () => {
|
|
280
|
+
const r = makeReport();
|
|
281
|
+
expect(mergeReports(r)).toBe(r);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("concatenates tests and sums durations", () => {
|
|
285
|
+
const a = makeReport({ suite: "unit" });
|
|
286
|
+
const b = makeReport({
|
|
287
|
+
suite: "integration",
|
|
288
|
+
tests: [{ name: "it works", status: "passed", durationMs: 3 }],
|
|
289
|
+
durationMs: 3,
|
|
290
|
+
timestamp: "2026-04-20T12:00:00.000Z",
|
|
291
|
+
});
|
|
292
|
+
const merged = mergeReports(a, b);
|
|
293
|
+
expect(merged.suite).toBe("combined");
|
|
294
|
+
expect(merged.kind).toBe("combined");
|
|
295
|
+
expect(merged.tests).toHaveLength(4);
|
|
296
|
+
expect(merged.durationMs).toBe(10);
|
|
297
|
+
expect(merged.timestamp).toBe("2026-04-20T12:00:00.000Z");
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it("picks the last non-empty coverage block", () => {
|
|
301
|
+
const a = makeReport({
|
|
302
|
+
coverage: { lines: { hit: 5, found: 10, pct: 50 } },
|
|
303
|
+
});
|
|
304
|
+
const b = makeReport({
|
|
305
|
+
coverage: { lines: { hit: 8, found: 10, pct: 80 } },
|
|
306
|
+
});
|
|
307
|
+
const merged = mergeReports(a, b);
|
|
308
|
+
expect(merged.coverage?.lines?.pct).toBe(80);
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
313
|
+
// checkCoverageThresholds
|
|
314
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
315
|
+
|
|
316
|
+
describe("checkCoverageThresholds", () => {
|
|
317
|
+
const coverage = {
|
|
318
|
+
lines: { hit: 80, found: 100, pct: 80 },
|
|
319
|
+
branches: { hit: 30, found: 60, pct: 50 },
|
|
320
|
+
functions: { hit: 9, found: 10, pct: 90 },
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
it("passes when all metrics are at or above target", () => {
|
|
324
|
+
const res = checkCoverageThresholds(coverage, {
|
|
325
|
+
lines: 80,
|
|
326
|
+
branches: 50,
|
|
327
|
+
functions: 90,
|
|
328
|
+
});
|
|
329
|
+
expect(res.ok).toBe(true);
|
|
330
|
+
expect(res.breakdown).toHaveLength(3);
|
|
331
|
+
expect(res.breakdown.every((b) => b.ok)).toBe(true);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it("fails when a metric is below target", () => {
|
|
335
|
+
const res = checkCoverageThresholds(coverage, { branches: 80 });
|
|
336
|
+
expect(res.ok).toBe(false);
|
|
337
|
+
expect(res.breakdown).toHaveLength(1);
|
|
338
|
+
expect(res.breakdown[0]!.metric).toBe("branches");
|
|
339
|
+
expect(res.breakdown[0]!.ok).toBe(false);
|
|
340
|
+
expect(res.breakdown[0]!.actual).toBe(50);
|
|
341
|
+
expect(res.breakdown[0]!.expected).toBe(80);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it("returns ok=true and empty breakdown when thresholds undefined", () => {
|
|
345
|
+
const res = checkCoverageThresholds(coverage, undefined);
|
|
346
|
+
expect(res.ok).toBe(true);
|
|
347
|
+
expect(res.breakdown).toHaveLength(0);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it("treats missing metric as actual=0 when threshold is set", () => {
|
|
351
|
+
const res = checkCoverageThresholds(
|
|
352
|
+
{ lines: { hit: 80, found: 100, pct: 80 } },
|
|
353
|
+
{ statements: 50 },
|
|
354
|
+
);
|
|
355
|
+
expect(res.ok).toBe(false);
|
|
356
|
+
expect(res.breakdown[0]!.actual).toBe(0);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it("skips metrics with zero or negative thresholds", () => {
|
|
360
|
+
const res = checkCoverageThresholds(coverage, {
|
|
361
|
+
lines: 0,
|
|
362
|
+
branches: -5,
|
|
363
|
+
functions: 80,
|
|
364
|
+
});
|
|
365
|
+
expect(res.breakdown).toHaveLength(1);
|
|
366
|
+
expect(res.breakdown[0]!.metric).toBe("functions");
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it("tolerates float-precision near-match at the threshold", () => {
|
|
370
|
+
// 80.0 minus a sub-nanosecond rounding artefact still counts as meeting
|
|
371
|
+
// the threshold, thanks to the 1e-9 tolerance.
|
|
372
|
+
const res = checkCoverageThresholds(
|
|
373
|
+
{ lines: { hit: 80, found: 100, pct: 80 - 1e-12 } },
|
|
374
|
+
{ lines: 80 },
|
|
375
|
+
);
|
|
376
|
+
expect(res.ok).toBe(true);
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
381
|
+
// formatThresholdFailure
|
|
382
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
383
|
+
|
|
384
|
+
describe("formatThresholdFailure", () => {
|
|
385
|
+
it("lists every failing metric with actual/expected values", () => {
|
|
386
|
+
const res = checkCoverageThresholds(
|
|
387
|
+
{
|
|
388
|
+
lines: { hit: 5, found: 10, pct: 50 },
|
|
389
|
+
branches: { hit: 1, found: 10, pct: 10 },
|
|
390
|
+
},
|
|
391
|
+
{ lines: 80, branches: 50 },
|
|
392
|
+
);
|
|
393
|
+
const out = formatThresholdFailure(res);
|
|
394
|
+
expect(out).toContain("Coverage below threshold:");
|
|
395
|
+
expect(out).toContain("lines");
|
|
396
|
+
expect(out).toContain("50.00%");
|
|
397
|
+
expect(out).toContain("< 80%");
|
|
398
|
+
expect(out).toContain("branches");
|
|
399
|
+
expect(out).toContain("10.00%");
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it("returns empty string when no metric fails", () => {
|
|
403
|
+
const res = checkCoverageThresholds(
|
|
404
|
+
{ lines: { hit: 80, found: 100, pct: 80 } },
|
|
405
|
+
{ lines: 80 },
|
|
406
|
+
);
|
|
407
|
+
expect(formatThresholdFailure(res)).toBe("");
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
412
|
+
// parseLcovSummary
|
|
413
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
414
|
+
|
|
415
|
+
describe("parseLcovSummary", () => {
|
|
416
|
+
it("aggregates LF/LH/BRF/BRH/FNF/FNH across records", () => {
|
|
417
|
+
const body = [
|
|
418
|
+
"SF:a.ts",
|
|
419
|
+
"FNF:2",
|
|
420
|
+
"FNH:1",
|
|
421
|
+
"BRF:4",
|
|
422
|
+
"BRH:2",
|
|
423
|
+
"LF:10",
|
|
424
|
+
"LH:7",
|
|
425
|
+
"end_of_record",
|
|
426
|
+
"SF:b.ts",
|
|
427
|
+
"FNF:3",
|
|
428
|
+
"FNH:3",
|
|
429
|
+
"LF:5",
|
|
430
|
+
"LH:5",
|
|
431
|
+
"end_of_record",
|
|
432
|
+
].join("\n");
|
|
433
|
+
const c = parseLcovSummary(body);
|
|
434
|
+
expect(c.lines).toEqual({ hit: 12, found: 15, pct: 80 });
|
|
435
|
+
expect(c.branches).toEqual({ hit: 2, found: 4, pct: 50 });
|
|
436
|
+
expect(c.functions).toEqual({ hit: 4, found: 5, pct: 80 });
|
|
437
|
+
expect(c.files).toBe(2);
|
|
438
|
+
expect(c.lcovBody).toBe(body);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
it("omits metric blocks with zero records", () => {
|
|
442
|
+
const body = "SF:a.ts\nLF:0\nLH:0\nend_of_record\n";
|
|
443
|
+
const c = parseLcovSummary(body);
|
|
444
|
+
expect(c.lines).toBeUndefined();
|
|
445
|
+
expect(c.branches).toBeUndefined();
|
|
446
|
+
expect(c.functions).toBeUndefined();
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it("is tolerant to CRLF line endings", () => {
|
|
450
|
+
const body = "SF:a.ts\r\nLF:10\r\nLH:5\r\nend_of_record\r\n";
|
|
451
|
+
const c = parseLcovSummary(body);
|
|
452
|
+
expect(c.lines?.pct).toBe(50);
|
|
453
|
+
});
|
|
454
|
+
});
|
package/src/testing/index.ts
CHANGED
|
@@ -303,3 +303,32 @@ export {
|
|
|
303
303
|
type SnapshotOptions,
|
|
304
304
|
type SnapshotResult,
|
|
305
305
|
} from "./snapshot";
|
|
306
|
+
|
|
307
|
+
// ========== Phase 18.σ — Unified reporter ==========
|
|
308
|
+
|
|
309
|
+
export {
|
|
310
|
+
formatReport,
|
|
311
|
+
formatHuman,
|
|
312
|
+
formatJson,
|
|
313
|
+
formatJunit,
|
|
314
|
+
formatLcov,
|
|
315
|
+
mergeReports,
|
|
316
|
+
summarizeReport,
|
|
317
|
+
checkCoverageThresholds,
|
|
318
|
+
formatThresholdFailure,
|
|
319
|
+
parseLcovSummary,
|
|
320
|
+
emptyReport,
|
|
321
|
+
type TestReport,
|
|
322
|
+
type TestCase,
|
|
323
|
+
type TestStatus,
|
|
324
|
+
type TestSuiteKind,
|
|
325
|
+
type Coverage,
|
|
326
|
+
type CoverageMetric,
|
|
327
|
+
type CoverageMetricResult,
|
|
328
|
+
type CoverageThresholds,
|
|
329
|
+
type CoverageThresholdBreakdown,
|
|
330
|
+
type CoverageThresholdResult,
|
|
331
|
+
type ReporterFormat,
|
|
332
|
+
type ReportSummary,
|
|
333
|
+
type FormatOptions,
|
|
334
|
+
} from "./reporter";
|