playwright-checkpoint 0.1.0-beta.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/LICENSE +21 -0
- package/README.md +665 -0
- package/dist/chunk-DGUM43GV.js +11 -0
- package/dist/chunk-DGUM43GV.js.map +1 -0
- package/dist/chunk-F5A6XGLJ.js +104 -0
- package/dist/chunk-F5A6XGLJ.js.map +1 -0
- package/dist/chunk-K5DX32TO.js +214 -0
- package/dist/chunk-K5DX32TO.js.map +1 -0
- package/dist/chunk-KG37WSYS.js +1549 -0
- package/dist/chunk-KG37WSYS.js.map +1 -0
- package/dist/chunk-X5IPL32H.js +1484 -0
- package/dist/chunk-X5IPL32H.js.map +1 -0
- package/dist/cli/bin.cjs +3972 -0
- package/dist/cli/bin.cjs.map +1 -0
- package/dist/cli/bin.d.cts +1 -0
- package/dist/cli/bin.d.ts +1 -0
- package/dist/cli/bin.js +43 -0
- package/dist/cli/bin.js.map +1 -0
- package/dist/cli/index.cjs +1672 -0
- package/dist/cli/index.cjs.map +1 -0
- package/dist/cli/index.d.cts +31 -0
- package/dist/cli/index.d.ts +31 -0
- package/dist/cli/index.js +17 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/mcp-args.cjs +129 -0
- package/dist/cli/mcp-args.cjs.map +1 -0
- package/dist/cli/mcp-args.d.cts +32 -0
- package/dist/cli/mcp-args.d.ts +32 -0
- package/dist/cli/mcp-args.js +10 -0
- package/dist/cli/mcp-args.js.map +1 -0
- package/dist/components.cjs +53 -0
- package/dist/components.cjs.map +1 -0
- package/dist/components.d.cts +27 -0
- package/dist/components.d.ts +27 -0
- package/dist/components.js +26 -0
- package/dist/components.js.map +1 -0
- package/dist/core-CD4jHGgI.d.cts +51 -0
- package/dist/core-CZvnc0rE.d.ts +51 -0
- package/dist/core.cjs +1576 -0
- package/dist/core.cjs.map +1 -0
- package/dist/core.d.cts +3 -0
- package/dist/core.d.ts +3 -0
- package/dist/core.js +32 -0
- package/dist/core.js.map +1 -0
- package/dist/index-BjYQX_hK.d.ts +8 -0
- package/dist/index-Cabk31qi.d.cts +8 -0
- package/dist/index.cjs +3318 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +94 -0
- package/dist/index.d.ts +94 -0
- package/dist/index.js +285 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/index.cjs +3467 -0
- package/dist/mcp/index.cjs.map +1 -0
- package/dist/mcp/index.d.cts +26 -0
- package/dist/mcp/index.d.ts +26 -0
- package/dist/mcp/index.js +586 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/teardown.cjs +1509 -0
- package/dist/teardown.cjs.map +1 -0
- package/dist/teardown.d.cts +5 -0
- package/dist/teardown.d.ts +5 -0
- package/dist/teardown.js +52 -0
- package/dist/teardown.js.map +1 -0
- package/dist/types-G7w4n8kR.d.cts +359 -0
- package/dist/types-G7w4n8kR.d.ts +359 -0
- package/package.json +109 -0
|
@@ -0,0 +1,1509 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/teardown.ts
|
|
31
|
+
var teardown_exports = {};
|
|
32
|
+
__export(teardown_exports, {
|
|
33
|
+
default: () => teardown_default,
|
|
34
|
+
globalTeardown: () => globalTeardown
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(teardown_exports);
|
|
37
|
+
var import_node_fs = __toESM(require("fs"), 1);
|
|
38
|
+
var import_node_path6 = __toESM(require("path"), 1);
|
|
39
|
+
|
|
40
|
+
// src/cli/index.ts
|
|
41
|
+
var import_node_path5 = __toESM(require("path"), 1);
|
|
42
|
+
|
|
43
|
+
// src/report/index.ts
|
|
44
|
+
var import_promises4 = __toESM(require("fs/promises"), 1);
|
|
45
|
+
var import_node_path4 = __toESM(require("path"), 1);
|
|
46
|
+
|
|
47
|
+
// src/report/html-reporter.ts
|
|
48
|
+
var import_promises = __toESM(require("fs/promises"), 1);
|
|
49
|
+
var import_node_path = __toESM(require("path"), 1);
|
|
50
|
+
|
|
51
|
+
// src/report/story-utils.ts
|
|
52
|
+
function groupByStory(runs) {
|
|
53
|
+
const stories = /* @__PURE__ */ new Map();
|
|
54
|
+
for (const run of runs) {
|
|
55
|
+
const existing = stories.get(run.title) ?? [];
|
|
56
|
+
existing.push(run);
|
|
57
|
+
stories.set(run.title, existing);
|
|
58
|
+
}
|
|
59
|
+
return stories;
|
|
60
|
+
}
|
|
61
|
+
function orderedCheckpointNames(runs) {
|
|
62
|
+
const names = [];
|
|
63
|
+
const seen = /* @__PURE__ */ new Set();
|
|
64
|
+
for (const run of runs) {
|
|
65
|
+
for (const checkpoint of run.checkpoints) {
|
|
66
|
+
if (seen.has(checkpoint.name)) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
seen.add(checkpoint.name);
|
|
70
|
+
names.push(checkpoint.name);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return names;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// src/report/html-reporter.ts
|
|
77
|
+
var DEFAULT_PROJECT_ORDER = ["desktop-light", "desktop-dark", "mobile-light", "mobile-dark"];
|
|
78
|
+
function escapeHtml(value) {
|
|
79
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
80
|
+
}
|
|
81
|
+
function slugify(value) {
|
|
82
|
+
return value.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "story";
|
|
83
|
+
}
|
|
84
|
+
function formatDateTime(isoDate) {
|
|
85
|
+
const date = new Date(isoDate);
|
|
86
|
+
if (Number.isNaN(date.getTime())) {
|
|
87
|
+
return isoDate;
|
|
88
|
+
}
|
|
89
|
+
return new Intl.DateTimeFormat("en-US", {
|
|
90
|
+
dateStyle: "medium",
|
|
91
|
+
timeStyle: "short"
|
|
92
|
+
}).format(date);
|
|
93
|
+
}
|
|
94
|
+
function projectWeight(projectName, projectOrder) {
|
|
95
|
+
const index = projectOrder.indexOf(projectName);
|
|
96
|
+
return index === -1 ? Number.MAX_SAFE_INTEGER : index;
|
|
97
|
+
}
|
|
98
|
+
function formatProjectLabel(projectName) {
|
|
99
|
+
const [device, mode] = projectName.split("-");
|
|
100
|
+
if (!device || !mode) {
|
|
101
|
+
return projectName;
|
|
102
|
+
}
|
|
103
|
+
const deviceLabel = device === "desktop" ? "Desktop" : device === "mobile" ? "Mobile" : device;
|
|
104
|
+
const modeLabel = mode === "light" ? "Light" : mode === "dark" ? "Dark" : mode;
|
|
105
|
+
return `${deviceLabel} / ${modeLabel}`;
|
|
106
|
+
}
|
|
107
|
+
function sortByProjectAndTime(a, b, projectOrder) {
|
|
108
|
+
const byProject = projectWeight(a.project, projectOrder) - projectWeight(b.project, projectOrder);
|
|
109
|
+
if (byProject !== 0) {
|
|
110
|
+
return byProject;
|
|
111
|
+
}
|
|
112
|
+
const byProjectName = a.project.localeCompare(b.project);
|
|
113
|
+
if (byProjectName !== 0) {
|
|
114
|
+
return byProjectName;
|
|
115
|
+
}
|
|
116
|
+
return new Date(a.startedAt).getTime() - new Date(b.startedAt).getTime();
|
|
117
|
+
}
|
|
118
|
+
function getCollectorSummaryNumber(checkpoint, collectorName, key) {
|
|
119
|
+
const value = checkpoint.collectors[collectorName]?.summary[key];
|
|
120
|
+
return typeof value === "number" ? value : null;
|
|
121
|
+
}
|
|
122
|
+
function screenshotData(checkpoint) {
|
|
123
|
+
const data = checkpoint.collectors.screenshot?.data;
|
|
124
|
+
return data && typeof data === "object" ? data : null;
|
|
125
|
+
}
|
|
126
|
+
function highlightOverlayStyle(checkpoint) {
|
|
127
|
+
const data = screenshotData(checkpoint);
|
|
128
|
+
const bounds = data?.highlightBounds;
|
|
129
|
+
const imageSize = data?.imageSize;
|
|
130
|
+
if (!bounds || !imageSize || typeof bounds.x !== "number" || typeof bounds.y !== "number" || typeof bounds.width !== "number" || typeof bounds.height !== "number" || typeof imageSize.width !== "number" || typeof imageSize.height !== "number" || imageSize.width <= 0 || imageSize.height <= 0) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
const left = bounds.x / imageSize.width * 100;
|
|
134
|
+
const top = bounds.y / imageSize.height * 100;
|
|
135
|
+
const width = bounds.width / imageSize.width * 100;
|
|
136
|
+
const height = bounds.height / imageSize.height * 100;
|
|
137
|
+
return [
|
|
138
|
+
`left:${left.toFixed(4)}%`,
|
|
139
|
+
`top:${top.toFixed(4)}%`,
|
|
140
|
+
`width:${width.toFixed(4)}%`,
|
|
141
|
+
`height:${height.toFixed(4)}%`
|
|
142
|
+
].join(";");
|
|
143
|
+
}
|
|
144
|
+
function highlightLabel(checkpoint) {
|
|
145
|
+
const selector = screenshotData(checkpoint)?.highlightSelector;
|
|
146
|
+
return typeof selector === "string" && selector.trim().length > 0 ? `Focus: ${selector.trim()}` : null;
|
|
147
|
+
}
|
|
148
|
+
function resolveArtifactPath(run, artifactPath) {
|
|
149
|
+
return import_node_path.default.isAbsolute(artifactPath) ? artifactPath : import_node_path.default.resolve(import_node_path.default.dirname(run.sourceManifestPath), artifactPath);
|
|
150
|
+
}
|
|
151
|
+
function toEncodedHref(outputDir, filePath) {
|
|
152
|
+
if (!filePath) {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
const relativePath = import_node_path.default.relative(outputDir, filePath);
|
|
156
|
+
return relativePath.split(import_node_path.default.sep).map(encodeURIComponent).join("/");
|
|
157
|
+
}
|
|
158
|
+
function getArtifactHref(run, checkpoint, outputDir, collectorName, artifactName) {
|
|
159
|
+
const artifacts = checkpoint.collectors[collectorName]?.artifacts ?? [];
|
|
160
|
+
const artifact = artifactName ? artifacts.find((entry) => entry.name === artifactName) : artifacts[0];
|
|
161
|
+
if (!artifact?.path) {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
return toEncodedHref(outputDir, resolveArtifactPath(run, artifact.path));
|
|
165
|
+
}
|
|
166
|
+
function renderArtifactLinks(run, checkpoint, outputDir) {
|
|
167
|
+
const links = [
|
|
168
|
+
{ label: "DOM HTML", href: getArtifactHref(run, checkpoint, outputDir, "html", "html") },
|
|
169
|
+
{ label: "Axe", href: getArtifactHref(run, checkpoint, outputDir, "axe", "axe") },
|
|
170
|
+
{ label: "Web Vitals", href: getArtifactHref(run, checkpoint, outputDir, "web-vitals", "web-vitals") },
|
|
171
|
+
{ label: "Console", href: getArtifactHref(run, checkpoint, outputDir, "console", "console-errors") },
|
|
172
|
+
{ label: "Failed Requests", href: getArtifactHref(run, checkpoint, outputDir, "network", "failed-requests") }
|
|
173
|
+
];
|
|
174
|
+
return links.map((link) => {
|
|
175
|
+
if (!link.href) {
|
|
176
|
+
return `<span class="artifact disabled">${escapeHtml(link.label)}</span>`;
|
|
177
|
+
}
|
|
178
|
+
return `<a class="artifact" href="${link.href}" target="_blank" rel="noreferrer">${escapeHtml(link.label)}</a>`;
|
|
179
|
+
}).join("");
|
|
180
|
+
}
|
|
181
|
+
function renderCheckpointCard(run, checkpointName, outputDir) {
|
|
182
|
+
const checkpoint = run.checkpoints.find((entry) => entry.name === checkpointName);
|
|
183
|
+
if (!checkpoint) {
|
|
184
|
+
return `
|
|
185
|
+
<article class="variant-card missing">
|
|
186
|
+
<header class="variant-card-header">
|
|
187
|
+
<div>
|
|
188
|
+
<h5>${escapeHtml(formatProjectLabel(run.project))}</h5>
|
|
189
|
+
<p>${escapeHtml(run.project)}</p>
|
|
190
|
+
</div>
|
|
191
|
+
<time>${escapeHtml(formatDateTime(run.startedAt))}</time>
|
|
192
|
+
</header>
|
|
193
|
+
<div class="empty-card">No checkpoint captured for this run.</div>
|
|
194
|
+
</article>
|
|
195
|
+
`;
|
|
196
|
+
}
|
|
197
|
+
const screenshotHref = getArtifactHref(run, checkpoint, outputDir, "screenshot", "screenshot");
|
|
198
|
+
const overlayStyle = highlightOverlayStyle(checkpoint);
|
|
199
|
+
const focus = highlightLabel(checkpoint);
|
|
200
|
+
const axeViolations = getCollectorSummaryNumber(checkpoint, "axe", "violations");
|
|
201
|
+
const consoleErrors = getCollectorSummaryNumber(checkpoint, "console", "consoleErrorCount") ?? 0;
|
|
202
|
+
const failedRequests = getCollectorSummaryNumber(checkpoint, "network", "failedRequestCount") ?? 0;
|
|
203
|
+
return `
|
|
204
|
+
<article class="variant-card">
|
|
205
|
+
<header class="variant-card-header">
|
|
206
|
+
<div>
|
|
207
|
+
<h5>${escapeHtml(formatProjectLabel(run.project))}</h5>
|
|
208
|
+
<p>${escapeHtml(run.project)}</p>
|
|
209
|
+
</div>
|
|
210
|
+
<time>${escapeHtml(formatDateTime(checkpoint.timestamp || run.startedAt))}</time>
|
|
211
|
+
</header>
|
|
212
|
+
<p class="page-meta">
|
|
213
|
+
<span>${escapeHtml(checkpoint.title || "Untitled page")}</span>
|
|
214
|
+
<span class="page-url">${escapeHtml(checkpoint.url)}</span>
|
|
215
|
+
</p>
|
|
216
|
+
${screenshotHref ? `<a class="thumbnail-link" href="${screenshotHref}" target="_blank" rel="noreferrer">
|
|
217
|
+
<img src="${screenshotHref}" alt="${escapeHtml(`${run.project} \u2014 ${checkpoint.name}`)}" loading="lazy" />
|
|
218
|
+
${overlayStyle ? `<span class="highlight-overlay" style="${overlayStyle}" aria-hidden="true"></span>` : ""}
|
|
219
|
+
${focus ? `<span class="highlight-label">${escapeHtml(focus)}</span>` : ""}
|
|
220
|
+
</a>` : '<div class="empty-card">Screenshot unavailable.</div>'}
|
|
221
|
+
<div class="stats-grid">
|
|
222
|
+
<span><strong>${axeViolations ?? "n/a"}</strong><small>Axe violations</small></span>
|
|
223
|
+
<span><strong>${consoleErrors}</strong><small>Console errors</small></span>
|
|
224
|
+
<span><strong>${failedRequests}</strong><small>Failed requests</small></span>
|
|
225
|
+
</div>
|
|
226
|
+
<div class="artifact-list">${renderArtifactLinks(run, checkpoint, outputDir)}</div>
|
|
227
|
+
</article>
|
|
228
|
+
`;
|
|
229
|
+
}
|
|
230
|
+
function renderStorySection(title, runs, outputDir) {
|
|
231
|
+
const checkpointNames = orderedCheckpointNames(runs);
|
|
232
|
+
const environments = [...new Set(runs.map((run) => run.environment))].sort();
|
|
233
|
+
const tags = [...new Set(runs.flatMap((run) => run.tags))].sort();
|
|
234
|
+
const checkpointBlocks = checkpointNames.map(
|
|
235
|
+
(checkpointName) => `
|
|
236
|
+
<details class="accordion checkpoint-block">
|
|
237
|
+
<summary class="checkpoint-summary">
|
|
238
|
+
<span>${escapeHtml(checkpointName)}</span>
|
|
239
|
+
<span class="checkpoint-meta">${runs.length} variants</span>
|
|
240
|
+
</summary>
|
|
241
|
+
<div class="variant-grid">
|
|
242
|
+
${runs.map((run) => renderCheckpointCard(run, checkpointName, outputDir)).join("")}
|
|
243
|
+
</div>
|
|
244
|
+
</details>
|
|
245
|
+
`
|
|
246
|
+
).join("");
|
|
247
|
+
return `
|
|
248
|
+
<details class="accordion story-block" id="story-${slugify(title)}" open>
|
|
249
|
+
<summary class="story-summary">
|
|
250
|
+
<span class="story-title">${escapeHtml(title)}</span>
|
|
251
|
+
<span class="story-meta-chip">${runs.length} run${runs.length === 1 ? "" : "s"}</span>
|
|
252
|
+
</summary>
|
|
253
|
+
<div class="story-body">
|
|
254
|
+
<div class="story-meta-row">
|
|
255
|
+
<span><strong>Projects</strong> ${escapeHtml(runs.map((run) => run.project).join(", "))}</span>
|
|
256
|
+
<span><strong>Environments</strong> ${escapeHtml(environments.join(", ") || "n/a")}</span>
|
|
257
|
+
<span><strong>Tags</strong> ${escapeHtml(tags.join(", ") || "none")}</span>
|
|
258
|
+
</div>
|
|
259
|
+
${checkpointBlocks || '<p class="empty-state">No checkpoints captured for this story.</p>'}
|
|
260
|
+
</div>
|
|
261
|
+
</details>
|
|
262
|
+
`;
|
|
263
|
+
}
|
|
264
|
+
function buildHtmlReport(runs, outputDir, config) {
|
|
265
|
+
const groupedRuns = groupByStory(runs);
|
|
266
|
+
const storyTitles = [...groupedRuns.keys()].sort((a, b) => a.localeCompare(b));
|
|
267
|
+
const projectOrder = Array.isArray(config.projectOrder) ? config.projectOrder.filter((value) => typeof value === "string") : DEFAULT_PROJECT_ORDER;
|
|
268
|
+
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
269
|
+
const reportTitle = typeof config.title === "string" && config.title.trim() ? config.title.trim() : "Playwright Checkpoint Report";
|
|
270
|
+
for (const title of storyTitles) {
|
|
271
|
+
groupedRuns.get(title)?.sort((a, b) => sortByProjectAndTime(a, b, projectOrder));
|
|
272
|
+
}
|
|
273
|
+
const navLinks = storyTitles.map((title) => `<a href="#story-${slugify(title)}">${escapeHtml(title)}</a>`).join("");
|
|
274
|
+
const storySections = storyTitles.map((title) => renderStorySection(title, groupedRuns.get(title) ?? [], outputDir)).join("");
|
|
275
|
+
return `<!doctype html>
|
|
276
|
+
<html lang="en">
|
|
277
|
+
<head>
|
|
278
|
+
<meta charset="utf-8" />
|
|
279
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
280
|
+
<title>${escapeHtml(reportTitle)}</title>
|
|
281
|
+
<style>
|
|
282
|
+
:root {
|
|
283
|
+
color-scheme: dark;
|
|
284
|
+
--bg: #0b1020;
|
|
285
|
+
--panel: rgba(15, 23, 42, 0.88);
|
|
286
|
+
--panel-2: rgba(17, 25, 40, 0.98);
|
|
287
|
+
--text: #e5eefb;
|
|
288
|
+
--muted: #9fb3c8;
|
|
289
|
+
--accent: #60a5fa;
|
|
290
|
+
--accent-2: #22d3ee;
|
|
291
|
+
--border: rgba(148, 163, 184, 0.18);
|
|
292
|
+
--success: #34d399;
|
|
293
|
+
--warning: #fbbf24;
|
|
294
|
+
--danger: #fb7185;
|
|
295
|
+
--shadow: 0 24px 64px rgba(2, 6, 23, 0.45);
|
|
296
|
+
}
|
|
297
|
+
* { box-sizing: border-box; }
|
|
298
|
+
html { scroll-behavior: smooth; }
|
|
299
|
+
body {
|
|
300
|
+
margin: 0;
|
|
301
|
+
min-height: 100vh;
|
|
302
|
+
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
303
|
+
background:
|
|
304
|
+
radial-gradient(circle at top, rgba(96, 165, 250, 0.14), transparent 30%),
|
|
305
|
+
linear-gradient(180deg, #07101f 0%, #0b1020 100%);
|
|
306
|
+
color: var(--text);
|
|
307
|
+
}
|
|
308
|
+
a { color: inherit; }
|
|
309
|
+
.page {
|
|
310
|
+
width: min(1600px, calc(100vw - 32px));
|
|
311
|
+
margin: 0 auto;
|
|
312
|
+
padding: 28px 0 56px;
|
|
313
|
+
}
|
|
314
|
+
.hero {
|
|
315
|
+
background: linear-gradient(180deg, rgba(15, 23, 42, 0.96), rgba(15, 23, 42, 0.84));
|
|
316
|
+
border: 1px solid var(--border);
|
|
317
|
+
border-radius: 24px;
|
|
318
|
+
padding: 24px;
|
|
319
|
+
box-shadow: var(--shadow);
|
|
320
|
+
backdrop-filter: blur(18px);
|
|
321
|
+
}
|
|
322
|
+
.hero h1 {
|
|
323
|
+
margin: 0;
|
|
324
|
+
font-size: clamp(1.9rem, 2.6vw, 3rem);
|
|
325
|
+
line-height: 1.1;
|
|
326
|
+
}
|
|
327
|
+
.hero p {
|
|
328
|
+
margin: 10px 0 0;
|
|
329
|
+
color: var(--muted);
|
|
330
|
+
max-width: 72ch;
|
|
331
|
+
line-height: 1.6;
|
|
332
|
+
}
|
|
333
|
+
.summary-bar {
|
|
334
|
+
display: flex;
|
|
335
|
+
flex-wrap: wrap;
|
|
336
|
+
gap: 12px;
|
|
337
|
+
margin-top: 18px;
|
|
338
|
+
}
|
|
339
|
+
.summary-pill {
|
|
340
|
+
display: inline-flex;
|
|
341
|
+
gap: 8px;
|
|
342
|
+
align-items: center;
|
|
343
|
+
padding: 9px 12px;
|
|
344
|
+
border: 1px solid var(--border);
|
|
345
|
+
border-radius: 999px;
|
|
346
|
+
background: rgba(15, 23, 42, 0.72);
|
|
347
|
+
color: var(--muted);
|
|
348
|
+
font-size: 0.92rem;
|
|
349
|
+
}
|
|
350
|
+
.summary-pill strong { color: var(--text); }
|
|
351
|
+
.toolbar {
|
|
352
|
+
display: flex;
|
|
353
|
+
flex-wrap: wrap;
|
|
354
|
+
justify-content: space-between;
|
|
355
|
+
gap: 16px;
|
|
356
|
+
margin-top: 18px;
|
|
357
|
+
padding-top: 18px;
|
|
358
|
+
border-top: 1px solid var(--border);
|
|
359
|
+
}
|
|
360
|
+
.story-nav {
|
|
361
|
+
display: flex;
|
|
362
|
+
flex-wrap: wrap;
|
|
363
|
+
gap: 10px;
|
|
364
|
+
}
|
|
365
|
+
.story-nav a,
|
|
366
|
+
.toolbar button,
|
|
367
|
+
.artifact {
|
|
368
|
+
border: 1px solid var(--border);
|
|
369
|
+
border-radius: 999px;
|
|
370
|
+
background: rgba(15, 23, 42, 0.7);
|
|
371
|
+
color: var(--text);
|
|
372
|
+
text-decoration: none;
|
|
373
|
+
padding: 8px 12px;
|
|
374
|
+
font: inherit;
|
|
375
|
+
font-size: 0.86rem;
|
|
376
|
+
transition: transform 140ms ease, border-color 140ms ease, background 140ms ease;
|
|
377
|
+
}
|
|
378
|
+
.toolbar button:hover,
|
|
379
|
+
.story-nav a:hover,
|
|
380
|
+
.artifact:hover {
|
|
381
|
+
transform: translateY(-1px);
|
|
382
|
+
border-color: rgba(96, 165, 250, 0.55);
|
|
383
|
+
background: rgba(30, 41, 59, 0.96);
|
|
384
|
+
cursor: pointer;
|
|
385
|
+
}
|
|
386
|
+
.content {
|
|
387
|
+
display: grid;
|
|
388
|
+
gap: 18px;
|
|
389
|
+
margin-top: 22px;
|
|
390
|
+
}
|
|
391
|
+
.accordion {
|
|
392
|
+
border: 1px solid var(--border);
|
|
393
|
+
border-radius: 22px;
|
|
394
|
+
background: var(--panel);
|
|
395
|
+
box-shadow: var(--shadow);
|
|
396
|
+
overflow: hidden;
|
|
397
|
+
}
|
|
398
|
+
.accordion summary {
|
|
399
|
+
list-style: none;
|
|
400
|
+
cursor: pointer;
|
|
401
|
+
}
|
|
402
|
+
.accordion summary::-webkit-details-marker { display: none; }
|
|
403
|
+
.story-summary,
|
|
404
|
+
.checkpoint-summary {
|
|
405
|
+
display: flex;
|
|
406
|
+
justify-content: space-between;
|
|
407
|
+
align-items: center;
|
|
408
|
+
gap: 12px;
|
|
409
|
+
}
|
|
410
|
+
.story-summary {
|
|
411
|
+
padding: 20px 22px;
|
|
412
|
+
background: linear-gradient(180deg, rgba(15, 23, 42, 0.92), rgba(15, 23, 42, 0.74));
|
|
413
|
+
}
|
|
414
|
+
.story-title {
|
|
415
|
+
font-size: 1.1rem;
|
|
416
|
+
font-weight: 700;
|
|
417
|
+
}
|
|
418
|
+
.story-meta-chip,
|
|
419
|
+
.checkpoint-meta {
|
|
420
|
+
color: var(--muted);
|
|
421
|
+
font-size: 0.84rem;
|
|
422
|
+
white-space: nowrap;
|
|
423
|
+
}
|
|
424
|
+
.story-body {
|
|
425
|
+
padding: 0 22px 22px;
|
|
426
|
+
}
|
|
427
|
+
.story-meta-row {
|
|
428
|
+
display: flex;
|
|
429
|
+
flex-wrap: wrap;
|
|
430
|
+
gap: 16px;
|
|
431
|
+
color: var(--muted);
|
|
432
|
+
font-size: 0.92rem;
|
|
433
|
+
line-height: 1.5;
|
|
434
|
+
margin: 4px 0 18px;
|
|
435
|
+
}
|
|
436
|
+
.story-meta-row strong { color: var(--text); margin-right: 6px; }
|
|
437
|
+
.checkpoint-block {
|
|
438
|
+
margin-top: 14px;
|
|
439
|
+
border-radius: 18px;
|
|
440
|
+
background: var(--panel-2);
|
|
441
|
+
border: 1px solid rgba(148, 163, 184, 0.14);
|
|
442
|
+
}
|
|
443
|
+
.checkpoint-summary {
|
|
444
|
+
padding: 16px 18px;
|
|
445
|
+
font-weight: 600;
|
|
446
|
+
background: rgba(15, 23, 42, 0.68);
|
|
447
|
+
}
|
|
448
|
+
.variant-grid {
|
|
449
|
+
display: grid;
|
|
450
|
+
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
451
|
+
gap: 14px;
|
|
452
|
+
padding: 0 18px 18px;
|
|
453
|
+
}
|
|
454
|
+
.variant-card {
|
|
455
|
+
display: grid;
|
|
456
|
+
gap: 12px;
|
|
457
|
+
border: 1px solid rgba(148, 163, 184, 0.14);
|
|
458
|
+
border-radius: 18px;
|
|
459
|
+
background: rgba(15, 23, 42, 0.72);
|
|
460
|
+
padding: 16px;
|
|
461
|
+
min-height: 100%;
|
|
462
|
+
}
|
|
463
|
+
.variant-card.missing {
|
|
464
|
+
opacity: 0.72;
|
|
465
|
+
border-style: dashed;
|
|
466
|
+
}
|
|
467
|
+
.variant-card-header {
|
|
468
|
+
display: flex;
|
|
469
|
+
justify-content: space-between;
|
|
470
|
+
align-items: flex-start;
|
|
471
|
+
gap: 10px;
|
|
472
|
+
}
|
|
473
|
+
.variant-card-header h5 {
|
|
474
|
+
margin: 0;
|
|
475
|
+
font-size: 1rem;
|
|
476
|
+
}
|
|
477
|
+
.variant-card-header p,
|
|
478
|
+
.variant-card-header time {
|
|
479
|
+
margin: 4px 0 0;
|
|
480
|
+
color: var(--muted);
|
|
481
|
+
font-size: 0.83rem;
|
|
482
|
+
}
|
|
483
|
+
.page-meta {
|
|
484
|
+
display: grid;
|
|
485
|
+
gap: 4px;
|
|
486
|
+
margin: 0;
|
|
487
|
+
color: var(--muted);
|
|
488
|
+
font-size: 0.9rem;
|
|
489
|
+
}
|
|
490
|
+
.page-url {
|
|
491
|
+
overflow-wrap: anywhere;
|
|
492
|
+
font-size: 0.82rem;
|
|
493
|
+
}
|
|
494
|
+
.thumbnail-link {
|
|
495
|
+
position: relative;
|
|
496
|
+
display: block;
|
|
497
|
+
border-radius: 14px;
|
|
498
|
+
overflow: hidden;
|
|
499
|
+
border: 1px solid rgba(148, 163, 184, 0.18);
|
|
500
|
+
background: rgba(2, 6, 23, 0.65);
|
|
501
|
+
}
|
|
502
|
+
.thumbnail-link img {
|
|
503
|
+
display: block;
|
|
504
|
+
width: 100%;
|
|
505
|
+
aspect-ratio: 16 / 10;
|
|
506
|
+
object-fit: cover;
|
|
507
|
+
}
|
|
508
|
+
.highlight-overlay {
|
|
509
|
+
position: absolute;
|
|
510
|
+
border: 2px solid var(--danger);
|
|
511
|
+
border-radius: 12px;
|
|
512
|
+
background: rgba(251, 113, 133, 0.08);
|
|
513
|
+
box-shadow: 0 0 0 999px rgba(251, 113, 133, 0.02);
|
|
514
|
+
pointer-events: none;
|
|
515
|
+
}
|
|
516
|
+
.highlight-label {
|
|
517
|
+
position: absolute;
|
|
518
|
+
left: 12px;
|
|
519
|
+
bottom: 12px;
|
|
520
|
+
max-width: calc(100% - 24px);
|
|
521
|
+
padding: 6px 9px;
|
|
522
|
+
border-radius: 999px;
|
|
523
|
+
background: rgba(15, 23, 42, 0.9);
|
|
524
|
+
border: 1px solid rgba(251, 113, 133, 0.35);
|
|
525
|
+
color: #ffe4e6;
|
|
526
|
+
font-size: 0.74rem;
|
|
527
|
+
line-height: 1.3;
|
|
528
|
+
overflow-wrap: anywhere;
|
|
529
|
+
}
|
|
530
|
+
.stats-grid {
|
|
531
|
+
display: grid;
|
|
532
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
533
|
+
gap: 10px;
|
|
534
|
+
}
|
|
535
|
+
.stats-grid span {
|
|
536
|
+
display: grid;
|
|
537
|
+
gap: 6px;
|
|
538
|
+
padding: 10px 12px;
|
|
539
|
+
border-radius: 14px;
|
|
540
|
+
background: rgba(2, 6, 23, 0.42);
|
|
541
|
+
border: 1px solid rgba(148, 163, 184, 0.12);
|
|
542
|
+
}
|
|
543
|
+
.stats-grid strong {
|
|
544
|
+
font-size: 1.15rem;
|
|
545
|
+
line-height: 1;
|
|
546
|
+
}
|
|
547
|
+
.stats-grid small {
|
|
548
|
+
color: var(--muted);
|
|
549
|
+
font-size: 0.76rem;
|
|
550
|
+
text-transform: uppercase;
|
|
551
|
+
letter-spacing: 0.04em;
|
|
552
|
+
}
|
|
553
|
+
.artifact-list {
|
|
554
|
+
display: flex;
|
|
555
|
+
flex-wrap: wrap;
|
|
556
|
+
gap: 8px;
|
|
557
|
+
}
|
|
558
|
+
.artifact.disabled {
|
|
559
|
+
opacity: 0.45;
|
|
560
|
+
pointer-events: none;
|
|
561
|
+
}
|
|
562
|
+
.empty-state,
|
|
563
|
+
.empty-card {
|
|
564
|
+
margin: 0;
|
|
565
|
+
color: var(--muted);
|
|
566
|
+
padding: 12px;
|
|
567
|
+
border: 1px dashed rgba(148, 163, 184, 0.2);
|
|
568
|
+
border-radius: 14px;
|
|
569
|
+
background: rgba(2, 6, 23, 0.24);
|
|
570
|
+
}
|
|
571
|
+
@media (max-width: 720px) {
|
|
572
|
+
.page {
|
|
573
|
+
width: min(100vw - 20px, 1600px);
|
|
574
|
+
padding-top: 18px;
|
|
575
|
+
}
|
|
576
|
+
.hero,
|
|
577
|
+
.story-summary,
|
|
578
|
+
.story-body,
|
|
579
|
+
.checkpoint-summary,
|
|
580
|
+
.variant-grid {
|
|
581
|
+
padding-left: 16px;
|
|
582
|
+
padding-right: 16px;
|
|
583
|
+
}
|
|
584
|
+
.variant-card-header,
|
|
585
|
+
.story-summary,
|
|
586
|
+
.checkpoint-summary,
|
|
587
|
+
.toolbar {
|
|
588
|
+
flex-direction: column;
|
|
589
|
+
align-items: flex-start;
|
|
590
|
+
}
|
|
591
|
+
.stats-grid {
|
|
592
|
+
grid-template-columns: 1fr;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
</style>
|
|
596
|
+
</head>
|
|
597
|
+
<body>
|
|
598
|
+
<main class="page">
|
|
599
|
+
<section class="hero">
|
|
600
|
+
<h1>${escapeHtml(reportTitle)}</h1>
|
|
601
|
+
<p>Explore checkpoint runs by story, inspect every project variant side-by-side, and jump directly to screenshots and generated artifacts.</p>
|
|
602
|
+
<div class="summary-bar">
|
|
603
|
+
<span class="summary-pill"><strong>Generated</strong> ${escapeHtml(formatDateTime(generatedAt))}</span>
|
|
604
|
+
<span class="summary-pill"><strong>Stories</strong> ${storyTitles.length}</span>
|
|
605
|
+
<span class="summary-pill"><strong>Runs</strong> ${runs.length}</span>
|
|
606
|
+
<span class="summary-pill"><strong>Output</strong> ${escapeHtml(outputDir)}</span>
|
|
607
|
+
</div>
|
|
608
|
+
<div class="toolbar">
|
|
609
|
+
<nav class="story-nav">${navLinks || '<span class="summary-pill">No stories found</span>'}</nav>
|
|
610
|
+
<div class="toolbar-actions">
|
|
611
|
+
<button type="button" data-action="expand-all">Expand all</button>
|
|
612
|
+
<button type="button" data-action="collapse-all">Collapse all</button>
|
|
613
|
+
</div>
|
|
614
|
+
</div>
|
|
615
|
+
</section>
|
|
616
|
+
<section class="content">
|
|
617
|
+
${storySections || '<p class="empty-state">No checkpoint manifests found.</p>'}
|
|
618
|
+
</section>
|
|
619
|
+
</main>
|
|
620
|
+
<script>
|
|
621
|
+
(() => {
|
|
622
|
+
const details = Array.from(document.querySelectorAll('details.accordion'));
|
|
623
|
+
const setAll = (open) => {
|
|
624
|
+
details.forEach((entry) => {
|
|
625
|
+
entry.open = open;
|
|
626
|
+
});
|
|
627
|
+
};
|
|
628
|
+
document.querySelector('[data-action="expand-all"]')?.addEventListener('click', () => setAll(true));
|
|
629
|
+
document.querySelector('[data-action="collapse-all"]')?.addEventListener('click', () => setAll(false));
|
|
630
|
+
const revealHash = () => {
|
|
631
|
+
if (!window.location.hash) {
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
const target = document.querySelector(window.location.hash);
|
|
635
|
+
if (!(target instanceof HTMLElement)) {
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
const parentDetails = target.closest('details');
|
|
639
|
+
if (parentDetails instanceof HTMLDetailsElement) {
|
|
640
|
+
parentDetails.open = true;
|
|
641
|
+
}
|
|
642
|
+
target.scrollIntoView({ block: 'start', behavior: 'smooth' });
|
|
643
|
+
};
|
|
644
|
+
window.addEventListener('hashchange', revealHash);
|
|
645
|
+
revealHash();
|
|
646
|
+
})();
|
|
647
|
+
</script>
|
|
648
|
+
</body>
|
|
649
|
+
</html>
|
|
650
|
+
`;
|
|
651
|
+
}
|
|
652
|
+
var htmlReporter = {
|
|
653
|
+
name: "html",
|
|
654
|
+
description: "Responsive HTML report for checkpoint manifests.",
|
|
655
|
+
validateConfig(config) {
|
|
656
|
+
return config != null && typeof config === "object" && !Array.isArray(config);
|
|
657
|
+
},
|
|
658
|
+
async generate(context) {
|
|
659
|
+
const outputFile = import_node_path.default.join(context.outputDir, "index.html");
|
|
660
|
+
const html = buildHtmlReport(context.runs, context.outputDir, context.config);
|
|
661
|
+
await import_promises.default.mkdir(context.outputDir, { recursive: true });
|
|
662
|
+
await import_promises.default.writeFile(outputFile, html, "utf8");
|
|
663
|
+
const storyCount = new Set(context.runs.map((run) => run.title)).size;
|
|
664
|
+
return {
|
|
665
|
+
files: [outputFile],
|
|
666
|
+
summary: `Generated HTML report for ${storyCount} stor${storyCount === 1 ? "y" : "ies"} (${context.runs.length} run${context.runs.length === 1 ? "" : "s"}).`
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
};
|
|
670
|
+
|
|
671
|
+
// src/report/markdown-reporter.ts
|
|
672
|
+
var import_promises2 = __toESM(require("fs/promises"), 1);
|
|
673
|
+
var import_node_path2 = __toESM(require("path"), 1);
|
|
674
|
+
function slugify2(value) {
|
|
675
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "story";
|
|
676
|
+
}
|
|
677
|
+
function stripTags(value) {
|
|
678
|
+
const stripped = value.replace(/\s+@[a-z0-9-]+/gi, " ").replace(/\s+/g, " ").trim();
|
|
679
|
+
return stripped || value.trim() || "Untitled story";
|
|
680
|
+
}
|
|
681
|
+
function normalizeConfig(config) {
|
|
682
|
+
return {
|
|
683
|
+
storiesDir: typeof config.storiesDir === "string" ? config.storiesDir : ".",
|
|
684
|
+
screenshotsDir: typeof config.screenshotsDir === "string" ? config.screenshotsDir : "screenshots",
|
|
685
|
+
includeTags: Array.isArray(config.includeTags) ? config.includeTags.filter((value) => typeof value === "string") : void 0,
|
|
686
|
+
preferredProject: typeof config.preferredProject === "string" ? config.preferredProject : void 0,
|
|
687
|
+
header: typeof config.header === "string" ? config.header : void 0,
|
|
688
|
+
footer: typeof config.footer === "string" ? config.footer : void 0,
|
|
689
|
+
frontmatter: config.frontmatter === true || config.frontmatter === false || config.frontmatter != null && typeof config.frontmatter === "object" && !Array.isArray(config.frontmatter) ? config.frontmatter : false,
|
|
690
|
+
imagePathPrefix: typeof config.imagePathPrefix === "string" ? config.imagePathPrefix : void 0,
|
|
691
|
+
copyScreenshots: typeof config.copyScreenshots === "boolean" ? config.copyScreenshots : true
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
function normalizeTags(tags) {
|
|
695
|
+
return (tags ?? []).map((tag) => tag.trim().toLowerCase()).filter(Boolean);
|
|
696
|
+
}
|
|
697
|
+
function shouldIncludeRun(run, config) {
|
|
698
|
+
const includeTags = normalizeTags(config.includeTags);
|
|
699
|
+
if (includeTags.length > 0) {
|
|
700
|
+
const runTags = new Set(normalizeTags(run.tags));
|
|
701
|
+
return includeTags.some((tag) => runTags.has(tag));
|
|
702
|
+
}
|
|
703
|
+
return run.checkpoints.some((checkpoint) => {
|
|
704
|
+
const hasDescription = typeof checkpoint.description === "string" && checkpoint.description.trim().length > 0;
|
|
705
|
+
return hasDescription || typeof checkpoint.step === "number";
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
function choosePrimaryRun(runs, preferredProject) {
|
|
709
|
+
if (runs.length === 0) {
|
|
710
|
+
return null;
|
|
711
|
+
}
|
|
712
|
+
return [...runs].sort((left, right) => {
|
|
713
|
+
const leftPreferred = preferredProject && left.project === preferredProject ? 0 : 1;
|
|
714
|
+
const rightPreferred = preferredProject && right.project === preferredProject ? 0 : 1;
|
|
715
|
+
if (leftPreferred !== rightPreferred) {
|
|
716
|
+
return leftPreferred - rightPreferred;
|
|
717
|
+
}
|
|
718
|
+
const rightTime = new Date(right.startedAt).getTime();
|
|
719
|
+
const leftTime = new Date(left.startedAt).getTime();
|
|
720
|
+
if (rightTime !== leftTime) {
|
|
721
|
+
return rightTime - leftTime;
|
|
722
|
+
}
|
|
723
|
+
return left.project.localeCompare(right.project);
|
|
724
|
+
})[0] ?? null;
|
|
725
|
+
}
|
|
726
|
+
function resolveArtifactPath2(run, artifactPath) {
|
|
727
|
+
return import_node_path2.default.isAbsolute(artifactPath) ? artifactPath : import_node_path2.default.resolve(import_node_path2.default.dirname(run.sourceManifestPath), artifactPath);
|
|
728
|
+
}
|
|
729
|
+
function screenshotSourcePath(run, checkpoint) {
|
|
730
|
+
const artifacts = checkpoint.collectors.screenshot?.artifacts ?? [];
|
|
731
|
+
const artifact = artifacts.find((entry) => entry.name === "screenshot") ?? artifacts[0];
|
|
732
|
+
return artifact?.path ? resolveArtifactPath2(run, artifact.path) : null;
|
|
733
|
+
}
|
|
734
|
+
function screenshotData2(checkpoint) {
|
|
735
|
+
const data = checkpoint.collectors.screenshot?.data;
|
|
736
|
+
return data && typeof data === "object" ? data : null;
|
|
737
|
+
}
|
|
738
|
+
function focusNote(checkpoint) {
|
|
739
|
+
const data = screenshotData2(checkpoint);
|
|
740
|
+
const selector = typeof data?.highlightSelector === "string" ? data.highlightSelector.trim() : "";
|
|
741
|
+
if (selector) {
|
|
742
|
+
return `Focus: \`${selector}\``;
|
|
743
|
+
}
|
|
744
|
+
const bounds = data?.highlightBounds;
|
|
745
|
+
if (bounds && typeof bounds.x === "number" && typeof bounds.y === "number" && typeof bounds.width === "number" && typeof bounds.height === "number") {
|
|
746
|
+
return "Focus: highlighted UI element.";
|
|
747
|
+
}
|
|
748
|
+
return null;
|
|
749
|
+
}
|
|
750
|
+
function urlLabel(url) {
|
|
751
|
+
try {
|
|
752
|
+
const parsed = new URL(url);
|
|
753
|
+
const value = `${parsed.pathname}${parsed.search}${parsed.hash}`;
|
|
754
|
+
return value || "/";
|
|
755
|
+
} catch {
|
|
756
|
+
return url || "/";
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
function breadcrumbLabel(url) {
|
|
760
|
+
const label = urlLabel(url);
|
|
761
|
+
if (!label.startsWith("/")) {
|
|
762
|
+
return null;
|
|
763
|
+
}
|
|
764
|
+
const [withoutQuery = label] = label.split("?");
|
|
765
|
+
const [withoutHash = withoutQuery] = withoutQuery.split("#");
|
|
766
|
+
const segments = withoutHash.split("/").map((segment) => segment.trim()).filter(Boolean).map((segment) => decodeURIComponent(segment).replace(/[-_]+/g, " "));
|
|
767
|
+
return segments.length > 0 ? segments.join(" \u203A ") : "home";
|
|
768
|
+
}
|
|
769
|
+
function autoDescription(checkpoint) {
|
|
770
|
+
const pageTitle = checkpoint.title.trim();
|
|
771
|
+
const location = urlLabel(checkpoint.url);
|
|
772
|
+
if (pageTitle) {
|
|
773
|
+
return `This step captures **${pageTitle}** at \`${location}\`.`;
|
|
774
|
+
}
|
|
775
|
+
return `This step captures **${checkpoint.name}** at \`${location}\`.`;
|
|
776
|
+
}
|
|
777
|
+
function markdownRelativePath(fromFile, toFile) {
|
|
778
|
+
const relativePath = import_node_path2.default.relative(import_node_path2.default.dirname(fromFile), toFile).split(import_node_path2.default.sep).join("/");
|
|
779
|
+
if (relativePath.startsWith(".")) {
|
|
780
|
+
return relativePath;
|
|
781
|
+
}
|
|
782
|
+
return `./${relativePath}`;
|
|
783
|
+
}
|
|
784
|
+
function rewriteImagePath(markdownFile, imageFile, outputDir, prefix) {
|
|
785
|
+
const relativePath = import_node_path2.default.relative(outputDir, imageFile).split(import_node_path2.default.sep).join("/");
|
|
786
|
+
if (prefix) {
|
|
787
|
+
return `${prefix.replace(/\/+$/g, "")}/${relativePath.replace(/^\/+/, "")}`;
|
|
788
|
+
}
|
|
789
|
+
return markdownRelativePath(markdownFile, imageFile);
|
|
790
|
+
}
|
|
791
|
+
function yamlScalar(value) {
|
|
792
|
+
return JSON.stringify(value);
|
|
793
|
+
}
|
|
794
|
+
function serializeFrontmatter(fields) {
|
|
795
|
+
const lines = ["---"];
|
|
796
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
797
|
+
if (value === void 0) {
|
|
798
|
+
continue;
|
|
799
|
+
}
|
|
800
|
+
if (Array.isArray(value)) {
|
|
801
|
+
lines.push(`${key}:`);
|
|
802
|
+
if (value.length === 0) {
|
|
803
|
+
lines.push(" []");
|
|
804
|
+
continue;
|
|
805
|
+
}
|
|
806
|
+
for (const item of value) {
|
|
807
|
+
lines.push(` - ${yamlScalar(item)}`);
|
|
808
|
+
}
|
|
809
|
+
continue;
|
|
810
|
+
}
|
|
811
|
+
lines.push(`${key}: ${yamlScalar(value)}`);
|
|
812
|
+
}
|
|
813
|
+
lines.push("---", "");
|
|
814
|
+
return lines.join("\n");
|
|
815
|
+
}
|
|
816
|
+
async function materializeScreenshot(args) {
|
|
817
|
+
const sourcePath = screenshotSourcePath(args.run, args.checkpoint);
|
|
818
|
+
if (!sourcePath) {
|
|
819
|
+
return null;
|
|
820
|
+
}
|
|
821
|
+
const extension = import_node_path2.default.extname(sourcePath) || ".png";
|
|
822
|
+
const targetPath = import_node_path2.default.join(
|
|
823
|
+
args.outputDir,
|
|
824
|
+
args.config.screenshotsDir ?? "screenshots",
|
|
825
|
+
args.storySlug,
|
|
826
|
+
`${String(args.stepOrder).padStart(2, "0")}-${slugify2(args.checkpoint.name)}${extension}`
|
|
827
|
+
);
|
|
828
|
+
try {
|
|
829
|
+
if (args.config.copyScreenshots !== false) {
|
|
830
|
+
await import_promises2.default.mkdir(import_node_path2.default.dirname(targetPath), { recursive: true });
|
|
831
|
+
await import_promises2.default.copyFile(sourcePath, targetPath);
|
|
832
|
+
args.writtenFiles.add(targetPath);
|
|
833
|
+
return rewriteImagePath(args.markdownFile, targetPath, args.outputDir, args.config.imagePathPrefix);
|
|
834
|
+
}
|
|
835
|
+
return rewriteImagePath(args.markdownFile, sourcePath, args.outputDir, args.config.imagePathPrefix);
|
|
836
|
+
} catch {
|
|
837
|
+
return null;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
function orderedCheckpoints(checkpoints) {
|
|
841
|
+
return [...checkpoints].sort((left, right) => {
|
|
842
|
+
const leftOrder = typeof left.step === "number" ? left.step : Number.MAX_SAFE_INTEGER;
|
|
843
|
+
const rightOrder = typeof right.step === "number" ? right.step : Number.MAX_SAFE_INTEGER;
|
|
844
|
+
if (leftOrder !== rightOrder) {
|
|
845
|
+
return leftOrder - rightOrder;
|
|
846
|
+
}
|
|
847
|
+
return checkpoints.indexOf(left) - checkpoints.indexOf(right);
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
async function buildSteps(args) {
|
|
851
|
+
const checkpoints = orderedCheckpoints(args.run.checkpoints);
|
|
852
|
+
const steps = [];
|
|
853
|
+
for (const [index, checkpoint] of checkpoints.entries()) {
|
|
854
|
+
const order = typeof checkpoint.step === "number" ? checkpoint.step : index + 1;
|
|
855
|
+
steps.push({
|
|
856
|
+
checkpoint,
|
|
857
|
+
order,
|
|
858
|
+
heading: checkpoint.name,
|
|
859
|
+
description: typeof checkpoint.description === "string" && checkpoint.description.trim().length > 0 ? checkpoint.description.trim() : autoDescription(checkpoint),
|
|
860
|
+
imagePath: await materializeScreenshot({
|
|
861
|
+
run: args.run,
|
|
862
|
+
checkpoint,
|
|
863
|
+
storySlug: args.storySlug,
|
|
864
|
+
stepOrder: order,
|
|
865
|
+
outputDir: args.outputDir,
|
|
866
|
+
markdownFile: args.markdownFile,
|
|
867
|
+
config: args.config,
|
|
868
|
+
writtenFiles: args.writtenFiles
|
|
869
|
+
}),
|
|
870
|
+
urlLabel: urlLabel(checkpoint.url),
|
|
871
|
+
breadcrumbLabel: breadcrumbLabel(checkpoint.url),
|
|
872
|
+
focusNote: focusNote(checkpoint)
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
return steps;
|
|
876
|
+
}
|
|
877
|
+
function renderMarkdown(args) {
|
|
878
|
+
const frontmatterFields = args.config.frontmatter === true || typeof args.config.frontmatter === "object" ? {
|
|
879
|
+
title: args.title,
|
|
880
|
+
project: args.run.project,
|
|
881
|
+
testId: args.run.testId,
|
|
882
|
+
tags: args.run.tags,
|
|
883
|
+
startedAt: args.run.startedAt,
|
|
884
|
+
generatedAt: args.generatedAt,
|
|
885
|
+
...args.config.frontmatter && typeof args.config.frontmatter === "object" ? args.config.frontmatter : {}
|
|
886
|
+
} : null;
|
|
887
|
+
const sections = args.steps.map((step) => {
|
|
888
|
+
const lines = [`## Step ${step.order}: ${step.heading}`, ""];
|
|
889
|
+
if (step.imagePath) {
|
|
890
|
+
lines.push(``, "");
|
|
891
|
+
}
|
|
892
|
+
lines.push(`**URL:** \`${step.urlLabel}\``);
|
|
893
|
+
if (step.breadcrumbLabel) {
|
|
894
|
+
lines.push("", `**Breadcrumb:** ${step.breadcrumbLabel}`);
|
|
895
|
+
}
|
|
896
|
+
if (step.focusNote) {
|
|
897
|
+
lines.push("", `> ${step.focusNote}`);
|
|
898
|
+
}
|
|
899
|
+
lines.push("", step.description);
|
|
900
|
+
return lines.join("\n");
|
|
901
|
+
}).join("\n\n");
|
|
902
|
+
const parts = [
|
|
903
|
+
frontmatterFields ? serializeFrontmatter(frontmatterFields) : "",
|
|
904
|
+
`# ${args.title}`,
|
|
905
|
+
args.config.header ? args.config.header.trim() : "",
|
|
906
|
+
sections,
|
|
907
|
+
args.config.footer ? args.config.footer.trim() : ""
|
|
908
|
+
].filter((value) => value.trim().length > 0);
|
|
909
|
+
return `${parts.join("\n\n")}
|
|
910
|
+
`;
|
|
911
|
+
}
|
|
912
|
+
var markdownReporter = {
|
|
913
|
+
name: "markdown",
|
|
914
|
+
description: "Generates one Markdown help article per captured story.",
|
|
915
|
+
validateConfig(config) {
|
|
916
|
+
return config != null && typeof config === "object" && !Array.isArray(config);
|
|
917
|
+
},
|
|
918
|
+
async generate(context) {
|
|
919
|
+
const config = normalizeConfig(context.config);
|
|
920
|
+
const stories = groupByStory(context.runs);
|
|
921
|
+
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
922
|
+
const writtenFiles = /* @__PURE__ */ new Set();
|
|
923
|
+
let articleCount = 0;
|
|
924
|
+
for (const [storyTitle, runs] of stories) {
|
|
925
|
+
const primaryRun = choosePrimaryRun(runs, config.preferredProject);
|
|
926
|
+
if (!primaryRun || !shouldIncludeRun(primaryRun, config)) {
|
|
927
|
+
continue;
|
|
928
|
+
}
|
|
929
|
+
const title = stripTags(storyTitle);
|
|
930
|
+
const storySlug = slugify2(title);
|
|
931
|
+
const markdownFile = import_node_path2.default.join(context.outputDir, config.storiesDir ?? ".", `${storySlug}.md`);
|
|
932
|
+
const steps = await buildSteps({
|
|
933
|
+
run: primaryRun,
|
|
934
|
+
storySlug,
|
|
935
|
+
outputDir: context.outputDir,
|
|
936
|
+
markdownFile,
|
|
937
|
+
config,
|
|
938
|
+
writtenFiles
|
|
939
|
+
});
|
|
940
|
+
await import_promises2.default.mkdir(import_node_path2.default.dirname(markdownFile), { recursive: true });
|
|
941
|
+
await import_promises2.default.writeFile(
|
|
942
|
+
markdownFile,
|
|
943
|
+
renderMarkdown({
|
|
944
|
+
title,
|
|
945
|
+
steps,
|
|
946
|
+
run: primaryRun,
|
|
947
|
+
config,
|
|
948
|
+
generatedAt
|
|
949
|
+
}),
|
|
950
|
+
"utf8"
|
|
951
|
+
);
|
|
952
|
+
writtenFiles.add(markdownFile);
|
|
953
|
+
articleCount += 1;
|
|
954
|
+
}
|
|
955
|
+
return {
|
|
956
|
+
files: [...writtenFiles],
|
|
957
|
+
summary: `Generated ${articleCount} Markdown article${articleCount === 1 ? "" : "s"}.`
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
};
|
|
961
|
+
|
|
962
|
+
// src/report/mdx-reporter.ts
|
|
963
|
+
var import_promises3 = __toESM(require("fs/promises"), 1);
|
|
964
|
+
var import_node_path3 = __toESM(require("path"), 1);
|
|
965
|
+
var DEFAULT_PROJECT_ORDER2 = ["desktop-light", "desktop-dark", "mobile-light", "mobile-dark"];
|
|
966
|
+
function slugify3(value) {
|
|
967
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "story";
|
|
968
|
+
}
|
|
969
|
+
function stripTags2(value) {
|
|
970
|
+
const stripped = value.replace(/\s+@[a-z0-9-]+/gi, " ").replace(/\s+/g, " ").trim();
|
|
971
|
+
return stripped || value.trim() || "Untitled story";
|
|
972
|
+
}
|
|
973
|
+
function normalizeConfig2(config) {
|
|
974
|
+
return {
|
|
975
|
+
storiesDir: typeof config.storiesDir === "string" ? config.storiesDir : ".",
|
|
976
|
+
screenshotsDir: typeof config.screenshotsDir === "string" ? config.screenshotsDir : "screenshots",
|
|
977
|
+
includeTags: Array.isArray(config.includeTags) ? config.includeTags.filter((value) => typeof value === "string") : void 0,
|
|
978
|
+
preferredProject: typeof config.preferredProject === "string" ? config.preferredProject : void 0,
|
|
979
|
+
imagePathPrefix: typeof config.imagePathPrefix === "string" ? config.imagePathPrefix : void 0,
|
|
980
|
+
copyScreenshots: typeof config.copyScreenshots === "boolean" ? config.copyScreenshots : true,
|
|
981
|
+
componentImportPath: typeof config.componentImportPath === "string" ? config.componentImportPath : "playwright-checkpoint/components"
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
function normalizeTags2(tags) {
|
|
985
|
+
return (tags ?? []).map((tag) => tag.trim().toLowerCase()).filter(Boolean);
|
|
986
|
+
}
|
|
987
|
+
function frontmatterTags(tags) {
|
|
988
|
+
return normalizeTags2(tags).map((tag) => tag.replace(/^@+/, "")).filter(Boolean);
|
|
989
|
+
}
|
|
990
|
+
function shouldIncludeRun2(run, config) {
|
|
991
|
+
const includeTags = normalizeTags2(config.includeTags);
|
|
992
|
+
if (includeTags.length > 0) {
|
|
993
|
+
const runTags = new Set(normalizeTags2(run.tags));
|
|
994
|
+
return includeTags.some((tag) => runTags.has(tag));
|
|
995
|
+
}
|
|
996
|
+
return run.checkpoints.some((checkpoint) => {
|
|
997
|
+
const hasDescription = typeof checkpoint.description === "string" && checkpoint.description.trim().length > 0;
|
|
998
|
+
return hasDescription || typeof checkpoint.step === "number";
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
function choosePrimaryRun2(runs, preferredProject) {
|
|
1002
|
+
if (runs.length === 0) {
|
|
1003
|
+
return null;
|
|
1004
|
+
}
|
|
1005
|
+
return [...runs].sort((left, right) => {
|
|
1006
|
+
const leftPreferred = preferredProject && left.project === preferredProject ? 0 : 1;
|
|
1007
|
+
const rightPreferred = preferredProject && right.project === preferredProject ? 0 : 1;
|
|
1008
|
+
if (leftPreferred !== rightPreferred) {
|
|
1009
|
+
return leftPreferred - rightPreferred;
|
|
1010
|
+
}
|
|
1011
|
+
const rightTime = new Date(right.startedAt).getTime();
|
|
1012
|
+
const leftTime = new Date(left.startedAt).getTime();
|
|
1013
|
+
if (rightTime !== leftTime) {
|
|
1014
|
+
return rightTime - leftTime;
|
|
1015
|
+
}
|
|
1016
|
+
return left.project.localeCompare(right.project);
|
|
1017
|
+
})[0] ?? null;
|
|
1018
|
+
}
|
|
1019
|
+
function orderedCheckpoints2(checkpoints) {
|
|
1020
|
+
return [...checkpoints].sort((left, right) => {
|
|
1021
|
+
const leftOrder = typeof left.step === "number" ? left.step : Number.MAX_SAFE_INTEGER;
|
|
1022
|
+
const rightOrder = typeof right.step === "number" ? right.step : Number.MAX_SAFE_INTEGER;
|
|
1023
|
+
if (leftOrder !== rightOrder) {
|
|
1024
|
+
return leftOrder - rightOrder;
|
|
1025
|
+
}
|
|
1026
|
+
return checkpoints.indexOf(left) - checkpoints.indexOf(right);
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
function resolveArtifactPath3(run, artifactPath) {
|
|
1030
|
+
return import_node_path3.default.isAbsolute(artifactPath) ? artifactPath : import_node_path3.default.resolve(import_node_path3.default.dirname(run.sourceManifestPath), artifactPath);
|
|
1031
|
+
}
|
|
1032
|
+
function screenshotSourcePath2(run, checkpoint) {
|
|
1033
|
+
const artifacts = checkpoint.collectors.screenshot?.artifacts ?? [];
|
|
1034
|
+
const artifact = artifacts.find((entry) => entry.name === "screenshot") ?? artifacts[0];
|
|
1035
|
+
return artifact?.path ? resolveArtifactPath3(run, artifact.path) : null;
|
|
1036
|
+
}
|
|
1037
|
+
function screenshotData3(checkpoint) {
|
|
1038
|
+
const data = checkpoint.collectors.screenshot?.data;
|
|
1039
|
+
return data && typeof data === "object" ? data : null;
|
|
1040
|
+
}
|
|
1041
|
+
function focusNote2(checkpoint) {
|
|
1042
|
+
const data = screenshotData3(checkpoint);
|
|
1043
|
+
const selector = typeof data?.highlightSelector === "string" ? data.highlightSelector.trim() : "";
|
|
1044
|
+
if (selector) {
|
|
1045
|
+
return `Focus: \`${selector}\``;
|
|
1046
|
+
}
|
|
1047
|
+
const bounds = data?.highlightBounds;
|
|
1048
|
+
if (bounds && typeof bounds.x === "number" && typeof bounds.y === "number" && typeof bounds.width === "number" && typeof bounds.height === "number") {
|
|
1049
|
+
return "Focus: highlighted UI element.";
|
|
1050
|
+
}
|
|
1051
|
+
return null;
|
|
1052
|
+
}
|
|
1053
|
+
function urlLabel2(url) {
|
|
1054
|
+
try {
|
|
1055
|
+
const parsed = new URL(url);
|
|
1056
|
+
const value = `${parsed.pathname}${parsed.search}${parsed.hash}`;
|
|
1057
|
+
return value || "/";
|
|
1058
|
+
} catch {
|
|
1059
|
+
return url || "/";
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
function autoDescription2(checkpoint) {
|
|
1063
|
+
const pageTitle = checkpoint.title.trim();
|
|
1064
|
+
const location = urlLabel2(checkpoint.url);
|
|
1065
|
+
if (pageTitle) {
|
|
1066
|
+
return `This step captures **${pageTitle}** at \`${location}\`.`;
|
|
1067
|
+
}
|
|
1068
|
+
return `This step captures **${checkpoint.name}** at \`${location}\`.`;
|
|
1069
|
+
}
|
|
1070
|
+
function markdownRelativePath2(fromFile, toFile) {
|
|
1071
|
+
const relativePath = import_node_path3.default.relative(import_node_path3.default.dirname(fromFile), toFile).split(import_node_path3.default.sep).join("/");
|
|
1072
|
+
if (relativePath.startsWith(".")) {
|
|
1073
|
+
return relativePath;
|
|
1074
|
+
}
|
|
1075
|
+
return `./${relativePath}`;
|
|
1076
|
+
}
|
|
1077
|
+
function rewriteImagePath2(mdxFile, imageFile, outputDir, prefix) {
|
|
1078
|
+
const relativePath = import_node_path3.default.relative(outputDir, imageFile).split(import_node_path3.default.sep).join("/");
|
|
1079
|
+
if (prefix) {
|
|
1080
|
+
return `${prefix.replace(/\/+$/g, "")}/${relativePath.replace(/^\/+/, "")}`;
|
|
1081
|
+
}
|
|
1082
|
+
return markdownRelativePath2(mdxFile, imageFile);
|
|
1083
|
+
}
|
|
1084
|
+
function yamlScalar2(value) {
|
|
1085
|
+
return JSON.stringify(value);
|
|
1086
|
+
}
|
|
1087
|
+
function serializeFrontmatter2(fields) {
|
|
1088
|
+
const lines = ["---"];
|
|
1089
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
1090
|
+
if (value === void 0) {
|
|
1091
|
+
continue;
|
|
1092
|
+
}
|
|
1093
|
+
if (Array.isArray(value)) {
|
|
1094
|
+
lines.push(`${key}:`);
|
|
1095
|
+
if (value.length === 0) {
|
|
1096
|
+
lines.push(" []");
|
|
1097
|
+
continue;
|
|
1098
|
+
}
|
|
1099
|
+
for (const item of value) {
|
|
1100
|
+
lines.push(` - ${yamlScalar2(item)}`);
|
|
1101
|
+
}
|
|
1102
|
+
continue;
|
|
1103
|
+
}
|
|
1104
|
+
lines.push(`${key}: ${yamlScalar2(value)}`);
|
|
1105
|
+
}
|
|
1106
|
+
lines.push("---", "");
|
|
1107
|
+
return lines.join("\n");
|
|
1108
|
+
}
|
|
1109
|
+
function quoteJsx(value) {
|
|
1110
|
+
return JSON.stringify(value);
|
|
1111
|
+
}
|
|
1112
|
+
function projectWeight2(projectName) {
|
|
1113
|
+
const index = DEFAULT_PROJECT_ORDER2.indexOf(projectName);
|
|
1114
|
+
return index === -1 ? Number.MAX_SAFE_INTEGER : index;
|
|
1115
|
+
}
|
|
1116
|
+
function formatProjectLabel2(projectName) {
|
|
1117
|
+
const [device, mode] = projectName.split("-");
|
|
1118
|
+
if (!device || !mode) {
|
|
1119
|
+
return projectName;
|
|
1120
|
+
}
|
|
1121
|
+
const deviceLabel = device === "desktop" ? "Desktop" : device === "mobile" ? "Mobile" : device;
|
|
1122
|
+
const modeLabel = mode === "light" ? "Light" : mode === "dark" ? "Dark" : mode;
|
|
1123
|
+
return `${deviceLabel} / ${modeLabel}`;
|
|
1124
|
+
}
|
|
1125
|
+
function sortRunsForVariants(runs) {
|
|
1126
|
+
return [...runs].sort((left, right) => {
|
|
1127
|
+
const byWeight = projectWeight2(left.project) - projectWeight2(right.project);
|
|
1128
|
+
if (byWeight !== 0) {
|
|
1129
|
+
return byWeight;
|
|
1130
|
+
}
|
|
1131
|
+
return left.project.localeCompare(right.project);
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1134
|
+
function findMatchingCheckpoint(run, baseCheckpoint, fallbackIndex) {
|
|
1135
|
+
const checkpoints = orderedCheckpoints2(run.checkpoints);
|
|
1136
|
+
if (typeof baseCheckpoint.step === "number") {
|
|
1137
|
+
const byStep = checkpoints.find((entry) => entry.step === baseCheckpoint.step);
|
|
1138
|
+
if (byStep) {
|
|
1139
|
+
return byStep;
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
const byName = checkpoints.find((entry) => entry.name === baseCheckpoint.name);
|
|
1143
|
+
if (byName) {
|
|
1144
|
+
return byName;
|
|
1145
|
+
}
|
|
1146
|
+
return checkpoints[fallbackIndex] ?? null;
|
|
1147
|
+
}
|
|
1148
|
+
async function materializeScreenshot2(args) {
|
|
1149
|
+
const sourcePath = screenshotSourcePath2(args.run, args.checkpoint);
|
|
1150
|
+
if (!sourcePath) {
|
|
1151
|
+
return null;
|
|
1152
|
+
}
|
|
1153
|
+
const extension = import_node_path3.default.extname(sourcePath) || ".png";
|
|
1154
|
+
const targetPath = import_node_path3.default.join(
|
|
1155
|
+
args.outputDir,
|
|
1156
|
+
args.config.screenshotsDir ?? "screenshots",
|
|
1157
|
+
args.storySlug,
|
|
1158
|
+
`${String(args.stepOrder).padStart(2, "0")}-${slugify3(args.run.project)}-${slugify3(args.checkpoint.name)}${extension}`
|
|
1159
|
+
);
|
|
1160
|
+
try {
|
|
1161
|
+
if (args.config.copyScreenshots !== false) {
|
|
1162
|
+
await import_promises3.default.mkdir(import_node_path3.default.dirname(targetPath), { recursive: true });
|
|
1163
|
+
await import_promises3.default.copyFile(sourcePath, targetPath);
|
|
1164
|
+
args.writtenFiles.add(targetPath);
|
|
1165
|
+
return rewriteImagePath2(args.mdxFile, targetPath, args.outputDir, args.config.imagePathPrefix);
|
|
1166
|
+
}
|
|
1167
|
+
return rewriteImagePath2(args.mdxFile, sourcePath, args.outputDir, args.config.imagePathPrefix);
|
|
1168
|
+
} catch {
|
|
1169
|
+
return null;
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
async function buildSteps2(args) {
|
|
1173
|
+
const baseCheckpoints = orderedCheckpoints2(args.primaryRun.checkpoints);
|
|
1174
|
+
const sortedRuns = sortRunsForVariants(args.runs);
|
|
1175
|
+
const steps = [];
|
|
1176
|
+
for (const [index, checkpoint] of baseCheckpoints.entries()) {
|
|
1177
|
+
const order = typeof checkpoint.step === "number" ? checkpoint.step : index + 1;
|
|
1178
|
+
const variants = [];
|
|
1179
|
+
const matchedCheckpoints = [];
|
|
1180
|
+
for (const run of sortedRuns) {
|
|
1181
|
+
const variantCheckpoint = findMatchingCheckpoint(run, checkpoint, index);
|
|
1182
|
+
if (!variantCheckpoint) {
|
|
1183
|
+
continue;
|
|
1184
|
+
}
|
|
1185
|
+
matchedCheckpoints.push(variantCheckpoint);
|
|
1186
|
+
variants.push({
|
|
1187
|
+
project: run.project,
|
|
1188
|
+
projectLabel: formatProjectLabel2(run.project),
|
|
1189
|
+
imagePath: await materializeScreenshot2({
|
|
1190
|
+
run,
|
|
1191
|
+
checkpoint: variantCheckpoint,
|
|
1192
|
+
storySlug: args.storySlug,
|
|
1193
|
+
stepOrder: order,
|
|
1194
|
+
outputDir: args.outputDir,
|
|
1195
|
+
mdxFile: args.mdxFile,
|
|
1196
|
+
config: args.config,
|
|
1197
|
+
writtenFiles: args.writtenFiles
|
|
1198
|
+
}),
|
|
1199
|
+
imageAlt: variantCheckpoint.title || `${checkpoint.name} (${formatProjectLabel2(run.project)})`
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
const descriptionSource = matchedCheckpoints.find(
|
|
1203
|
+
(entry) => typeof entry.description === "string" && entry.description.trim().length > 0
|
|
1204
|
+
) ?? checkpoint;
|
|
1205
|
+
const stepFocus = matchedCheckpoints.map((entry) => focusNote2(entry)).find((value) => Boolean(value)) ?? null;
|
|
1206
|
+
steps.push({
|
|
1207
|
+
checkpoint,
|
|
1208
|
+
order,
|
|
1209
|
+
title: checkpoint.name,
|
|
1210
|
+
description: typeof descriptionSource.description === "string" && descriptionSource.description.trim().length > 0 ? descriptionSource.description.trim() : autoDescription2(descriptionSource),
|
|
1211
|
+
focusNote: stepFocus,
|
|
1212
|
+
variants
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1215
|
+
return steps;
|
|
1216
|
+
}
|
|
1217
|
+
function renderVariantTabs(variants) {
|
|
1218
|
+
if (variants.length === 0) {
|
|
1219
|
+
return "";
|
|
1220
|
+
}
|
|
1221
|
+
if (variants.length === 1) {
|
|
1222
|
+
const [variant] = variants;
|
|
1223
|
+
if (!variant?.imagePath) {
|
|
1224
|
+
return "";
|
|
1225
|
+
}
|
|
1226
|
+
return `<Screenshot src={${quoteJsx(variant.imagePath)}} alt={${quoteJsx(variant.imageAlt)}} />`;
|
|
1227
|
+
}
|
|
1228
|
+
const tabs = variants.map((variant) => {
|
|
1229
|
+
const lines = [` <DeviceTab label={${quoteJsx(variant.projectLabel)}}>`];
|
|
1230
|
+
if (variant.imagePath) {
|
|
1231
|
+
lines.push(` <Screenshot src={${quoteJsx(variant.imagePath)}} alt={${quoteJsx(variant.imageAlt)}} />`);
|
|
1232
|
+
} else {
|
|
1233
|
+
lines.push(` <p>No screenshot captured for ${variant.projectLabel}.</p>`);
|
|
1234
|
+
}
|
|
1235
|
+
lines.push(" </DeviceTab>");
|
|
1236
|
+
return lines.join("\n");
|
|
1237
|
+
}).join("\n");
|
|
1238
|
+
return `<DeviceTabs>
|
|
1239
|
+
${tabs}
|
|
1240
|
+
</DeviceTabs>`;
|
|
1241
|
+
}
|
|
1242
|
+
function renderStep(step) {
|
|
1243
|
+
const lines = [` <Step number={${step.order}} title={${quoteJsx(step.title)}}>`];
|
|
1244
|
+
const variantBlock = renderVariantTabs(step.variants);
|
|
1245
|
+
if (variantBlock) {
|
|
1246
|
+
lines.push(` ${variantBlock.replace(/\n/g, "\n ")}`, "");
|
|
1247
|
+
}
|
|
1248
|
+
if (step.focusNote) {
|
|
1249
|
+
lines.push(` ${step.focusNote}`, "");
|
|
1250
|
+
}
|
|
1251
|
+
lines.push(` ${step.description}`, " </Step>");
|
|
1252
|
+
return lines.join("\n");
|
|
1253
|
+
}
|
|
1254
|
+
function renderMdx(args) {
|
|
1255
|
+
const importNames = /* @__PURE__ */ new Set(["Screenshot", "StepList", "Step"]);
|
|
1256
|
+
if (args.steps.some((step) => step.variants.length > 1)) {
|
|
1257
|
+
importNames.add("DeviceTabs");
|
|
1258
|
+
importNames.add("DeviceTab");
|
|
1259
|
+
}
|
|
1260
|
+
const frontmatter = serializeFrontmatter2({
|
|
1261
|
+
title: args.title,
|
|
1262
|
+
tags: frontmatterTags([...new Set(args.runs.flatMap((run) => run.tags))]),
|
|
1263
|
+
generatedAt: args.generatedAt,
|
|
1264
|
+
projects: [...new Set(args.runs.map((run) => run.project))]
|
|
1265
|
+
});
|
|
1266
|
+
const stepBlocks = args.steps.map(renderStep).join("\n\n");
|
|
1267
|
+
return `${frontmatter}import { ${[...importNames].join(", ")} } from '${args.config.componentImportPath}';
|
|
1268
|
+
|
|
1269
|
+
<StepList>
|
|
1270
|
+
${stepBlocks}
|
|
1271
|
+
</StepList>
|
|
1272
|
+
`;
|
|
1273
|
+
}
|
|
1274
|
+
var mdxReporter = {
|
|
1275
|
+
name: "mdx",
|
|
1276
|
+
description: "Generates one MDX help article per captured story.",
|
|
1277
|
+
validateConfig(config) {
|
|
1278
|
+
return config != null && typeof config === "object" && !Array.isArray(config);
|
|
1279
|
+
},
|
|
1280
|
+
async generate(context) {
|
|
1281
|
+
const config = normalizeConfig2(context.config);
|
|
1282
|
+
const stories = groupByStory(context.runs);
|
|
1283
|
+
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1284
|
+
const writtenFiles = /* @__PURE__ */ new Set();
|
|
1285
|
+
let articleCount = 0;
|
|
1286
|
+
for (const [storyTitle, runs] of stories) {
|
|
1287
|
+
const primaryRun = choosePrimaryRun2(runs, config.preferredProject);
|
|
1288
|
+
if (!primaryRun || !shouldIncludeRun2(primaryRun, config)) {
|
|
1289
|
+
continue;
|
|
1290
|
+
}
|
|
1291
|
+
const title = stripTags2(storyTitle);
|
|
1292
|
+
const storySlug = slugify3(title);
|
|
1293
|
+
const mdxFile = import_node_path3.default.join(context.outputDir, config.storiesDir ?? ".", `${storySlug}.mdx`);
|
|
1294
|
+
const steps = await buildSteps2({
|
|
1295
|
+
runs,
|
|
1296
|
+
primaryRun,
|
|
1297
|
+
storySlug,
|
|
1298
|
+
outputDir: context.outputDir,
|
|
1299
|
+
mdxFile,
|
|
1300
|
+
config,
|
|
1301
|
+
writtenFiles
|
|
1302
|
+
});
|
|
1303
|
+
await import_promises3.default.mkdir(import_node_path3.default.dirname(mdxFile), { recursive: true });
|
|
1304
|
+
await import_promises3.default.writeFile(
|
|
1305
|
+
mdxFile,
|
|
1306
|
+
renderMdx({
|
|
1307
|
+
title,
|
|
1308
|
+
steps,
|
|
1309
|
+
runs,
|
|
1310
|
+
config,
|
|
1311
|
+
generatedAt
|
|
1312
|
+
}),
|
|
1313
|
+
"utf8"
|
|
1314
|
+
);
|
|
1315
|
+
writtenFiles.add(mdxFile);
|
|
1316
|
+
articleCount += 1;
|
|
1317
|
+
}
|
|
1318
|
+
return {
|
|
1319
|
+
files: [...writtenFiles],
|
|
1320
|
+
summary: `Generated ${articleCount} MDX article${articleCount === 1 ? "" : "s"}.`
|
|
1321
|
+
};
|
|
1322
|
+
}
|
|
1323
|
+
};
|
|
1324
|
+
|
|
1325
|
+
// src/report/index.ts
|
|
1326
|
+
var builtinReporters = /* @__PURE__ */ new Map();
|
|
1327
|
+
var builtinReporterDefaults = {
|
|
1328
|
+
html: true,
|
|
1329
|
+
markdown: false,
|
|
1330
|
+
mdx: false
|
|
1331
|
+
};
|
|
1332
|
+
async function walkFiles(directory) {
|
|
1333
|
+
const dirents = await import_promises4.default.readdir(directory, { withFileTypes: true });
|
|
1334
|
+
const files = [];
|
|
1335
|
+
for (const dirent of dirents) {
|
|
1336
|
+
const absolutePath = import_node_path4.default.join(directory, dirent.name);
|
|
1337
|
+
if (dirent.isDirectory()) {
|
|
1338
|
+
files.push(...await walkFiles(absolutePath));
|
|
1339
|
+
continue;
|
|
1340
|
+
}
|
|
1341
|
+
if (dirent.isFile()) {
|
|
1342
|
+
files.push(absolutePath);
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
return files;
|
|
1346
|
+
}
|
|
1347
|
+
function isCheckpointManifestFile(filePath) {
|
|
1348
|
+
const fileName = import_node_path4.default.basename(filePath);
|
|
1349
|
+
return fileName === "checkpoint-manifest.json" || fileName.startsWith("checkpoint-manifest-") && fileName.endsWith(".json");
|
|
1350
|
+
}
|
|
1351
|
+
function isCheckpointManifest(value) {
|
|
1352
|
+
if (!value || typeof value !== "object") {
|
|
1353
|
+
return false;
|
|
1354
|
+
}
|
|
1355
|
+
const manifest = value;
|
|
1356
|
+
return typeof manifest.project === "string" && typeof manifest.testId === "string" && typeof manifest.title === "string" && typeof manifest.startedAt === "string" && Array.isArray(manifest.tags) && Array.isArray(manifest.checkpoints);
|
|
1357
|
+
}
|
|
1358
|
+
function toRunRecord(manifest, sourceManifestPath) {
|
|
1359
|
+
return {
|
|
1360
|
+
key: `${manifest.testId}|${manifest.project}|${manifest.startedAt}`,
|
|
1361
|
+
sourceManifestPath,
|
|
1362
|
+
environment: manifest.environment || "unknown",
|
|
1363
|
+
project: manifest.project,
|
|
1364
|
+
testId: manifest.testId,
|
|
1365
|
+
title: manifest.title,
|
|
1366
|
+
tags: manifest.tags,
|
|
1367
|
+
startedAt: manifest.startedAt,
|
|
1368
|
+
checkpoints: manifest.checkpoints
|
|
1369
|
+
};
|
|
1370
|
+
}
|
|
1371
|
+
function toManifest(run) {
|
|
1372
|
+
return {
|
|
1373
|
+
environment: run.environment,
|
|
1374
|
+
project: run.project,
|
|
1375
|
+
testId: run.testId,
|
|
1376
|
+
title: run.title,
|
|
1377
|
+
tags: run.tags,
|
|
1378
|
+
startedAt: run.startedAt,
|
|
1379
|
+
checkpoints: run.checkpoints
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1382
|
+
function normalizeReporterConfig(config) {
|
|
1383
|
+
if (config == null || config === false) {
|
|
1384
|
+
return null;
|
|
1385
|
+
}
|
|
1386
|
+
if (config === true) {
|
|
1387
|
+
return {};
|
|
1388
|
+
}
|
|
1389
|
+
return { ...config };
|
|
1390
|
+
}
|
|
1391
|
+
function registerBuiltinReporter(reporter) {
|
|
1392
|
+
builtinReporters.set(reporter.name, reporter);
|
|
1393
|
+
}
|
|
1394
|
+
function dedupeRuns(runs) {
|
|
1395
|
+
const map = /* @__PURE__ */ new Map();
|
|
1396
|
+
for (const run of runs) {
|
|
1397
|
+
const existing = map.get(run.key);
|
|
1398
|
+
if (!existing) {
|
|
1399
|
+
map.set(run.key, run);
|
|
1400
|
+
continue;
|
|
1401
|
+
}
|
|
1402
|
+
const existingTime = new Date(existing.startedAt).getTime();
|
|
1403
|
+
const currentTime = new Date(run.startedAt).getTime();
|
|
1404
|
+
if (currentTime >= existingTime) {
|
|
1405
|
+
map.set(run.key, run);
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
return [...map.values()];
|
|
1409
|
+
}
|
|
1410
|
+
async function loadRuns(testResultsDir) {
|
|
1411
|
+
let manifestFiles;
|
|
1412
|
+
try {
|
|
1413
|
+
manifestFiles = (await walkFiles(testResultsDir)).filter(isCheckpointManifestFile);
|
|
1414
|
+
} catch {
|
|
1415
|
+
return [];
|
|
1416
|
+
}
|
|
1417
|
+
const runs = [];
|
|
1418
|
+
for (const manifestPath of manifestFiles) {
|
|
1419
|
+
let rawManifest;
|
|
1420
|
+
try {
|
|
1421
|
+
rawManifest = JSON.parse(await import_promises4.default.readFile(manifestPath, "utf8"));
|
|
1422
|
+
} catch {
|
|
1423
|
+
continue;
|
|
1424
|
+
}
|
|
1425
|
+
if (!isCheckpointManifest(rawManifest)) {
|
|
1426
|
+
continue;
|
|
1427
|
+
}
|
|
1428
|
+
runs.push(toRunRecord(rawManifest, manifestPath));
|
|
1429
|
+
}
|
|
1430
|
+
return dedupeRuns(runs);
|
|
1431
|
+
}
|
|
1432
|
+
async function runReporters(config, testResultsDir, outputDir) {
|
|
1433
|
+
const runs = await loadRuns(testResultsDir);
|
|
1434
|
+
const manifests = runs.map(toManifest);
|
|
1435
|
+
const results = {};
|
|
1436
|
+
const reporterConfigMap = {
|
|
1437
|
+
...builtinReporterDefaults,
|
|
1438
|
+
...config.reporters ?? {}
|
|
1439
|
+
};
|
|
1440
|
+
for (const [name, value] of Object.entries(reporterConfigMap)) {
|
|
1441
|
+
const reporterConfig = normalizeReporterConfig(value);
|
|
1442
|
+
if (!reporterConfig) {
|
|
1443
|
+
continue;
|
|
1444
|
+
}
|
|
1445
|
+
const reporter = builtinReporters.get(name);
|
|
1446
|
+
if (!reporter) {
|
|
1447
|
+
throw new Error(`Reporter "${name}" is enabled but no implementation is registered.`);
|
|
1448
|
+
}
|
|
1449
|
+
if (reporter.validateConfig && !reporter.validateConfig(reporterConfig)) {
|
|
1450
|
+
throw new Error(`Reporter "${name}" received invalid configuration.`);
|
|
1451
|
+
}
|
|
1452
|
+
results[name] = await reporter.generate({
|
|
1453
|
+
runs,
|
|
1454
|
+
outputDir,
|
|
1455
|
+
config: reporterConfig,
|
|
1456
|
+
manifests
|
|
1457
|
+
});
|
|
1458
|
+
}
|
|
1459
|
+
return results;
|
|
1460
|
+
}
|
|
1461
|
+
registerBuiltinReporter(htmlReporter);
|
|
1462
|
+
registerBuiltinReporter(markdownReporter);
|
|
1463
|
+
registerBuiltinReporter(mdxReporter);
|
|
1464
|
+
|
|
1465
|
+
// src/cli/index.ts
|
|
1466
|
+
var DEFAULT_RESULTS_DIR = "test-results";
|
|
1467
|
+
var DEFAULT_REPORT_OUTPUT_DIR = "report";
|
|
1468
|
+
|
|
1469
|
+
// src/teardown.ts
|
|
1470
|
+
function resolveResultsDir(config) {
|
|
1471
|
+
if (process.env.PLAYWRIGHT_CHECKPOINT_RESULTS_DIR) {
|
|
1472
|
+
return import_node_path6.default.resolve(process.env.PLAYWRIGHT_CHECKPOINT_RESULTS_DIR);
|
|
1473
|
+
}
|
|
1474
|
+
const defaultDir = import_node_path6.default.resolve(process.cwd(), DEFAULT_RESULTS_DIR);
|
|
1475
|
+
if (import_node_fs.default.existsSync(defaultDir)) {
|
|
1476
|
+
return defaultDir;
|
|
1477
|
+
}
|
|
1478
|
+
const firstProjectOutputDir = config?.projects.find((project) => typeof project.outputDir === "string")?.outputDir;
|
|
1479
|
+
return import_node_path6.default.resolve(firstProjectOutputDir ?? defaultDir);
|
|
1480
|
+
}
|
|
1481
|
+
function resolveOutputDir() {
|
|
1482
|
+
if (process.env.PLAYWRIGHT_CHECKPOINT_REPORT_DIR) {
|
|
1483
|
+
return import_node_path6.default.resolve(process.env.PLAYWRIGHT_CHECKPOINT_REPORT_DIR);
|
|
1484
|
+
}
|
|
1485
|
+
return import_node_path6.default.resolve(process.cwd(), DEFAULT_REPORT_OUTPUT_DIR);
|
|
1486
|
+
}
|
|
1487
|
+
async function globalTeardown(config) {
|
|
1488
|
+
const testResultsDir = resolveResultsDir(config);
|
|
1489
|
+
const outputDir = resolveOutputDir();
|
|
1490
|
+
try {
|
|
1491
|
+
const results = await runReporters({}, testResultsDir, outputDir);
|
|
1492
|
+
const summaries = Object.entries(results).map(([name, result]) => `- ${name}: ${result.summary}`);
|
|
1493
|
+
console.log(`[playwright-checkpoint] Generated reports from ${testResultsDir} to ${outputDir}`);
|
|
1494
|
+
if (summaries.length > 0) {
|
|
1495
|
+
for (const summary of summaries) {
|
|
1496
|
+
console.log(summary);
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
} catch (error) {
|
|
1500
|
+
console.error("[playwright-checkpoint] Global teardown report generation failed.");
|
|
1501
|
+
console.error(error instanceof Error ? error.stack ?? error.message : String(error));
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
var teardown_default = globalTeardown;
|
|
1505
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1506
|
+
0 && (module.exports = {
|
|
1507
|
+
globalTeardown
|
|
1508
|
+
});
|
|
1509
|
+
//# sourceMappingURL=teardown.cjs.map
|