dungbeetle 0.1.1
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 +105 -0
- package/NOTICE +19 -0
- package/README.md +139 -0
- package/dist/api/capture.d.ts +24 -0
- package/dist/api/capture.js +61 -0
- package/dist/baselines.d.ts +7 -0
- package/dist/baselines.js +38 -0
- package/dist/brand.d.ts +2 -0
- package/dist/brand.js +9 -0
- package/dist/capture.d.ts +15 -0
- package/dist/capture.js +7 -0
- package/dist/captures/api.d.ts +2 -0
- package/dist/captures/api.js +114 -0
- package/dist/captures/check.d.ts +2 -0
- package/dist/captures/check.js +116 -0
- package/dist/captures/desktop.d.ts +2 -0
- package/dist/captures/desktop.js +97 -0
- package/dist/captures/game.d.ts +4 -0
- package/dist/captures/game.js +266 -0
- package/dist/captures/performance.d.ts +2 -0
- package/dist/captures/performance.js +47 -0
- package/dist/captures/registry.d.ts +4 -0
- package/dist/captures/registry.js +23 -0
- package/dist/captures/terminal.d.ts +2 -0
- package/dist/captures/terminal.js +65 -0
- package/dist/captures/types.d.ts +18 -0
- package/dist/captures/types.js +1 -0
- package/dist/captures/web.d.ts +3 -0
- package/dist/captures/web.js +248 -0
- package/dist/check/capture.d.ts +15 -0
- package/dist/check/capture.js +76 -0
- package/dist/check/junit.d.ts +9 -0
- package/dist/check/junit.js +51 -0
- package/dist/check/laravel.d.ts +2 -0
- package/dist/check/laravel.js +44 -0
- package/dist/check/parsers.d.ts +12 -0
- package/dist/check/parsers.js +278 -0
- package/dist/check/schema.d.ts +2 -0
- package/dist/check/schema.js +114 -0
- package/dist/cloud.d.ts +42 -0
- package/dist/cloud.js +334 -0
- package/dist/compare/shared.d.ts +42 -0
- package/dist/compare/shared.js +115 -0
- package/dist/compare.d.ts +3 -0
- package/dist/compare.js +33 -0
- package/dist/config.d.ts +146 -0
- package/dist/config.js +382 -0
- package/dist/desktop/a11y.d.ts +18 -0
- package/dist/desktop/a11y.js +74 -0
- package/dist/desktop/capture.d.ts +13 -0
- package/dist/desktop/capture.js +80 -0
- package/dist/desktop/macos.d.ts +8 -0
- package/dist/desktop/macos.js +98 -0
- package/dist/desktop/ocr.d.ts +17 -0
- package/dist/desktop/ocr.js +99 -0
- package/dist/diff/lcs.d.ts +5 -0
- package/dist/diff/lcs.js +42 -0
- package/dist/diff/numeric.d.ts +6 -0
- package/dist/diff/numeric.js +24 -0
- package/dist/diff/pixel.d.ts +23 -0
- package/dist/diff/pixel.js +97 -0
- package/dist/diff/structural.d.ts +11 -0
- package/dist/diff/structural.js +38 -0
- package/dist/diff/text.d.ts +7 -0
- package/dist/diff/text.js +64 -0
- package/dist/diff/tree.d.ts +46 -0
- package/dist/diff/tree.js +188 -0
- package/dist/doctor.d.ts +18 -0
- package/dist/doctor.js +57 -0
- package/dist/game/capture.d.ts +24 -0
- package/dist/game/capture.js +51 -0
- package/dist/game/protocol.d.ts +30 -0
- package/dist/game/protocol.js +146 -0
- package/dist/game/walkthrough.d.ts +45 -0
- package/dist/game/walkthrough.js +85 -0
- package/dist/guards.d.ts +2 -0
- package/dist/guards.js +15 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +504 -0
- package/dist/json.d.ts +2 -0
- package/dist/json.js +40 -0
- package/dist/lifecycle.d.ts +14 -0
- package/dist/lifecycle.js +190 -0
- package/dist/normalization.d.ts +4 -0
- package/dist/normalization.js +27 -0
- package/dist/perf/ab.d.ts +6 -0
- package/dist/perf/ab.js +89 -0
- package/dist/perf/autocannon.d.ts +6 -0
- package/dist/perf/autocannon.js +101 -0
- package/dist/perf/capture.d.ts +7 -0
- package/dist/perf/capture.js +6 -0
- package/dist/perf/k6.d.ts +9 -0
- package/dist/perf/k6.js +44 -0
- package/dist/perf/parsers.d.ts +15 -0
- package/dist/perf/parsers.js +69 -0
- package/dist/perf/run.d.ts +8 -0
- package/dist/perf/run.js +45 -0
- package/dist/perf/toolOutput.d.ts +3 -0
- package/dist/perf/toolOutput.js +24 -0
- package/dist/reporters.d.ts +11 -0
- package/dist/reporters.js +314 -0
- package/dist/runner.d.ts +48 -0
- package/dist/runner.js +352 -0
- package/dist/snapshot.d.ts +48 -0
- package/dist/snapshot.js +37 -0
- package/dist/terminal/ansi.d.ts +21 -0
- package/dist/terminal/ansi.js +144 -0
- package/dist/terminal/capture.d.ts +30 -0
- package/dist/terminal/capture.js +91 -0
- package/dist/tty.d.ts +72 -0
- package/dist/tty.js +175 -0
- package/dist/web/domSnapshot.d.ts +27 -0
- package/dist/web/domSnapshot.js +55 -0
- package/dist/web/playwrightCapture.d.ts +16 -0
- package/dist/web/playwrightCapture.js +64 -0
- package/package.json +79 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { RunnerReport } from "./runner.js";
|
|
2
|
+
export type ConsoleReportOptions = {
|
|
3
|
+
/** Emit ANSI colour. Defaults to auto-detection of stdout. */
|
|
4
|
+
color?: boolean;
|
|
5
|
+
/** Emit emoji/unicode glyphs. Defaults to auto-detection of the terminal. */
|
|
6
|
+
unicode?: boolean;
|
|
7
|
+
};
|
|
8
|
+
export declare function renderConsoleReport(report: RunnerReport, options?: ConsoleReportOptions): string;
|
|
9
|
+
export declare function writeJsonReport(filePath: string, report: RunnerReport): Promise<void>;
|
|
10
|
+
export declare function renderHtmlReport(report: RunnerReport): string;
|
|
11
|
+
export declare function writeHtmlReport(filePath: string, report: RunnerReport): Promise<void>;
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { stableStringify } from "./json.js";
|
|
4
|
+
import { stripControlChars } from "./terminal/ansi.js";
|
|
5
|
+
import { BRAND_NAME } from "./brand.js";
|
|
6
|
+
import { colorEnabled, formatDuration, makePalette, makeSymbols, unicodeEnabled } from "./tty.js";
|
|
7
|
+
const STATUS_ORDER = ["passed", "updated", "failed", "missing", "error"];
|
|
8
|
+
export function renderConsoleReport(report, options = {}) {
|
|
9
|
+
const color = options.color ?? colorEnabled(process.stdout);
|
|
10
|
+
const unicode = options.unicode ?? unicodeEnabled();
|
|
11
|
+
const palette = makePalette(color);
|
|
12
|
+
const symbols = makeSymbols(unicode);
|
|
13
|
+
const headTone = report.passed ? palette.green : palette.red;
|
|
14
|
+
const headIcon = report.passed ? symbols.passed : symbols.failed;
|
|
15
|
+
const headWord = report.passed ? "PASS" : "FAIL";
|
|
16
|
+
const lines = [];
|
|
17
|
+
// Header banner: status, project, and mode.
|
|
18
|
+
lines.push(`${headIcon} ${palette.bold(headTone(headWord))} ${palette.dim(symbols.arrow)} ` +
|
|
19
|
+
`${palette.bold(report.project)} ${palette.dim(symbols.arrow)} ${report.mode}`);
|
|
20
|
+
// Summary counts, one coloured chip per non-zero status.
|
|
21
|
+
lines.push(`${palette.dim("Snapshots")} ${summaryCounts(report.results, palette)}`);
|
|
22
|
+
if (report.lifecycle.length > 0) {
|
|
23
|
+
lines.push(`${palette.dim("Lifecycle")} ${report.lifecycle
|
|
24
|
+
.map((run) => `${run.phase}${palette.dim(":")}${run.exitCode ?? "signal"}`)
|
|
25
|
+
.join(palette.dim(", "))}`);
|
|
26
|
+
}
|
|
27
|
+
for (const result of report.results) {
|
|
28
|
+
lines.push("");
|
|
29
|
+
lines.push(renderResultLine(result, palette, symbols));
|
|
30
|
+
lines.push(` ${palette.dim(result.baselinePath)}`);
|
|
31
|
+
if (result.error) {
|
|
32
|
+
lines.push(` ${palette.red(stripControlChars(result.error))}`);
|
|
33
|
+
}
|
|
34
|
+
if (result.diff) {
|
|
35
|
+
lines.push(colorizeDiff(result.diff, palette));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// Footer: overall result and elapsed wall-clock time.
|
|
39
|
+
const elapsed = elapsedMs(report.startedAt, report.finishedAt);
|
|
40
|
+
lines.push("");
|
|
41
|
+
lines.push(`${headIcon} ${palette.bold(headTone(headWord))} ` +
|
|
42
|
+
`${palette.dim(`${symbols.clock ? `${symbols.clock} ` : ""}${formatDuration(elapsed)}`)}`);
|
|
43
|
+
return lines.join("\n");
|
|
44
|
+
}
|
|
45
|
+
export async function writeJsonReport(filePath, report) {
|
|
46
|
+
const resolvedPath = path.resolve(filePath);
|
|
47
|
+
await mkdir(path.dirname(resolvedPath), { recursive: true });
|
|
48
|
+
// Screenshots are kept in the JSON report (not just the HTML) so `dungbeetle push`
|
|
49
|
+
// carries them to the server, where the run page renders them too.
|
|
50
|
+
await writeFile(resolvedPath, `${stableStringify(report)}\n`, "utf8");
|
|
51
|
+
}
|
|
52
|
+
export function renderHtmlReport(report) {
|
|
53
|
+
const status = report.passed ? "PASS" : "FAIL";
|
|
54
|
+
// Spot-check ergonomics: surface what needs attention (failed / missing /
|
|
55
|
+
// error) expanded at the top, and tuck passing/updated targets behind a
|
|
56
|
+
// collapsed <details> so a quick scan lands on the regressions, not the noise.
|
|
57
|
+
const attention = report.results.filter((result) => result.status === "failed" || result.status === "missing" || result.status === "error");
|
|
58
|
+
const ok = report.results.filter((result) => result.status === "passed" || result.status === "updated");
|
|
59
|
+
const lifecycleRows = report.lifecycle
|
|
60
|
+
.map((run) => {
|
|
61
|
+
return `<tr><td>${escapeHtml(run.phase)}</td><td><code>${escapeHtml(run.command)}</code></td><td>${escapeHtml(String(run.exitCode ?? "running"))}</td><td>${run.durationMs}</td></tr>`;
|
|
62
|
+
})
|
|
63
|
+
.join("\n");
|
|
64
|
+
const resultsHtml = report.results.length === 0
|
|
65
|
+
? "<p>No snapshot targets ran.</p>"
|
|
66
|
+
: [
|
|
67
|
+
attention.length > 0
|
|
68
|
+
? attention.map(renderResultSection).join("\n")
|
|
69
|
+
: "<p>No failing targets.</p>",
|
|
70
|
+
ok.length > 0
|
|
71
|
+
? `<details class="passing"><summary>${ok.length} passing/updated target${ok.length === 1 ? "" : "s"}</summary>\n${ok
|
|
72
|
+
.map(renderResultSection)
|
|
73
|
+
.join("\n")}\n</details>`
|
|
74
|
+
: ""
|
|
75
|
+
]
|
|
76
|
+
.filter((part) => part.length > 0)
|
|
77
|
+
.join("\n");
|
|
78
|
+
return `<!doctype html>
|
|
79
|
+
<html lang="en">
|
|
80
|
+
<head>
|
|
81
|
+
<meta charset="utf-8">
|
|
82
|
+
<title>${BRAND_NAME} ${escapeHtml(report.project)} ${escapeHtml(report.mode)} report</title>
|
|
83
|
+
<style>
|
|
84
|
+
/* Dark by default, matching the product's dark-first surfaces (docs,
|
|
85
|
+
cloud review UI); screenshots keep a light backing since captured
|
|
86
|
+
pages are usually light. */
|
|
87
|
+
:root { color-scheme: dark; font-family: system-ui, sans-serif; }
|
|
88
|
+
body { margin: 2rem; line-height: 1.5; background: #121514; color: #e8ecea; }
|
|
89
|
+
code, pre { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
|
90
|
+
a { color: #3ad17f; }
|
|
91
|
+
.summary { border: 1px solid #ffffff24; border-radius: 0.5rem; padding: 1rem; background: #191d1b; }
|
|
92
|
+
.chips { display: flex; flex-wrap: wrap; gap: 0.4rem; }
|
|
93
|
+
.status { border-radius: 999px; padding: 0.1rem 0.5rem; font-size: 0.8rem; }
|
|
94
|
+
.passed, .updated { background: #3ad17f26; color: #3ad17f; }
|
|
95
|
+
.failed, .missing, .error { background: #ff7b7226; color: #ff7b72; }
|
|
96
|
+
.result { border-top: 1px solid #ffffff24; margin-top: 1.5rem; padding-top: 1rem; }
|
|
97
|
+
.diff { background: #ffffff0d; border-radius: 0.5rem; overflow-x: auto; padding: 1rem; }
|
|
98
|
+
.diff .add { color: #3ad17f; }
|
|
99
|
+
.diff .del { color: #ff7b72; }
|
|
100
|
+
.diff .ctx { opacity: 0.75; }
|
|
101
|
+
.pixel { color: #9aa8a1; font-size: 0.95rem; }
|
|
102
|
+
details.passing { margin-top: 1.5rem; }
|
|
103
|
+
details.passing > summary { cursor: pointer; font-weight: 600; padding: 0.5rem 0; }
|
|
104
|
+
.screenshots { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 1rem; margin-top: 1rem; }
|
|
105
|
+
.screenshots figure { margin: 0; }
|
|
106
|
+
.screenshots figcaption { font-weight: 600; margin-bottom: 0.3rem; }
|
|
107
|
+
.screenshots img { width: 100%; height: auto; border: 1px solid #ffffff2e; border-radius: 0.5rem; background: #fff; }
|
|
108
|
+
table { border-collapse: collapse; width: 100%; }
|
|
109
|
+
th, td { border: 1px solid #ffffff24; padding: 0.4rem; text-align: left; }
|
|
110
|
+
</style>
|
|
111
|
+
</head>
|
|
112
|
+
<body>
|
|
113
|
+
<h1>${BRAND_NAME} ${escapeHtml(report.mode)} report: ${status}</h1>
|
|
114
|
+
<section class="summary">
|
|
115
|
+
<p><strong>Project:</strong> ${escapeHtml(report.project)}</p>
|
|
116
|
+
<p><strong>Started:</strong> ${escapeHtml(report.startedAt)}</p>
|
|
117
|
+
<p><strong>Finished:</strong> ${escapeHtml(report.finishedAt)}</p>
|
|
118
|
+
<p><strong>Snapshots:</strong></p>
|
|
119
|
+
<div class="chips">${summaryChipsHtml(report.results)}</div>
|
|
120
|
+
</section>
|
|
121
|
+
<h2>Lifecycle</h2>
|
|
122
|
+
${report.lifecycle.length > 0
|
|
123
|
+
? `<table><thead><tr><th>Phase</th><th>Command</th><th>Exit</th><th>Duration ms</th></tr></thead><tbody>${lifecycleRows}</tbody></table>`
|
|
124
|
+
: "<p>No lifecycle commands ran.</p>"}
|
|
125
|
+
<h2>Results</h2>
|
|
126
|
+
${resultsHtml}
|
|
127
|
+
</body>
|
|
128
|
+
</html>
|
|
129
|
+
`;
|
|
130
|
+
}
|
|
131
|
+
// One result block: status badge, baseline, error, the (colourised) diff, the
|
|
132
|
+
// pixel-diff summary when present, and the before/after/diff screenshots.
|
|
133
|
+
function renderResultSection(result) {
|
|
134
|
+
return [
|
|
135
|
+
'<section class="result">',
|
|
136
|
+
`<h2>${escapeHtml(result.kind)}:${escapeHtml(result.name)} <span class="status ${escapeHtml(result.status)}">${escapeHtml(result.status.toUpperCase())}</span></h2>`,
|
|
137
|
+
`<p><strong>Baseline:</strong> <code>${escapeHtml(result.baselinePath)}</code></p>`,
|
|
138
|
+
result.error ? `<p class="error">${escapeHtml(result.error)}</p>` : "",
|
|
139
|
+
result.diff ? renderDiffHtml(result.diff) : "<p>No diff.</p>",
|
|
140
|
+
result.pixel ? renderPixelSummary(result.pixel) : "",
|
|
141
|
+
renderScreenshots(result.screenshot),
|
|
142
|
+
renderNamedScreenshots(result.screenshots),
|
|
143
|
+
"</section>"
|
|
144
|
+
].join("\n");
|
|
145
|
+
}
|
|
146
|
+
// Named per-marker screenshots (game targets): one before/after/diff block per
|
|
147
|
+
// marker, headed by the marker name, in stable alphabetical order.
|
|
148
|
+
function renderNamedScreenshots(screenshots) {
|
|
149
|
+
if (!screenshots) {
|
|
150
|
+
return "";
|
|
151
|
+
}
|
|
152
|
+
return Object.keys(screenshots)
|
|
153
|
+
.sort()
|
|
154
|
+
.map((marker) => [
|
|
155
|
+
`<h3 class="marker">${escapeHtml(marker)}</h3>`,
|
|
156
|
+
renderScreenshots(screenshots[marker])
|
|
157
|
+
].join("\n"))
|
|
158
|
+
.join("\n");
|
|
159
|
+
}
|
|
160
|
+
// Colour the diff like the console — additions green, removals red, context
|
|
161
|
+
// dimmed — by the leading marker, so a regression reads at a glance. Each line is
|
|
162
|
+
// escaped, then wrapped in a span; the escaped content is preserved verbatim.
|
|
163
|
+
function renderDiffHtml(diff) {
|
|
164
|
+
const body = diff
|
|
165
|
+
.split("\n")
|
|
166
|
+
.map((line) => {
|
|
167
|
+
const trimmed = line.trimStart();
|
|
168
|
+
const cls = trimmed.startsWith("+") || trimmed.startsWith("{+")
|
|
169
|
+
? "add"
|
|
170
|
+
: trimmed.startsWith("-") || trimmed.startsWith("[-")
|
|
171
|
+
? "del"
|
|
172
|
+
: "ctx";
|
|
173
|
+
return `<span class="${cls}">${escapeHtml(line)}</span>`;
|
|
174
|
+
})
|
|
175
|
+
.join("\n");
|
|
176
|
+
return `<pre class="diff">${body}</pre>`;
|
|
177
|
+
}
|
|
178
|
+
// A compact, scannable read of a screenshot pixel diff (web targets): how many
|
|
179
|
+
// pixels changed, the ratio, whether it cleared tolerance, and a dimension note.
|
|
180
|
+
function renderPixelSummary(pixel) {
|
|
181
|
+
const changed = pixel.changedPixels.toLocaleString("en-US");
|
|
182
|
+
const total = pixel.totalPixels.toLocaleString("en-US");
|
|
183
|
+
const pct = (pixel.changedRatio * 100).toFixed(2);
|
|
184
|
+
const tolerance = pixel.withinTolerance ? "within tolerance" : "over tolerance";
|
|
185
|
+
const dims = pixel.dimensionsMatch ? `${pixel.width}×${pixel.height}` : "dimensions differ";
|
|
186
|
+
return `<p class="pixel"><strong>Pixels:</strong> ${changed} / ${total} changed · ${pct}% · ${escapeHtml(tolerance)} · ${escapeHtml(dims)}</p>`;
|
|
187
|
+
}
|
|
188
|
+
// Coloured status chips for the summary, one per non-empty status.
|
|
189
|
+
function summaryChipsHtml(results) {
|
|
190
|
+
const counts = new Map();
|
|
191
|
+
for (const result of results) {
|
|
192
|
+
counts.set(result.status, (counts.get(result.status) ?? 0) + 1);
|
|
193
|
+
}
|
|
194
|
+
const chips = STATUS_ORDER.filter((status) => counts.has(status)).map((status) => `<span class="status ${status}">${escapeHtml(status)} ${counts.get(status)}</span>`);
|
|
195
|
+
return chips.length > 0 ? chips.join("\n") : '<span class="dim">none</span>';
|
|
196
|
+
}
|
|
197
|
+
export async function writeHtmlReport(filePath, report) {
|
|
198
|
+
const resolvedPath = path.resolve(filePath);
|
|
199
|
+
await mkdir(path.dirname(resolvedPath), { recursive: true });
|
|
200
|
+
await writeFile(resolvedPath, renderHtmlReport(report), "utf8");
|
|
201
|
+
}
|
|
202
|
+
function statusTone(status, palette) {
|
|
203
|
+
switch (status) {
|
|
204
|
+
case "passed":
|
|
205
|
+
return palette.green;
|
|
206
|
+
case "updated":
|
|
207
|
+
return palette.cyan;
|
|
208
|
+
case "missing":
|
|
209
|
+
return palette.yellow;
|
|
210
|
+
case "failed":
|
|
211
|
+
case "error":
|
|
212
|
+
return palette.red;
|
|
213
|
+
default:
|
|
214
|
+
return (value) => value;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
function statusIcon(status, symbols) {
|
|
218
|
+
switch (status) {
|
|
219
|
+
case "passed":
|
|
220
|
+
return symbols.passed;
|
|
221
|
+
case "updated":
|
|
222
|
+
return symbols.updated;
|
|
223
|
+
case "missing":
|
|
224
|
+
return symbols.missing;
|
|
225
|
+
case "error":
|
|
226
|
+
return symbols.error;
|
|
227
|
+
default:
|
|
228
|
+
return symbols.failed;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
function renderResultLine(result, palette, symbols) {
|
|
232
|
+
const tone = statusTone(result.status, palette);
|
|
233
|
+
const icon = statusIcon(result.status, symbols);
|
|
234
|
+
const label = `${result.kind}${palette.dim(":")}${palette.bold(result.name)}`;
|
|
235
|
+
return `${icon} ${tone(result.status.toUpperCase())} ${label}`;
|
|
236
|
+
}
|
|
237
|
+
function summaryCounts(results, palette) {
|
|
238
|
+
const counts = new Map();
|
|
239
|
+
for (const result of results) {
|
|
240
|
+
counts.set(result.status, (counts.get(result.status) ?? 0) + 1);
|
|
241
|
+
}
|
|
242
|
+
const chips = STATUS_ORDER.filter((status) => counts.has(status)).map((status) => {
|
|
243
|
+
const tone = statusTone(status, palette);
|
|
244
|
+
return `${tone(status)}${palette.dim("=")}${tone(String(counts.get(status)))}`;
|
|
245
|
+
});
|
|
246
|
+
return chips.length > 0 ? chips.join(" ") : palette.dim("none");
|
|
247
|
+
}
|
|
248
|
+
// Colour diff lines by their leading marker: additions green, removals red,
|
|
249
|
+
// and inline word markers ({+ +} / [- -]) tinted too, so failures read at a
|
|
250
|
+
// glance. Falls back to plain indented text when colour is disabled.
|
|
251
|
+
function colorizeDiff(diff, palette) {
|
|
252
|
+
// The baseline side of the diff is read verbatim from a committed file, so it
|
|
253
|
+
// can carry raw terminal escapes; strip them before printing to the reviewer.
|
|
254
|
+
return stripControlChars(diff)
|
|
255
|
+
.split("\n")
|
|
256
|
+
.map((line) => {
|
|
257
|
+
const trimmed = line.trimStart();
|
|
258
|
+
let rendered = line;
|
|
259
|
+
if (trimmed.startsWith("+") || trimmed.startsWith("{+")) {
|
|
260
|
+
rendered = palette.green(line);
|
|
261
|
+
}
|
|
262
|
+
else if (trimmed.startsWith("-") || trimmed.startsWith("[-")) {
|
|
263
|
+
rendered = palette.red(line);
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
rendered = palette.dim(line);
|
|
267
|
+
}
|
|
268
|
+
return ` ${rendered}`;
|
|
269
|
+
})
|
|
270
|
+
.join("\n");
|
|
271
|
+
}
|
|
272
|
+
function elapsedMs(startedAt, finishedAt) {
|
|
273
|
+
const start = Date.parse(startedAt);
|
|
274
|
+
const end = Date.parse(finishedAt);
|
|
275
|
+
if (Number.isNaN(start) || Number.isNaN(end)) {
|
|
276
|
+
return 0;
|
|
277
|
+
}
|
|
278
|
+
return end - start;
|
|
279
|
+
}
|
|
280
|
+
// Embed the before/after (and optional diff) screenshots as inline data URIs so
|
|
281
|
+
// the HTML report stays a single self-contained file. Returns an empty string
|
|
282
|
+
// when the result has no screenshots (non-web targets, passing tests).
|
|
283
|
+
function renderScreenshots(screenshot) {
|
|
284
|
+
if (!screenshot) {
|
|
285
|
+
return "";
|
|
286
|
+
}
|
|
287
|
+
const figures = [
|
|
288
|
+
figureFor("Before", screenshot.baseline),
|
|
289
|
+
figureFor("After", screenshot.candidate),
|
|
290
|
+
figureFor("Diff", screenshot.diff)
|
|
291
|
+
].filter((figure) => figure.length > 0);
|
|
292
|
+
if (figures.length === 0) {
|
|
293
|
+
return "";
|
|
294
|
+
}
|
|
295
|
+
return `<div class="screenshots">${figures.join("\n")}</div>`;
|
|
296
|
+
}
|
|
297
|
+
function figureFor(caption, base64) {
|
|
298
|
+
if (!base64) {
|
|
299
|
+
return "";
|
|
300
|
+
}
|
|
301
|
+
return [
|
|
302
|
+
"<figure>",
|
|
303
|
+
`<figcaption>${escapeHtml(caption)}</figcaption>`,
|
|
304
|
+
`<img alt="${escapeHtml(caption)} screenshot" src="data:image/png;base64,${base64}">`,
|
|
305
|
+
"</figure>"
|
|
306
|
+
].join("\n");
|
|
307
|
+
}
|
|
308
|
+
function escapeHtml(value) {
|
|
309
|
+
return value
|
|
310
|
+
.replace(/&/g, "&")
|
|
311
|
+
.replace(/</g, "<")
|
|
312
|
+
.replace(/>/g, ">")
|
|
313
|
+
.replace(/"/g, """);
|
|
314
|
+
}
|
package/dist/runner.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type ScreenshotImages } from "./compare.js";
|
|
2
|
+
import type { CaptureTarget, DungbeetleConfig, ComparisonConfig } from "./config.js";
|
|
3
|
+
import { type PixelDiffResult } from "./diff/pixel.js";
|
|
4
|
+
import { type LifecycleRun } from "./lifecycle.js";
|
|
5
|
+
import { type SnapshotArtifact } from "./capture.js";
|
|
6
|
+
export type SnapshotStatus = "passed" | "failed" | "missing" | "updated" | "error";
|
|
7
|
+
export type ScreenshotComparison = {
|
|
8
|
+
baseline?: string;
|
|
9
|
+
candidate?: string;
|
|
10
|
+
diff?: string;
|
|
11
|
+
};
|
|
12
|
+
export type SnapshotResult = {
|
|
13
|
+
name: string;
|
|
14
|
+
kind: CaptureTarget["kind"];
|
|
15
|
+
status: SnapshotStatus;
|
|
16
|
+
baselinePath: string;
|
|
17
|
+
diff: string;
|
|
18
|
+
pixel?: PixelDiffResult;
|
|
19
|
+
screenshot?: ScreenshotComparison;
|
|
20
|
+
screenshots?: Record<string, ScreenshotComparison>;
|
|
21
|
+
snapshot?: string;
|
|
22
|
+
error?: string;
|
|
23
|
+
};
|
|
24
|
+
export type RunnerReport = {
|
|
25
|
+
kind: "dungbeetle-run";
|
|
26
|
+
mode: "test" | "update";
|
|
27
|
+
passed: boolean;
|
|
28
|
+
project: string;
|
|
29
|
+
startedAt: string;
|
|
30
|
+
finishedAt: string;
|
|
31
|
+
lifecycle: LifecycleRun[];
|
|
32
|
+
results: SnapshotResult[];
|
|
33
|
+
};
|
|
34
|
+
export declare function updateBaselines(options: {
|
|
35
|
+
config: DungbeetleConfig;
|
|
36
|
+
cwd?: string;
|
|
37
|
+
targets?: string[];
|
|
38
|
+
includeSnapshots?: boolean;
|
|
39
|
+
}): Promise<RunnerReport>;
|
|
40
|
+
export declare function testBaselines(options: {
|
|
41
|
+
config: DungbeetleConfig;
|
|
42
|
+
cwd?: string;
|
|
43
|
+
targets?: string[];
|
|
44
|
+
includeSnapshots?: boolean;
|
|
45
|
+
}): Promise<RunnerReport>;
|
|
46
|
+
export declare function namedScreenshotBuffers(snapshot: SnapshotArtifact): Record<string, Buffer>;
|
|
47
|
+
export declare function buildScreenshotComparison(baseline: Buffer | undefined, candidate: Buffer, pixelTolerance: ComparisonConfig["pixelTolerance"], includeDiff: boolean, images?: ScreenshotImages): ScreenshotComparison | undefined;
|
|
48
|
+
export declare function screenshotBuffer(snapshot: SnapshotArtifact): Buffer | undefined;
|