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