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,3972 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __esm = (fn, res) => function __init() {
10
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
11
+ };
12
+ var __export = (target, all) => {
13
+ for (var name in all)
14
+ __defProp(target, name, { get: all[name], enumerable: true });
15
+ };
16
+ var __copyProps = (to, from, except, desc) => {
17
+ if (from && typeof from === "object" || typeof from === "function") {
18
+ for (let key of __getOwnPropNames(from))
19
+ if (!__hasOwnProp.call(to, key) && key !== except)
20
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
21
+ }
22
+ return to;
23
+ };
24
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
25
+ // If the importer is in node compatibility mode or this is not an ESM
26
+ // file that has been converted to a CommonJS file using a Babel-
27
+ // compatible transform (i.e. "__esModule" has not been set), then set
28
+ // "default" to the CommonJS "module.exports" for node compatibility.
29
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
30
+ mod
31
+ ));
32
+
33
+ // src/report/story-utils.ts
34
+ function groupByStory(runs) {
35
+ const stories = /* @__PURE__ */ new Map();
36
+ for (const run of runs) {
37
+ const existing = stories.get(run.title) ?? [];
38
+ existing.push(run);
39
+ stories.set(run.title, existing);
40
+ }
41
+ return stories;
42
+ }
43
+ function orderedCheckpointNames(runs) {
44
+ const names = [];
45
+ const seen = /* @__PURE__ */ new Set();
46
+ for (const run of runs) {
47
+ for (const checkpoint of run.checkpoints) {
48
+ if (seen.has(checkpoint.name)) {
49
+ continue;
50
+ }
51
+ seen.add(checkpoint.name);
52
+ names.push(checkpoint.name);
53
+ }
54
+ }
55
+ return names;
56
+ }
57
+ var init_story_utils = __esm({
58
+ "src/report/story-utils.ts"() {
59
+ "use strict";
60
+ }
61
+ });
62
+
63
+ // src/report/html-reporter.ts
64
+ function escapeHtml(value) {
65
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
66
+ }
67
+ function slugify(value) {
68
+ return value.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "story";
69
+ }
70
+ function formatDateTime(isoDate) {
71
+ const date = new Date(isoDate);
72
+ if (Number.isNaN(date.getTime())) {
73
+ return isoDate;
74
+ }
75
+ return new Intl.DateTimeFormat("en-US", {
76
+ dateStyle: "medium",
77
+ timeStyle: "short"
78
+ }).format(date);
79
+ }
80
+ function projectWeight(projectName, projectOrder) {
81
+ const index = projectOrder.indexOf(projectName);
82
+ return index === -1 ? Number.MAX_SAFE_INTEGER : index;
83
+ }
84
+ function formatProjectLabel(projectName) {
85
+ const [device, mode] = projectName.split("-");
86
+ if (!device || !mode) {
87
+ return projectName;
88
+ }
89
+ const deviceLabel = device === "desktop" ? "Desktop" : device === "mobile" ? "Mobile" : device;
90
+ const modeLabel = mode === "light" ? "Light" : mode === "dark" ? "Dark" : mode;
91
+ return `${deviceLabel} / ${modeLabel}`;
92
+ }
93
+ function sortByProjectAndTime(a, b, projectOrder) {
94
+ const byProject = projectWeight(a.project, projectOrder) - projectWeight(b.project, projectOrder);
95
+ if (byProject !== 0) {
96
+ return byProject;
97
+ }
98
+ const byProjectName = a.project.localeCompare(b.project);
99
+ if (byProjectName !== 0) {
100
+ return byProjectName;
101
+ }
102
+ return new Date(a.startedAt).getTime() - new Date(b.startedAt).getTime();
103
+ }
104
+ function getCollectorSummaryNumber(checkpoint, collectorName, key) {
105
+ const value = checkpoint.collectors[collectorName]?.summary[key];
106
+ return typeof value === "number" ? value : null;
107
+ }
108
+ function screenshotData(checkpoint) {
109
+ const data = checkpoint.collectors.screenshot?.data;
110
+ return data && typeof data === "object" ? data : null;
111
+ }
112
+ function highlightOverlayStyle(checkpoint) {
113
+ const data = screenshotData(checkpoint);
114
+ const bounds = data?.highlightBounds;
115
+ const imageSize = data?.imageSize;
116
+ 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) {
117
+ return null;
118
+ }
119
+ const left = bounds.x / imageSize.width * 100;
120
+ const top = bounds.y / imageSize.height * 100;
121
+ const width = bounds.width / imageSize.width * 100;
122
+ const height = bounds.height / imageSize.height * 100;
123
+ return [
124
+ `left:${left.toFixed(4)}%`,
125
+ `top:${top.toFixed(4)}%`,
126
+ `width:${width.toFixed(4)}%`,
127
+ `height:${height.toFixed(4)}%`
128
+ ].join(";");
129
+ }
130
+ function highlightLabel(checkpoint) {
131
+ const selector = screenshotData(checkpoint)?.highlightSelector;
132
+ return typeof selector === "string" && selector.trim().length > 0 ? `Focus: ${selector.trim()}` : null;
133
+ }
134
+ function resolveArtifactPath(run, artifactPath) {
135
+ return import_node_path.default.isAbsolute(artifactPath) ? artifactPath : import_node_path.default.resolve(import_node_path.default.dirname(run.sourceManifestPath), artifactPath);
136
+ }
137
+ function toEncodedHref(outputDir, filePath) {
138
+ if (!filePath) {
139
+ return null;
140
+ }
141
+ const relativePath = import_node_path.default.relative(outputDir, filePath);
142
+ return relativePath.split(import_node_path.default.sep).map(encodeURIComponent).join("/");
143
+ }
144
+ function getArtifactHref(run, checkpoint, outputDir, collectorName, artifactName) {
145
+ const artifacts = checkpoint.collectors[collectorName]?.artifacts ?? [];
146
+ const artifact = artifactName ? artifacts.find((entry) => entry.name === artifactName) : artifacts[0];
147
+ if (!artifact?.path) {
148
+ return null;
149
+ }
150
+ return toEncodedHref(outputDir, resolveArtifactPath(run, artifact.path));
151
+ }
152
+ function renderArtifactLinks(run, checkpoint, outputDir) {
153
+ const links = [
154
+ { label: "DOM HTML", href: getArtifactHref(run, checkpoint, outputDir, "html", "html") },
155
+ { label: "Axe", href: getArtifactHref(run, checkpoint, outputDir, "axe", "axe") },
156
+ { label: "Web Vitals", href: getArtifactHref(run, checkpoint, outputDir, "web-vitals", "web-vitals") },
157
+ { label: "Console", href: getArtifactHref(run, checkpoint, outputDir, "console", "console-errors") },
158
+ { label: "Failed Requests", href: getArtifactHref(run, checkpoint, outputDir, "network", "failed-requests") }
159
+ ];
160
+ return links.map((link) => {
161
+ if (!link.href) {
162
+ return `<span class="artifact disabled">${escapeHtml(link.label)}</span>`;
163
+ }
164
+ return `<a class="artifact" href="${link.href}" target="_blank" rel="noreferrer">${escapeHtml(link.label)}</a>`;
165
+ }).join("");
166
+ }
167
+ function renderCheckpointCard(run, checkpointName, outputDir) {
168
+ const checkpoint = run.checkpoints.find((entry) => entry.name === checkpointName);
169
+ if (!checkpoint) {
170
+ return `
171
+ <article class="variant-card missing">
172
+ <header class="variant-card-header">
173
+ <div>
174
+ <h5>${escapeHtml(formatProjectLabel(run.project))}</h5>
175
+ <p>${escapeHtml(run.project)}</p>
176
+ </div>
177
+ <time>${escapeHtml(formatDateTime(run.startedAt))}</time>
178
+ </header>
179
+ <div class="empty-card">No checkpoint captured for this run.</div>
180
+ </article>
181
+ `;
182
+ }
183
+ const screenshotHref = getArtifactHref(run, checkpoint, outputDir, "screenshot", "screenshot");
184
+ const overlayStyle = highlightOverlayStyle(checkpoint);
185
+ const focus = highlightLabel(checkpoint);
186
+ const axeViolations = getCollectorSummaryNumber(checkpoint, "axe", "violations");
187
+ const consoleErrors = getCollectorSummaryNumber(checkpoint, "console", "consoleErrorCount") ?? 0;
188
+ const failedRequests = getCollectorSummaryNumber(checkpoint, "network", "failedRequestCount") ?? 0;
189
+ return `
190
+ <article class="variant-card">
191
+ <header class="variant-card-header">
192
+ <div>
193
+ <h5>${escapeHtml(formatProjectLabel(run.project))}</h5>
194
+ <p>${escapeHtml(run.project)}</p>
195
+ </div>
196
+ <time>${escapeHtml(formatDateTime(checkpoint.timestamp || run.startedAt))}</time>
197
+ </header>
198
+ <p class="page-meta">
199
+ <span>${escapeHtml(checkpoint.title || "Untitled page")}</span>
200
+ <span class="page-url">${escapeHtml(checkpoint.url)}</span>
201
+ </p>
202
+ ${screenshotHref ? `<a class="thumbnail-link" href="${screenshotHref}" target="_blank" rel="noreferrer">
203
+ <img src="${screenshotHref}" alt="${escapeHtml(`${run.project} \u2014 ${checkpoint.name}`)}" loading="lazy" />
204
+ ${overlayStyle ? `<span class="highlight-overlay" style="${overlayStyle}" aria-hidden="true"></span>` : ""}
205
+ ${focus ? `<span class="highlight-label">${escapeHtml(focus)}</span>` : ""}
206
+ </a>` : '<div class="empty-card">Screenshot unavailable.</div>'}
207
+ <div class="stats-grid">
208
+ <span><strong>${axeViolations ?? "n/a"}</strong><small>Axe violations</small></span>
209
+ <span><strong>${consoleErrors}</strong><small>Console errors</small></span>
210
+ <span><strong>${failedRequests}</strong><small>Failed requests</small></span>
211
+ </div>
212
+ <div class="artifact-list">${renderArtifactLinks(run, checkpoint, outputDir)}</div>
213
+ </article>
214
+ `;
215
+ }
216
+ function renderStorySection(title, runs, outputDir) {
217
+ const checkpointNames = orderedCheckpointNames(runs);
218
+ const environments = [...new Set(runs.map((run) => run.environment))].sort();
219
+ const tags = [...new Set(runs.flatMap((run) => run.tags))].sort();
220
+ const checkpointBlocks = checkpointNames.map(
221
+ (checkpointName) => `
222
+ <details class="accordion checkpoint-block">
223
+ <summary class="checkpoint-summary">
224
+ <span>${escapeHtml(checkpointName)}</span>
225
+ <span class="checkpoint-meta">${runs.length} variants</span>
226
+ </summary>
227
+ <div class="variant-grid">
228
+ ${runs.map((run) => renderCheckpointCard(run, checkpointName, outputDir)).join("")}
229
+ </div>
230
+ </details>
231
+ `
232
+ ).join("");
233
+ return `
234
+ <details class="accordion story-block" id="story-${slugify(title)}" open>
235
+ <summary class="story-summary">
236
+ <span class="story-title">${escapeHtml(title)}</span>
237
+ <span class="story-meta-chip">${runs.length} run${runs.length === 1 ? "" : "s"}</span>
238
+ </summary>
239
+ <div class="story-body">
240
+ <div class="story-meta-row">
241
+ <span><strong>Projects</strong> ${escapeHtml(runs.map((run) => run.project).join(", "))}</span>
242
+ <span><strong>Environments</strong> ${escapeHtml(environments.join(", ") || "n/a")}</span>
243
+ <span><strong>Tags</strong> ${escapeHtml(tags.join(", ") || "none")}</span>
244
+ </div>
245
+ ${checkpointBlocks || '<p class="empty-state">No checkpoints captured for this story.</p>'}
246
+ </div>
247
+ </details>
248
+ `;
249
+ }
250
+ function buildHtmlReport(runs, outputDir, config) {
251
+ const groupedRuns = groupByStory(runs);
252
+ const storyTitles = [...groupedRuns.keys()].sort((a, b) => a.localeCompare(b));
253
+ const projectOrder = Array.isArray(config.projectOrder) ? config.projectOrder.filter((value) => typeof value === "string") : DEFAULT_PROJECT_ORDER;
254
+ const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
255
+ const reportTitle = typeof config.title === "string" && config.title.trim() ? config.title.trim() : "Playwright Checkpoint Report";
256
+ for (const title of storyTitles) {
257
+ groupedRuns.get(title)?.sort((a, b) => sortByProjectAndTime(a, b, projectOrder));
258
+ }
259
+ const navLinks = storyTitles.map((title) => `<a href="#story-${slugify(title)}">${escapeHtml(title)}</a>`).join("");
260
+ const storySections = storyTitles.map((title) => renderStorySection(title, groupedRuns.get(title) ?? [], outputDir)).join("");
261
+ return `<!doctype html>
262
+ <html lang="en">
263
+ <head>
264
+ <meta charset="utf-8" />
265
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
266
+ <title>${escapeHtml(reportTitle)}</title>
267
+ <style>
268
+ :root {
269
+ color-scheme: dark;
270
+ --bg: #0b1020;
271
+ --panel: rgba(15, 23, 42, 0.88);
272
+ --panel-2: rgba(17, 25, 40, 0.98);
273
+ --text: #e5eefb;
274
+ --muted: #9fb3c8;
275
+ --accent: #60a5fa;
276
+ --accent-2: #22d3ee;
277
+ --border: rgba(148, 163, 184, 0.18);
278
+ --success: #34d399;
279
+ --warning: #fbbf24;
280
+ --danger: #fb7185;
281
+ --shadow: 0 24px 64px rgba(2, 6, 23, 0.45);
282
+ }
283
+ * { box-sizing: border-box; }
284
+ html { scroll-behavior: smooth; }
285
+ body {
286
+ margin: 0;
287
+ min-height: 100vh;
288
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
289
+ background:
290
+ radial-gradient(circle at top, rgba(96, 165, 250, 0.14), transparent 30%),
291
+ linear-gradient(180deg, #07101f 0%, #0b1020 100%);
292
+ color: var(--text);
293
+ }
294
+ a { color: inherit; }
295
+ .page {
296
+ width: min(1600px, calc(100vw - 32px));
297
+ margin: 0 auto;
298
+ padding: 28px 0 56px;
299
+ }
300
+ .hero {
301
+ background: linear-gradient(180deg, rgba(15, 23, 42, 0.96), rgba(15, 23, 42, 0.84));
302
+ border: 1px solid var(--border);
303
+ border-radius: 24px;
304
+ padding: 24px;
305
+ box-shadow: var(--shadow);
306
+ backdrop-filter: blur(18px);
307
+ }
308
+ .hero h1 {
309
+ margin: 0;
310
+ font-size: clamp(1.9rem, 2.6vw, 3rem);
311
+ line-height: 1.1;
312
+ }
313
+ .hero p {
314
+ margin: 10px 0 0;
315
+ color: var(--muted);
316
+ max-width: 72ch;
317
+ line-height: 1.6;
318
+ }
319
+ .summary-bar {
320
+ display: flex;
321
+ flex-wrap: wrap;
322
+ gap: 12px;
323
+ margin-top: 18px;
324
+ }
325
+ .summary-pill {
326
+ display: inline-flex;
327
+ gap: 8px;
328
+ align-items: center;
329
+ padding: 9px 12px;
330
+ border: 1px solid var(--border);
331
+ border-radius: 999px;
332
+ background: rgba(15, 23, 42, 0.72);
333
+ color: var(--muted);
334
+ font-size: 0.92rem;
335
+ }
336
+ .summary-pill strong { color: var(--text); }
337
+ .toolbar {
338
+ display: flex;
339
+ flex-wrap: wrap;
340
+ justify-content: space-between;
341
+ gap: 16px;
342
+ margin-top: 18px;
343
+ padding-top: 18px;
344
+ border-top: 1px solid var(--border);
345
+ }
346
+ .story-nav {
347
+ display: flex;
348
+ flex-wrap: wrap;
349
+ gap: 10px;
350
+ }
351
+ .story-nav a,
352
+ .toolbar button,
353
+ .artifact {
354
+ border: 1px solid var(--border);
355
+ border-radius: 999px;
356
+ background: rgba(15, 23, 42, 0.7);
357
+ color: var(--text);
358
+ text-decoration: none;
359
+ padding: 8px 12px;
360
+ font: inherit;
361
+ font-size: 0.86rem;
362
+ transition: transform 140ms ease, border-color 140ms ease, background 140ms ease;
363
+ }
364
+ .toolbar button:hover,
365
+ .story-nav a:hover,
366
+ .artifact:hover {
367
+ transform: translateY(-1px);
368
+ border-color: rgba(96, 165, 250, 0.55);
369
+ background: rgba(30, 41, 59, 0.96);
370
+ cursor: pointer;
371
+ }
372
+ .content {
373
+ display: grid;
374
+ gap: 18px;
375
+ margin-top: 22px;
376
+ }
377
+ .accordion {
378
+ border: 1px solid var(--border);
379
+ border-radius: 22px;
380
+ background: var(--panel);
381
+ box-shadow: var(--shadow);
382
+ overflow: hidden;
383
+ }
384
+ .accordion summary {
385
+ list-style: none;
386
+ cursor: pointer;
387
+ }
388
+ .accordion summary::-webkit-details-marker { display: none; }
389
+ .story-summary,
390
+ .checkpoint-summary {
391
+ display: flex;
392
+ justify-content: space-between;
393
+ align-items: center;
394
+ gap: 12px;
395
+ }
396
+ .story-summary {
397
+ padding: 20px 22px;
398
+ background: linear-gradient(180deg, rgba(15, 23, 42, 0.92), rgba(15, 23, 42, 0.74));
399
+ }
400
+ .story-title {
401
+ font-size: 1.1rem;
402
+ font-weight: 700;
403
+ }
404
+ .story-meta-chip,
405
+ .checkpoint-meta {
406
+ color: var(--muted);
407
+ font-size: 0.84rem;
408
+ white-space: nowrap;
409
+ }
410
+ .story-body {
411
+ padding: 0 22px 22px;
412
+ }
413
+ .story-meta-row {
414
+ display: flex;
415
+ flex-wrap: wrap;
416
+ gap: 16px;
417
+ color: var(--muted);
418
+ font-size: 0.92rem;
419
+ line-height: 1.5;
420
+ margin: 4px 0 18px;
421
+ }
422
+ .story-meta-row strong { color: var(--text); margin-right: 6px; }
423
+ .checkpoint-block {
424
+ margin-top: 14px;
425
+ border-radius: 18px;
426
+ background: var(--panel-2);
427
+ border: 1px solid rgba(148, 163, 184, 0.14);
428
+ }
429
+ .checkpoint-summary {
430
+ padding: 16px 18px;
431
+ font-weight: 600;
432
+ background: rgba(15, 23, 42, 0.68);
433
+ }
434
+ .variant-grid {
435
+ display: grid;
436
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
437
+ gap: 14px;
438
+ padding: 0 18px 18px;
439
+ }
440
+ .variant-card {
441
+ display: grid;
442
+ gap: 12px;
443
+ border: 1px solid rgba(148, 163, 184, 0.14);
444
+ border-radius: 18px;
445
+ background: rgba(15, 23, 42, 0.72);
446
+ padding: 16px;
447
+ min-height: 100%;
448
+ }
449
+ .variant-card.missing {
450
+ opacity: 0.72;
451
+ border-style: dashed;
452
+ }
453
+ .variant-card-header {
454
+ display: flex;
455
+ justify-content: space-between;
456
+ align-items: flex-start;
457
+ gap: 10px;
458
+ }
459
+ .variant-card-header h5 {
460
+ margin: 0;
461
+ font-size: 1rem;
462
+ }
463
+ .variant-card-header p,
464
+ .variant-card-header time {
465
+ margin: 4px 0 0;
466
+ color: var(--muted);
467
+ font-size: 0.83rem;
468
+ }
469
+ .page-meta {
470
+ display: grid;
471
+ gap: 4px;
472
+ margin: 0;
473
+ color: var(--muted);
474
+ font-size: 0.9rem;
475
+ }
476
+ .page-url {
477
+ overflow-wrap: anywhere;
478
+ font-size: 0.82rem;
479
+ }
480
+ .thumbnail-link {
481
+ position: relative;
482
+ display: block;
483
+ border-radius: 14px;
484
+ overflow: hidden;
485
+ border: 1px solid rgba(148, 163, 184, 0.18);
486
+ background: rgba(2, 6, 23, 0.65);
487
+ }
488
+ .thumbnail-link img {
489
+ display: block;
490
+ width: 100%;
491
+ aspect-ratio: 16 / 10;
492
+ object-fit: cover;
493
+ }
494
+ .highlight-overlay {
495
+ position: absolute;
496
+ border: 2px solid var(--danger);
497
+ border-radius: 12px;
498
+ background: rgba(251, 113, 133, 0.08);
499
+ box-shadow: 0 0 0 999px rgba(251, 113, 133, 0.02);
500
+ pointer-events: none;
501
+ }
502
+ .highlight-label {
503
+ position: absolute;
504
+ left: 12px;
505
+ bottom: 12px;
506
+ max-width: calc(100% - 24px);
507
+ padding: 6px 9px;
508
+ border-radius: 999px;
509
+ background: rgba(15, 23, 42, 0.9);
510
+ border: 1px solid rgba(251, 113, 133, 0.35);
511
+ color: #ffe4e6;
512
+ font-size: 0.74rem;
513
+ line-height: 1.3;
514
+ overflow-wrap: anywhere;
515
+ }
516
+ .stats-grid {
517
+ display: grid;
518
+ grid-template-columns: repeat(3, minmax(0, 1fr));
519
+ gap: 10px;
520
+ }
521
+ .stats-grid span {
522
+ display: grid;
523
+ gap: 6px;
524
+ padding: 10px 12px;
525
+ border-radius: 14px;
526
+ background: rgba(2, 6, 23, 0.42);
527
+ border: 1px solid rgba(148, 163, 184, 0.12);
528
+ }
529
+ .stats-grid strong {
530
+ font-size: 1.15rem;
531
+ line-height: 1;
532
+ }
533
+ .stats-grid small {
534
+ color: var(--muted);
535
+ font-size: 0.76rem;
536
+ text-transform: uppercase;
537
+ letter-spacing: 0.04em;
538
+ }
539
+ .artifact-list {
540
+ display: flex;
541
+ flex-wrap: wrap;
542
+ gap: 8px;
543
+ }
544
+ .artifact.disabled {
545
+ opacity: 0.45;
546
+ pointer-events: none;
547
+ }
548
+ .empty-state,
549
+ .empty-card {
550
+ margin: 0;
551
+ color: var(--muted);
552
+ padding: 12px;
553
+ border: 1px dashed rgba(148, 163, 184, 0.2);
554
+ border-radius: 14px;
555
+ background: rgba(2, 6, 23, 0.24);
556
+ }
557
+ @media (max-width: 720px) {
558
+ .page {
559
+ width: min(100vw - 20px, 1600px);
560
+ padding-top: 18px;
561
+ }
562
+ .hero,
563
+ .story-summary,
564
+ .story-body,
565
+ .checkpoint-summary,
566
+ .variant-grid {
567
+ padding-left: 16px;
568
+ padding-right: 16px;
569
+ }
570
+ .variant-card-header,
571
+ .story-summary,
572
+ .checkpoint-summary,
573
+ .toolbar {
574
+ flex-direction: column;
575
+ align-items: flex-start;
576
+ }
577
+ .stats-grid {
578
+ grid-template-columns: 1fr;
579
+ }
580
+ }
581
+ </style>
582
+ </head>
583
+ <body>
584
+ <main class="page">
585
+ <section class="hero">
586
+ <h1>${escapeHtml(reportTitle)}</h1>
587
+ <p>Explore checkpoint runs by story, inspect every project variant side-by-side, and jump directly to screenshots and generated artifacts.</p>
588
+ <div class="summary-bar">
589
+ <span class="summary-pill"><strong>Generated</strong> ${escapeHtml(formatDateTime(generatedAt))}</span>
590
+ <span class="summary-pill"><strong>Stories</strong> ${storyTitles.length}</span>
591
+ <span class="summary-pill"><strong>Runs</strong> ${runs.length}</span>
592
+ <span class="summary-pill"><strong>Output</strong> ${escapeHtml(outputDir)}</span>
593
+ </div>
594
+ <div class="toolbar">
595
+ <nav class="story-nav">${navLinks || '<span class="summary-pill">No stories found</span>'}</nav>
596
+ <div class="toolbar-actions">
597
+ <button type="button" data-action="expand-all">Expand all</button>
598
+ <button type="button" data-action="collapse-all">Collapse all</button>
599
+ </div>
600
+ </div>
601
+ </section>
602
+ <section class="content">
603
+ ${storySections || '<p class="empty-state">No checkpoint manifests found.</p>'}
604
+ </section>
605
+ </main>
606
+ <script>
607
+ (() => {
608
+ const details = Array.from(document.querySelectorAll('details.accordion'));
609
+ const setAll = (open) => {
610
+ details.forEach((entry) => {
611
+ entry.open = open;
612
+ });
613
+ };
614
+ document.querySelector('[data-action="expand-all"]')?.addEventListener('click', () => setAll(true));
615
+ document.querySelector('[data-action="collapse-all"]')?.addEventListener('click', () => setAll(false));
616
+ const revealHash = () => {
617
+ if (!window.location.hash) {
618
+ return;
619
+ }
620
+ const target = document.querySelector(window.location.hash);
621
+ if (!(target instanceof HTMLElement)) {
622
+ return;
623
+ }
624
+ const parentDetails = target.closest('details');
625
+ if (parentDetails instanceof HTMLDetailsElement) {
626
+ parentDetails.open = true;
627
+ }
628
+ target.scrollIntoView({ block: 'start', behavior: 'smooth' });
629
+ };
630
+ window.addEventListener('hashchange', revealHash);
631
+ revealHash();
632
+ })();
633
+ </script>
634
+ </body>
635
+ </html>
636
+ `;
637
+ }
638
+ var import_promises, import_node_path, DEFAULT_PROJECT_ORDER, htmlReporter;
639
+ var init_html_reporter = __esm({
640
+ "src/report/html-reporter.ts"() {
641
+ "use strict";
642
+ import_promises = __toESM(require("fs/promises"), 1);
643
+ import_node_path = __toESM(require("path"), 1);
644
+ init_story_utils();
645
+ DEFAULT_PROJECT_ORDER = ["desktop-light", "desktop-dark", "mobile-light", "mobile-dark"];
646
+ htmlReporter = {
647
+ name: "html",
648
+ description: "Responsive HTML report for checkpoint manifests.",
649
+ validateConfig(config) {
650
+ return config != null && typeof config === "object" && !Array.isArray(config);
651
+ },
652
+ async generate(context) {
653
+ const outputFile = import_node_path.default.join(context.outputDir, "index.html");
654
+ const html = buildHtmlReport(context.runs, context.outputDir, context.config);
655
+ await import_promises.default.mkdir(context.outputDir, { recursive: true });
656
+ await import_promises.default.writeFile(outputFile, html, "utf8");
657
+ const storyCount = new Set(context.runs.map((run) => run.title)).size;
658
+ return {
659
+ files: [outputFile],
660
+ summary: `Generated HTML report for ${storyCount} stor${storyCount === 1 ? "y" : "ies"} (${context.runs.length} run${context.runs.length === 1 ? "" : "s"}).`
661
+ };
662
+ }
663
+ };
664
+ }
665
+ });
666
+
667
+ // src/report/markdown-reporter.ts
668
+ function slugify2(value) {
669
+ return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "story";
670
+ }
671
+ function stripTags(value) {
672
+ const stripped = value.replace(/\s+@[a-z0-9-]+/gi, " ").replace(/\s+/g, " ").trim();
673
+ return stripped || value.trim() || "Untitled story";
674
+ }
675
+ function normalizeConfig(config) {
676
+ return {
677
+ storiesDir: typeof config.storiesDir === "string" ? config.storiesDir : ".",
678
+ screenshotsDir: typeof config.screenshotsDir === "string" ? config.screenshotsDir : "screenshots",
679
+ includeTags: Array.isArray(config.includeTags) ? config.includeTags.filter((value) => typeof value === "string") : void 0,
680
+ preferredProject: typeof config.preferredProject === "string" ? config.preferredProject : void 0,
681
+ header: typeof config.header === "string" ? config.header : void 0,
682
+ footer: typeof config.footer === "string" ? config.footer : void 0,
683
+ frontmatter: config.frontmatter === true || config.frontmatter === false || config.frontmatter != null && typeof config.frontmatter === "object" && !Array.isArray(config.frontmatter) ? config.frontmatter : false,
684
+ imagePathPrefix: typeof config.imagePathPrefix === "string" ? config.imagePathPrefix : void 0,
685
+ copyScreenshots: typeof config.copyScreenshots === "boolean" ? config.copyScreenshots : true
686
+ };
687
+ }
688
+ function normalizeTags(tags) {
689
+ return (tags ?? []).map((tag) => tag.trim().toLowerCase()).filter(Boolean);
690
+ }
691
+ function shouldIncludeRun(run, config) {
692
+ const includeTags = normalizeTags(config.includeTags);
693
+ if (includeTags.length > 0) {
694
+ const runTags = new Set(normalizeTags(run.tags));
695
+ return includeTags.some((tag) => runTags.has(tag));
696
+ }
697
+ return run.checkpoints.some((checkpoint) => {
698
+ const hasDescription = typeof checkpoint.description === "string" && checkpoint.description.trim().length > 0;
699
+ return hasDescription || typeof checkpoint.step === "number";
700
+ });
701
+ }
702
+ function choosePrimaryRun(runs, preferredProject) {
703
+ if (runs.length === 0) {
704
+ return null;
705
+ }
706
+ return [...runs].sort((left, right) => {
707
+ const leftPreferred = preferredProject && left.project === preferredProject ? 0 : 1;
708
+ const rightPreferred = preferredProject && right.project === preferredProject ? 0 : 1;
709
+ if (leftPreferred !== rightPreferred) {
710
+ return leftPreferred - rightPreferred;
711
+ }
712
+ const rightTime = new Date(right.startedAt).getTime();
713
+ const leftTime = new Date(left.startedAt).getTime();
714
+ if (rightTime !== leftTime) {
715
+ return rightTime - leftTime;
716
+ }
717
+ return left.project.localeCompare(right.project);
718
+ })[0] ?? null;
719
+ }
720
+ function resolveArtifactPath2(run, artifactPath) {
721
+ return import_node_path2.default.isAbsolute(artifactPath) ? artifactPath : import_node_path2.default.resolve(import_node_path2.default.dirname(run.sourceManifestPath), artifactPath);
722
+ }
723
+ function screenshotSourcePath(run, checkpoint) {
724
+ const artifacts = checkpoint.collectors.screenshot?.artifacts ?? [];
725
+ const artifact = artifacts.find((entry) => entry.name === "screenshot") ?? artifacts[0];
726
+ return artifact?.path ? resolveArtifactPath2(run, artifact.path) : null;
727
+ }
728
+ function screenshotData2(checkpoint) {
729
+ const data = checkpoint.collectors.screenshot?.data;
730
+ return data && typeof data === "object" ? data : null;
731
+ }
732
+ function focusNote(checkpoint) {
733
+ const data = screenshotData2(checkpoint);
734
+ const selector = typeof data?.highlightSelector === "string" ? data.highlightSelector.trim() : "";
735
+ if (selector) {
736
+ return `Focus: \`${selector}\``;
737
+ }
738
+ const bounds = data?.highlightBounds;
739
+ if (bounds && typeof bounds.x === "number" && typeof bounds.y === "number" && typeof bounds.width === "number" && typeof bounds.height === "number") {
740
+ return "Focus: highlighted UI element.";
741
+ }
742
+ return null;
743
+ }
744
+ function urlLabel(url) {
745
+ try {
746
+ const parsed = new URL(url);
747
+ const value = `${parsed.pathname}${parsed.search}${parsed.hash}`;
748
+ return value || "/";
749
+ } catch {
750
+ return url || "/";
751
+ }
752
+ }
753
+ function breadcrumbLabel(url) {
754
+ const label = urlLabel(url);
755
+ if (!label.startsWith("/")) {
756
+ return null;
757
+ }
758
+ const [withoutQuery = label] = label.split("?");
759
+ const [withoutHash = withoutQuery] = withoutQuery.split("#");
760
+ const segments = withoutHash.split("/").map((segment) => segment.trim()).filter(Boolean).map((segment) => decodeURIComponent(segment).replace(/[-_]+/g, " "));
761
+ return segments.length > 0 ? segments.join(" \u203A ") : "home";
762
+ }
763
+ function autoDescription(checkpoint) {
764
+ const pageTitle = checkpoint.title.trim();
765
+ const location2 = urlLabel(checkpoint.url);
766
+ if (pageTitle) {
767
+ return `This step captures **${pageTitle}** at \`${location2}\`.`;
768
+ }
769
+ return `This step captures **${checkpoint.name}** at \`${location2}\`.`;
770
+ }
771
+ function markdownRelativePath(fromFile, toFile) {
772
+ const relativePath = import_node_path2.default.relative(import_node_path2.default.dirname(fromFile), toFile).split(import_node_path2.default.sep).join("/");
773
+ if (relativePath.startsWith(".")) {
774
+ return relativePath;
775
+ }
776
+ return `./${relativePath}`;
777
+ }
778
+ function rewriteImagePath(markdownFile, imageFile, outputDir, prefix) {
779
+ const relativePath = import_node_path2.default.relative(outputDir, imageFile).split(import_node_path2.default.sep).join("/");
780
+ if (prefix) {
781
+ return `${prefix.replace(/\/+$/g, "")}/${relativePath.replace(/^\/+/, "")}`;
782
+ }
783
+ return markdownRelativePath(markdownFile, imageFile);
784
+ }
785
+ function yamlScalar(value) {
786
+ return JSON.stringify(value);
787
+ }
788
+ function serializeFrontmatter(fields) {
789
+ const lines = ["---"];
790
+ for (const [key, value] of Object.entries(fields)) {
791
+ if (value === void 0) {
792
+ continue;
793
+ }
794
+ if (Array.isArray(value)) {
795
+ lines.push(`${key}:`);
796
+ if (value.length === 0) {
797
+ lines.push(" []");
798
+ continue;
799
+ }
800
+ for (const item of value) {
801
+ lines.push(` - ${yamlScalar(item)}`);
802
+ }
803
+ continue;
804
+ }
805
+ lines.push(`${key}: ${yamlScalar(value)}`);
806
+ }
807
+ lines.push("---", "");
808
+ return lines.join("\n");
809
+ }
810
+ async function materializeScreenshot(args) {
811
+ const sourcePath = screenshotSourcePath(args.run, args.checkpoint);
812
+ if (!sourcePath) {
813
+ return null;
814
+ }
815
+ const extension = import_node_path2.default.extname(sourcePath) || ".png";
816
+ const targetPath = import_node_path2.default.join(
817
+ args.outputDir,
818
+ args.config.screenshotsDir ?? "screenshots",
819
+ args.storySlug,
820
+ `${String(args.stepOrder).padStart(2, "0")}-${slugify2(args.checkpoint.name)}${extension}`
821
+ );
822
+ try {
823
+ if (args.config.copyScreenshots !== false) {
824
+ await import_promises2.default.mkdir(import_node_path2.default.dirname(targetPath), { recursive: true });
825
+ await import_promises2.default.copyFile(sourcePath, targetPath);
826
+ args.writtenFiles.add(targetPath);
827
+ return rewriteImagePath(args.markdownFile, targetPath, args.outputDir, args.config.imagePathPrefix);
828
+ }
829
+ return rewriteImagePath(args.markdownFile, sourcePath, args.outputDir, args.config.imagePathPrefix);
830
+ } catch {
831
+ return null;
832
+ }
833
+ }
834
+ function orderedCheckpoints(checkpoints) {
835
+ return [...checkpoints].sort((left, right) => {
836
+ const leftOrder = typeof left.step === "number" ? left.step : Number.MAX_SAFE_INTEGER;
837
+ const rightOrder = typeof right.step === "number" ? right.step : Number.MAX_SAFE_INTEGER;
838
+ if (leftOrder !== rightOrder) {
839
+ return leftOrder - rightOrder;
840
+ }
841
+ return checkpoints.indexOf(left) - checkpoints.indexOf(right);
842
+ });
843
+ }
844
+ async function buildSteps(args) {
845
+ const checkpoints = orderedCheckpoints(args.run.checkpoints);
846
+ const steps = [];
847
+ for (const [index, checkpoint] of checkpoints.entries()) {
848
+ const order = typeof checkpoint.step === "number" ? checkpoint.step : index + 1;
849
+ steps.push({
850
+ checkpoint,
851
+ order,
852
+ heading: checkpoint.name,
853
+ description: typeof checkpoint.description === "string" && checkpoint.description.trim().length > 0 ? checkpoint.description.trim() : autoDescription(checkpoint),
854
+ imagePath: await materializeScreenshot({
855
+ run: args.run,
856
+ checkpoint,
857
+ storySlug: args.storySlug,
858
+ stepOrder: order,
859
+ outputDir: args.outputDir,
860
+ markdownFile: args.markdownFile,
861
+ config: args.config,
862
+ writtenFiles: args.writtenFiles
863
+ }),
864
+ urlLabel: urlLabel(checkpoint.url),
865
+ breadcrumbLabel: breadcrumbLabel(checkpoint.url),
866
+ focusNote: focusNote(checkpoint)
867
+ });
868
+ }
869
+ return steps;
870
+ }
871
+ function renderMarkdown(args) {
872
+ const frontmatterFields = args.config.frontmatter === true || typeof args.config.frontmatter === "object" ? {
873
+ title: args.title,
874
+ project: args.run.project,
875
+ testId: args.run.testId,
876
+ tags: args.run.tags,
877
+ startedAt: args.run.startedAt,
878
+ generatedAt: args.generatedAt,
879
+ ...args.config.frontmatter && typeof args.config.frontmatter === "object" ? args.config.frontmatter : {}
880
+ } : null;
881
+ const sections = args.steps.map((step) => {
882
+ const lines = [`## Step ${step.order}: ${step.heading}`, ""];
883
+ if (step.imagePath) {
884
+ lines.push(`![${step.checkpoint.title || step.heading}](${step.imagePath})`, "");
885
+ }
886
+ lines.push(`**URL:** \`${step.urlLabel}\``);
887
+ if (step.breadcrumbLabel) {
888
+ lines.push("", `**Breadcrumb:** ${step.breadcrumbLabel}`);
889
+ }
890
+ if (step.focusNote) {
891
+ lines.push("", `> ${step.focusNote}`);
892
+ }
893
+ lines.push("", step.description);
894
+ return lines.join("\n");
895
+ }).join("\n\n");
896
+ const parts = [
897
+ frontmatterFields ? serializeFrontmatter(frontmatterFields) : "",
898
+ `# ${args.title}`,
899
+ args.config.header ? args.config.header.trim() : "",
900
+ sections,
901
+ args.config.footer ? args.config.footer.trim() : ""
902
+ ].filter((value) => value.trim().length > 0);
903
+ return `${parts.join("\n\n")}
904
+ `;
905
+ }
906
+ var import_promises2, import_node_path2, markdownReporter;
907
+ var init_markdown_reporter = __esm({
908
+ "src/report/markdown-reporter.ts"() {
909
+ "use strict";
910
+ import_promises2 = __toESM(require("fs/promises"), 1);
911
+ import_node_path2 = __toESM(require("path"), 1);
912
+ init_story_utils();
913
+ markdownReporter = {
914
+ name: "markdown",
915
+ description: "Generates one Markdown help article per captured story.",
916
+ validateConfig(config) {
917
+ return config != null && typeof config === "object" && !Array.isArray(config);
918
+ },
919
+ async generate(context) {
920
+ const config = normalizeConfig(context.config);
921
+ const stories = groupByStory(context.runs);
922
+ const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
923
+ const writtenFiles = /* @__PURE__ */ new Set();
924
+ let articleCount = 0;
925
+ for (const [storyTitle, runs] of stories) {
926
+ const primaryRun = choosePrimaryRun(runs, config.preferredProject);
927
+ if (!primaryRun || !shouldIncludeRun(primaryRun, config)) {
928
+ continue;
929
+ }
930
+ const title = stripTags(storyTitle);
931
+ const storySlug = slugify2(title);
932
+ const markdownFile = import_node_path2.default.join(context.outputDir, config.storiesDir ?? ".", `${storySlug}.md`);
933
+ const steps = await buildSteps({
934
+ run: primaryRun,
935
+ storySlug,
936
+ outputDir: context.outputDir,
937
+ markdownFile,
938
+ config,
939
+ writtenFiles
940
+ });
941
+ await import_promises2.default.mkdir(import_node_path2.default.dirname(markdownFile), { recursive: true });
942
+ await import_promises2.default.writeFile(
943
+ markdownFile,
944
+ renderMarkdown({
945
+ title,
946
+ steps,
947
+ run: primaryRun,
948
+ config,
949
+ generatedAt
950
+ }),
951
+ "utf8"
952
+ );
953
+ writtenFiles.add(markdownFile);
954
+ articleCount += 1;
955
+ }
956
+ return {
957
+ files: [...writtenFiles],
958
+ summary: `Generated ${articleCount} Markdown article${articleCount === 1 ? "" : "s"}.`
959
+ };
960
+ }
961
+ };
962
+ }
963
+ });
964
+
965
+ // src/report/mdx-reporter.ts
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 location2 = urlLabel2(checkpoint.url);
1065
+ if (pageTitle) {
1066
+ return `This step captures **${pageTitle}** at \`${location2}\`.`;
1067
+ }
1068
+ return `This step captures **${checkpoint.name}** at \`${location2}\`.`;
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 import_promises3, import_node_path3, DEFAULT_PROJECT_ORDER2, mdxReporter;
1275
+ var init_mdx_reporter = __esm({
1276
+ "src/report/mdx-reporter.ts"() {
1277
+ "use strict";
1278
+ import_promises3 = __toESM(require("fs/promises"), 1);
1279
+ import_node_path3 = __toESM(require("path"), 1);
1280
+ init_story_utils();
1281
+ DEFAULT_PROJECT_ORDER2 = ["desktop-light", "desktop-dark", "mobile-light", "mobile-dark"];
1282
+ mdxReporter = {
1283
+ name: "mdx",
1284
+ description: "Generates one MDX help article per captured story.",
1285
+ validateConfig(config) {
1286
+ return config != null && typeof config === "object" && !Array.isArray(config);
1287
+ },
1288
+ async generate(context) {
1289
+ const config = normalizeConfig2(context.config);
1290
+ const stories = groupByStory(context.runs);
1291
+ const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
1292
+ const writtenFiles = /* @__PURE__ */ new Set();
1293
+ let articleCount = 0;
1294
+ for (const [storyTitle, runs] of stories) {
1295
+ const primaryRun = choosePrimaryRun2(runs, config.preferredProject);
1296
+ if (!primaryRun || !shouldIncludeRun2(primaryRun, config)) {
1297
+ continue;
1298
+ }
1299
+ const title = stripTags2(storyTitle);
1300
+ const storySlug = slugify3(title);
1301
+ const mdxFile = import_node_path3.default.join(context.outputDir, config.storiesDir ?? ".", `${storySlug}.mdx`);
1302
+ const steps = await buildSteps2({
1303
+ runs,
1304
+ primaryRun,
1305
+ storySlug,
1306
+ outputDir: context.outputDir,
1307
+ mdxFile,
1308
+ config,
1309
+ writtenFiles
1310
+ });
1311
+ await import_promises3.default.mkdir(import_node_path3.default.dirname(mdxFile), { recursive: true });
1312
+ await import_promises3.default.writeFile(
1313
+ mdxFile,
1314
+ renderMdx({
1315
+ title,
1316
+ steps,
1317
+ runs,
1318
+ config,
1319
+ generatedAt
1320
+ }),
1321
+ "utf8"
1322
+ );
1323
+ writtenFiles.add(mdxFile);
1324
+ articleCount += 1;
1325
+ }
1326
+ return {
1327
+ files: [...writtenFiles],
1328
+ summary: `Generated ${articleCount} MDX article${articleCount === 1 ? "" : "s"}.`
1329
+ };
1330
+ }
1331
+ };
1332
+ }
1333
+ });
1334
+
1335
+ // src/report/index.ts
1336
+ async function walkFiles(directory) {
1337
+ const dirents = await import_promises4.default.readdir(directory, { withFileTypes: true });
1338
+ const files = [];
1339
+ for (const dirent of dirents) {
1340
+ const absolutePath = import_node_path4.default.join(directory, dirent.name);
1341
+ if (dirent.isDirectory()) {
1342
+ files.push(...await walkFiles(absolutePath));
1343
+ continue;
1344
+ }
1345
+ if (dirent.isFile()) {
1346
+ files.push(absolutePath);
1347
+ }
1348
+ }
1349
+ return files;
1350
+ }
1351
+ function isCheckpointManifestFile(filePath) {
1352
+ const fileName = import_node_path4.default.basename(filePath);
1353
+ return fileName === "checkpoint-manifest.json" || fileName.startsWith("checkpoint-manifest-") && fileName.endsWith(".json");
1354
+ }
1355
+ function isCheckpointManifest(value) {
1356
+ if (!value || typeof value !== "object") {
1357
+ return false;
1358
+ }
1359
+ const manifest = value;
1360
+ 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);
1361
+ }
1362
+ function toRunRecord(manifest, sourceManifestPath) {
1363
+ return {
1364
+ key: `${manifest.testId}|${manifest.project}|${manifest.startedAt}`,
1365
+ sourceManifestPath,
1366
+ environment: manifest.environment || "unknown",
1367
+ project: manifest.project,
1368
+ testId: manifest.testId,
1369
+ title: manifest.title,
1370
+ tags: manifest.tags,
1371
+ startedAt: manifest.startedAt,
1372
+ checkpoints: manifest.checkpoints
1373
+ };
1374
+ }
1375
+ function toManifest(run) {
1376
+ return {
1377
+ environment: run.environment,
1378
+ project: run.project,
1379
+ testId: run.testId,
1380
+ title: run.title,
1381
+ tags: run.tags,
1382
+ startedAt: run.startedAt,
1383
+ checkpoints: run.checkpoints
1384
+ };
1385
+ }
1386
+ function normalizeReporterConfig(config) {
1387
+ if (config == null || config === false) {
1388
+ return null;
1389
+ }
1390
+ if (config === true) {
1391
+ return {};
1392
+ }
1393
+ return { ...config };
1394
+ }
1395
+ function registerBuiltinReporter(reporter) {
1396
+ builtinReporters.set(reporter.name, reporter);
1397
+ }
1398
+ function dedupeRuns(runs) {
1399
+ const map = /* @__PURE__ */ new Map();
1400
+ for (const run of runs) {
1401
+ const existing = map.get(run.key);
1402
+ if (!existing) {
1403
+ map.set(run.key, run);
1404
+ continue;
1405
+ }
1406
+ const existingTime = new Date(existing.startedAt).getTime();
1407
+ const currentTime = new Date(run.startedAt).getTime();
1408
+ if (currentTime >= existingTime) {
1409
+ map.set(run.key, run);
1410
+ }
1411
+ }
1412
+ return [...map.values()];
1413
+ }
1414
+ async function loadRuns(testResultsDir) {
1415
+ let manifestFiles;
1416
+ try {
1417
+ manifestFiles = (await walkFiles(testResultsDir)).filter(isCheckpointManifestFile);
1418
+ } catch {
1419
+ return [];
1420
+ }
1421
+ const runs = [];
1422
+ for (const manifestPath of manifestFiles) {
1423
+ let rawManifest;
1424
+ try {
1425
+ rawManifest = JSON.parse(await import_promises4.default.readFile(manifestPath, "utf8"));
1426
+ } catch {
1427
+ continue;
1428
+ }
1429
+ if (!isCheckpointManifest(rawManifest)) {
1430
+ continue;
1431
+ }
1432
+ runs.push(toRunRecord(rawManifest, manifestPath));
1433
+ }
1434
+ return dedupeRuns(runs);
1435
+ }
1436
+ async function runReporters(config, testResultsDir, outputDir) {
1437
+ const runs = await loadRuns(testResultsDir);
1438
+ const manifests = runs.map(toManifest);
1439
+ const results = {};
1440
+ const reporterConfigMap = {
1441
+ ...builtinReporterDefaults,
1442
+ ...config.reporters ?? {}
1443
+ };
1444
+ for (const [name, value] of Object.entries(reporterConfigMap)) {
1445
+ const reporterConfig2 = normalizeReporterConfig(value);
1446
+ if (!reporterConfig2) {
1447
+ continue;
1448
+ }
1449
+ const reporter = builtinReporters.get(name);
1450
+ if (!reporter) {
1451
+ throw new Error(`Reporter "${name}" is enabled but no implementation is registered.`);
1452
+ }
1453
+ if (reporter.validateConfig && !reporter.validateConfig(reporterConfig2)) {
1454
+ throw new Error(`Reporter "${name}" received invalid configuration.`);
1455
+ }
1456
+ results[name] = await reporter.generate({
1457
+ runs,
1458
+ outputDir,
1459
+ config: reporterConfig2,
1460
+ manifests
1461
+ });
1462
+ }
1463
+ return results;
1464
+ }
1465
+ var import_promises4, import_node_path4, builtinReporters, builtinReporterDefaults;
1466
+ var init_report = __esm({
1467
+ "src/report/index.ts"() {
1468
+ "use strict";
1469
+ import_promises4 = __toESM(require("fs/promises"), 1);
1470
+ import_node_path4 = __toESM(require("path"), 1);
1471
+ init_html_reporter();
1472
+ init_markdown_reporter();
1473
+ init_mdx_reporter();
1474
+ builtinReporters = /* @__PURE__ */ new Map();
1475
+ builtinReporterDefaults = {
1476
+ html: true,
1477
+ markdown: false,
1478
+ mdx: false
1479
+ };
1480
+ registerBuiltinReporter(htmlReporter);
1481
+ registerBuiltinReporter(markdownReporter);
1482
+ registerBuiltinReporter(mdxReporter);
1483
+ }
1484
+ });
1485
+
1486
+ // src/mcp/transport.ts
1487
+ function serializeMessage(message) {
1488
+ return JSON.stringify(message) + "\n";
1489
+ }
1490
+ var ReadBuffer, StdioServerTransport;
1491
+ var init_transport = __esm({
1492
+ "src/mcp/transport.ts"() {
1493
+ "use strict";
1494
+ ReadBuffer = class {
1495
+ _buffer;
1496
+ append(chunk) {
1497
+ this._buffer = this._buffer ? Buffer.concat([this._buffer, chunk]) : chunk;
1498
+ }
1499
+ readMessage() {
1500
+ if (!this._buffer) return null;
1501
+ const index = this._buffer.indexOf("\n");
1502
+ if (index === -1) return null;
1503
+ const line = this._buffer.toString("utf8", 0, index).replace(/\r$/, "");
1504
+ this._buffer = this._buffer.subarray(index + 1);
1505
+ try {
1506
+ return JSON.parse(line);
1507
+ } catch {
1508
+ return null;
1509
+ }
1510
+ }
1511
+ clear() {
1512
+ this._buffer = void 0;
1513
+ }
1514
+ };
1515
+ StdioServerTransport = class {
1516
+ _stdin;
1517
+ _stdout;
1518
+ _readBuffer = new ReadBuffer();
1519
+ _started = false;
1520
+ constructor(stdin, stdout) {
1521
+ this._stdin = stdin ?? process.stdin;
1522
+ this._stdout = stdout ?? process.stdout;
1523
+ }
1524
+ onclose;
1525
+ onerror;
1526
+ onmessage;
1527
+ async start() {
1528
+ if (this._started) {
1529
+ throw new Error("StdioServerTransport already started!");
1530
+ }
1531
+ this._started = true;
1532
+ this._stdin.on("data", (chunk) => {
1533
+ this._readBuffer.append(chunk);
1534
+ this.#processReadBuffer();
1535
+ });
1536
+ this._stdin.on("error", (error) => {
1537
+ this.onerror?.(error);
1538
+ });
1539
+ }
1540
+ #processReadBuffer() {
1541
+ while (true) {
1542
+ try {
1543
+ const message = this._readBuffer.readMessage();
1544
+ if (message === null) break;
1545
+ this.onmessage?.(message);
1546
+ } catch (error) {
1547
+ this.onerror?.(error instanceof Error ? error : new Error(String(error)));
1548
+ }
1549
+ }
1550
+ }
1551
+ async close() {
1552
+ this._stdin.pause?.();
1553
+ this._readBuffer.clear();
1554
+ this.onclose?.();
1555
+ }
1556
+ async send(message) {
1557
+ const json = serializeMessage(message);
1558
+ if (this._stdout.write(json)) {
1559
+ return Promise.resolve();
1560
+ }
1561
+ return new Promise((resolve) => {
1562
+ this._stdout.once("drain", resolve);
1563
+ });
1564
+ }
1565
+ };
1566
+ }
1567
+ });
1568
+
1569
+ // src/mcp/upstream.ts
1570
+ function getNodeModuleCreateRequire() {
1571
+ const { createRequire } = require("module");
1572
+ return createRequire(process.cwd() + "/noop.js");
1573
+ }
1574
+ function resolvePackageJson(pkg) {
1575
+ try {
1576
+ const req = getNodeModuleCreateRequire();
1577
+ return req.resolve(`${pkg}/package.json`);
1578
+ } catch {
1579
+ return null;
1580
+ }
1581
+ }
1582
+ function resolveUpstream(options) {
1583
+ if (options.upstream) {
1584
+ return options.upstream;
1585
+ }
1586
+ if (resolvePackageJson("@playwright/mcp")) {
1587
+ return "@playwright/mcp";
1588
+ }
1589
+ if (resolvePackageJson("playwright-mcp-advanced")) {
1590
+ return "playwright-mcp-advanced";
1591
+ }
1592
+ return null;
1593
+ }
1594
+ function injectDebugPort(args) {
1595
+ const hasDebugFlag = args.some(
1596
+ (a) => a.startsWith("--remote-debugging-port") || a.startsWith("--cdp-endpoint")
1597
+ );
1598
+ if (hasDebugFlag) {
1599
+ return args;
1600
+ }
1601
+ return ["--remote-debugging-port=9222", ...args];
1602
+ }
1603
+ function spawnUpstream(pkg, passthroughArgs) {
1604
+ const npxBin = process.platform === "win32" ? "npx.cmd" : "npx";
1605
+ const npxArgs = [pkg, ...passthroughArgs];
1606
+ const child = (0, import_node_child_process.spawn)(npxBin, npxArgs, {
1607
+ stdio: ["pipe", "pipe", "pipe"],
1608
+ env: {
1609
+ ...process.env,
1610
+ // Prevent npx from prompting or caching in CI
1611
+ NPM_CONFIG_YES: "true",
1612
+ NPM_CONFIG_INTERACTIVE: "false"
1613
+ },
1614
+ shell: false,
1615
+ windowsHide: true
1616
+ });
1617
+ const transport = new ChildProcessTransport(
1618
+ child.stdin,
1619
+ child.stdout,
1620
+ child.stderr,
1621
+ (_msg) => {
1622
+ },
1623
+ () => {
1624
+ }
1625
+ );
1626
+ transport.start();
1627
+ const connection = {
1628
+ process: child,
1629
+ async listTools() {
1630
+ const result = await transport.request("tools/list");
1631
+ return result;
1632
+ },
1633
+ async callTool(name, args) {
1634
+ const result = await transport.request("tools/call", { name, arguments: args ?? {} });
1635
+ return result;
1636
+ },
1637
+ close() {
1638
+ transport.close();
1639
+ }
1640
+ };
1641
+ return connection;
1642
+ }
1643
+ var import_node_child_process, ChildProcessTransport;
1644
+ var init_upstream = __esm({
1645
+ "src/mcp/upstream.ts"() {
1646
+ "use strict";
1647
+ import_node_child_process = require("child_process");
1648
+ ChildProcessTransport = class {
1649
+ constructor(stdin, stdout, stderr, onMessage, onClose) {
1650
+ this.stdin = stdin;
1651
+ this.stdout = stdout;
1652
+ this.stderr = stderr;
1653
+ this.onMessage = onMessage;
1654
+ this.onClose = onClose;
1655
+ }
1656
+ stdin;
1657
+ stdout;
1658
+ stderr;
1659
+ onMessage;
1660
+ onClose;
1661
+ pendingRequests = /* @__PURE__ */ new Map();
1662
+ id = 0;
1663
+ start() {
1664
+ let buffer = "";
1665
+ this.stdout.on("data", (chunk) => {
1666
+ buffer += chunk.toString();
1667
+ const lines = buffer.split("\n");
1668
+ buffer = lines.pop() ?? "";
1669
+ for (const line of lines) {
1670
+ if (line.trim()) {
1671
+ try {
1672
+ const msg = JSON.parse(line);
1673
+ this.handleMessage(msg);
1674
+ } catch {
1675
+ }
1676
+ }
1677
+ }
1678
+ });
1679
+ this.stdout.on("end", () => {
1680
+ this.onClose?.();
1681
+ });
1682
+ this.stderr?.on("data", (chunk) => {
1683
+ process.stderr.write(chunk);
1684
+ });
1685
+ }
1686
+ handleMessage(msg) {
1687
+ if (msg.id !== void 0) {
1688
+ const pending = this.pendingRequests.get(String(msg.id));
1689
+ if (pending) {
1690
+ this.pendingRequests.delete(String(msg.id));
1691
+ if (msg.error) {
1692
+ pending.reject(new Error(String(msg.error)));
1693
+ } else {
1694
+ pending.resolve(msg.result);
1695
+ }
1696
+ }
1697
+ return;
1698
+ }
1699
+ if (msg.method) {
1700
+ this.onMessage(msg);
1701
+ }
1702
+ }
1703
+ send(message) {
1704
+ const json = JSON.stringify(message) + "\n";
1705
+ this.stdin.write(json);
1706
+ }
1707
+ request(method, params) {
1708
+ const id = String(++this.id);
1709
+ const promise = new Promise((resolve, reject) => {
1710
+ this.pendingRequests.set(id, { resolve, reject });
1711
+ });
1712
+ this.send({ method, params, id });
1713
+ return promise;
1714
+ }
1715
+ close() {
1716
+ this.stdin.end();
1717
+ }
1718
+ };
1719
+ }
1720
+ });
1721
+
1722
+ // src/mcp/browser-connect.ts
1723
+ function extractCdpUrlFromStderr(data) {
1724
+ const match = CDP_WS_REGEX.exec(data);
1725
+ return match ? match[1] ?? null : null;
1726
+ }
1727
+ async function getUpstreamPage(upstreamProcess, options) {
1728
+ if (cachedConnection) {
1729
+ return cachedConnection;
1730
+ }
1731
+ let cdpEndpoint = options.cdpEndpoint;
1732
+ if (!cdpEndpoint) {
1733
+ if (upstreamProcess?.stderr) {
1734
+ const stderrLines = [];
1735
+ upstreamProcess.stderr.on("data", (chunk) => {
1736
+ stderrLines.push(chunk.toString());
1737
+ });
1738
+ await new Promise((resolve) => setTimeout(resolve, 500));
1739
+ for (const line of stderrLines) {
1740
+ const url = extractCdpUrlFromStderr(line);
1741
+ if (url) {
1742
+ cdpEndpoint = url;
1743
+ break;
1744
+ }
1745
+ }
1746
+ }
1747
+ }
1748
+ if (!cdpEndpoint) {
1749
+ const port = options.debugPort ?? 9222;
1750
+ cdpEndpoint = `http://localhost:${port}`;
1751
+ }
1752
+ const browser = await chromiumConnect(cdpEndpoint);
1753
+ const pages = await browser.contexts()[0]?.pages() ?? [];
1754
+ const page = pages[0] ?? await browser.newPage();
1755
+ cachedConnection = { browser, page };
1756
+ return cachedConnection;
1757
+ }
1758
+ function getNodeModuleCreateRequire2() {
1759
+ const { createRequire } = require("module");
1760
+ return createRequire(import_meta.url);
1761
+ }
1762
+ async function chromiumConnect(endpoint) {
1763
+ const req = getNodeModuleCreateRequire2();
1764
+ const pw = req("playwright-core");
1765
+ return pw.chromium.connectOverCDP(endpoint);
1766
+ }
1767
+ function resetCachedConnection() {
1768
+ cachedConnection = null;
1769
+ }
1770
+ var import_meta, CDP_WS_REGEX, cachedConnection;
1771
+ var init_browser_connect = __esm({
1772
+ "src/mcp/browser-connect.ts"() {
1773
+ "use strict";
1774
+ import_meta = {};
1775
+ CDP_WS_REGEX = /DevTools listening on (ws:\/\/[^\s]+)/;
1776
+ cachedConnection = null;
1777
+ }
1778
+ });
1779
+
1780
+ // src/collectors/aria-snapshot.ts
1781
+ function countSnapshotNodes(value) {
1782
+ if (value == null) {
1783
+ return 0;
1784
+ }
1785
+ if (Array.isArray(value)) {
1786
+ return value.reduce((total, item) => total + countSnapshotNodes(item), 0);
1787
+ }
1788
+ if (typeof value !== "object") {
1789
+ return 1;
1790
+ }
1791
+ const node = value;
1792
+ const children = Array.isArray(node.children) ? node.children : [];
1793
+ return 1 + children.reduce((total, child) => total + countSnapshotNodes(child), 0);
1794
+ }
1795
+ async function captureAriaSnapshot(page) {
1796
+ try {
1797
+ const root = page.locator(":root");
1798
+ if (typeof root.ariaSnapshot === "function") {
1799
+ const snapshot = await root.ariaSnapshot();
1800
+ return snapshot ?? null;
1801
+ }
1802
+ } catch {
1803
+ }
1804
+ if (typeof page.accessibility?.snapshot === "function") {
1805
+ try {
1806
+ const snapshot = await page.accessibility.snapshot({ interestingOnly: false });
1807
+ return snapshot ?? null;
1808
+ } catch {
1809
+ return null;
1810
+ }
1811
+ }
1812
+ return null;
1813
+ }
1814
+ var import_promises5, import_node_path6, ariaSnapshotCollector;
1815
+ var init_aria_snapshot = __esm({
1816
+ "src/collectors/aria-snapshot.ts"() {
1817
+ "use strict";
1818
+ import_promises5 = __toESM(require("fs/promises"), 1);
1819
+ import_node_path6 = __toESM(require("path"), 1);
1820
+ ariaSnapshotCollector = {
1821
+ name: "aria-snapshot",
1822
+ defaultEnabled: false,
1823
+ async collect(ctx) {
1824
+ const snapshot = await captureAriaSnapshot(ctx.page);
1825
+ const nodeCount = countSnapshotNodes(snapshot);
1826
+ const outputPath = import_node_path6.default.join(ctx.checkpointDir, "aria-snapshot.json");
1827
+ const data = {
1828
+ snapshot,
1829
+ nodeCount
1830
+ };
1831
+ await import_promises5.default.writeFile(outputPath, `${JSON.stringify(data, null, 2)}
1832
+ `, "utf8");
1833
+ return {
1834
+ data,
1835
+ artifacts: [
1836
+ {
1837
+ name: "aria-snapshot",
1838
+ path: outputPath,
1839
+ contentType: "application/json"
1840
+ }
1841
+ ],
1842
+ summary: {
1843
+ nodeCount
1844
+ }
1845
+ };
1846
+ }
1847
+ };
1848
+ }
1849
+ });
1850
+
1851
+ // src/page-utils.ts
1852
+ async function settlePage(page) {
1853
+ await page.waitForLoadState("domcontentloaded").catch(() => void 0);
1854
+ await page.waitForLoadState("load", { timeout: 3e3 }).catch(() => void 0);
1855
+ }
1856
+ var init_page_utils = __esm({
1857
+ "src/page-utils.ts"() {
1858
+ "use strict";
1859
+ }
1860
+ });
1861
+
1862
+ // src/collectors/axe.ts
1863
+ function warnOnce(message, error) {
1864
+ if (warnedAboutMissingAxe) {
1865
+ return;
1866
+ }
1867
+ warnedAboutMissingAxe = true;
1868
+ if (error instanceof Error) {
1869
+ console.warn(`[playwright-checkpoint] ${message}`, error);
1870
+ return;
1871
+ }
1872
+ if (error !== void 0) {
1873
+ console.warn(`[playwright-checkpoint] ${message}`, String(error));
1874
+ return;
1875
+ }
1876
+ console.warn(`[playwright-checkpoint] ${message}`);
1877
+ }
1878
+ function resolveAxeBuilder(module2) {
1879
+ return module2.default ?? module2.AxeBuilder ?? null;
1880
+ }
1881
+ async function analyzeAccessibility(page, AxeBuilder) {
1882
+ try {
1883
+ await settlePage(page);
1884
+ return await new AxeBuilder({ page }).analyze();
1885
+ } catch {
1886
+ await page.waitForTimeout(500);
1887
+ await settlePage(page);
1888
+ return await new AxeBuilder({ page }).analyze();
1889
+ }
1890
+ }
1891
+ function skippedAxeResult(reason) {
1892
+ return {
1893
+ data: {
1894
+ skipped: true,
1895
+ reason,
1896
+ violations: 0,
1897
+ results: null
1898
+ },
1899
+ artifacts: [],
1900
+ summary: {
1901
+ violations: 0
1902
+ }
1903
+ };
1904
+ }
1905
+ var import_promises6, import_node_path7, axeLoader, warnedAboutMissingAxe, axeCollector;
1906
+ var init_axe = __esm({
1907
+ "src/collectors/axe.ts"() {
1908
+ "use strict";
1909
+ import_promises6 = __toESM(require("fs/promises"), 1);
1910
+ import_node_path7 = __toESM(require("path"), 1);
1911
+ init_page_utils();
1912
+ axeLoader = () => import("@axe-core/playwright");
1913
+ warnedAboutMissingAxe = false;
1914
+ axeCollector = {
1915
+ name: "axe",
1916
+ defaultEnabled: true,
1917
+ async collect(ctx) {
1918
+ const timeoutBudgetMs = typeof ctx.config.timeoutMs === "number" ? ctx.config.timeoutMs : 5e3;
1919
+ if (timeoutBudgetMs > 0) {
1920
+ if (typeof ctx.adjustTimeout === "function") {
1921
+ ctx.adjustTimeout(timeoutBudgetMs);
1922
+ } else if (ctx.testInfo && typeof ctx.testInfo.setTimeout === "function") {
1923
+ ctx.testInfo.setTimeout(ctx.testInfo.timeout + timeoutBudgetMs);
1924
+ }
1925
+ }
1926
+ let module2;
1927
+ try {
1928
+ module2 = await axeLoader();
1929
+ } catch (error) {
1930
+ warnOnce("Skipping axe collector because @axe-core/playwright is unavailable.", error);
1931
+ return skippedAxeResult("@axe-core/playwright is unavailable");
1932
+ }
1933
+ const AxeBuilder = resolveAxeBuilder(module2);
1934
+ if (!AxeBuilder) {
1935
+ warnOnce("Skipping axe collector because @axe-core/playwright did not expose an AxeBuilder export.");
1936
+ return skippedAxeResult("@axe-core/playwright did not expose AxeBuilder");
1937
+ }
1938
+ const results = await analyzeAccessibility(ctx.page, AxeBuilder);
1939
+ const violations = results && typeof results === "object" && Array.isArray(results.violations) ? results.violations.length : 0;
1940
+ const axePath = import_node_path7.default.join(ctx.checkpointDir, "axe.json");
1941
+ await import_promises6.default.writeFile(axePath, `${JSON.stringify(results, null, 2)}
1942
+ `, "utf8");
1943
+ return {
1944
+ data: {
1945
+ skipped: false,
1946
+ reason: null,
1947
+ violations,
1948
+ results
1949
+ },
1950
+ artifacts: [
1951
+ {
1952
+ name: "axe",
1953
+ path: axePath,
1954
+ contentType: "application/json"
1955
+ }
1956
+ ],
1957
+ summary: {
1958
+ violations
1959
+ }
1960
+ };
1961
+ }
1962
+ };
1963
+ }
1964
+ });
1965
+
1966
+ // src/collectors/console.ts
1967
+ function getLocation(message) {
1968
+ const location2 = message.location();
1969
+ if (!location2.url && location2.lineNumber == null && location2.columnNumber == null) {
1970
+ return null;
1971
+ }
1972
+ return {
1973
+ ...location2.url ? { url: location2.url } : {},
1974
+ ...location2.lineNumber == null ? {} : { lineNumber: location2.lineNumber },
1975
+ ...location2.columnNumber == null ? {} : { columnNumber: location2.columnNumber }
1976
+ };
1977
+ }
1978
+ var import_promises7, import_node_path8, consoleStates, consoleCollector;
1979
+ var init_console = __esm({
1980
+ "src/collectors/console.ts"() {
1981
+ "use strict";
1982
+ import_promises7 = __toESM(require("fs/promises"), 1);
1983
+ import_node_path8 = __toESM(require("path"), 1);
1984
+ consoleStates = /* @__PURE__ */ new WeakMap();
1985
+ consoleCollector = {
1986
+ name: "console",
1987
+ defaultEnabled: true,
1988
+ async setup({ page }) {
1989
+ if (consoleStates.has(page)) {
1990
+ return;
1991
+ }
1992
+ const entries = [];
1993
+ const recordConsoleMessage = (message) => {
1994
+ if (message.type() !== "error") {
1995
+ return;
1996
+ }
1997
+ entries.push({
1998
+ type: message.type(),
1999
+ text: message.text(),
2000
+ location: getLocation(message),
2001
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2002
+ });
2003
+ };
2004
+ const recordPageError = (error) => {
2005
+ entries.push({
2006
+ type: "pageerror",
2007
+ text: error.message,
2008
+ location: null,
2009
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2010
+ });
2011
+ };
2012
+ page.on("console", recordConsoleMessage);
2013
+ page.on("pageerror", recordPageError);
2014
+ consoleStates.set(page, {
2015
+ entries,
2016
+ offset: 0,
2017
+ recordConsoleMessage,
2018
+ recordPageError
2019
+ });
2020
+ },
2021
+ async collect(ctx) {
2022
+ const state = consoleStates.get(ctx.page);
2023
+ const checkpointEntries = state ? state.entries.slice(state.offset) : [];
2024
+ if (state) {
2025
+ state.offset = state.entries.length;
2026
+ }
2027
+ const outputPath = import_node_path8.default.join(ctx.checkpointDir, "console-errors.json");
2028
+ await import_promises7.default.writeFile(outputPath, `${JSON.stringify(checkpointEntries, null, 2)}
2029
+ `, "utf8");
2030
+ return {
2031
+ data: checkpointEntries,
2032
+ artifacts: [
2033
+ {
2034
+ name: "console-errors",
2035
+ path: outputPath,
2036
+ contentType: "application/json"
2037
+ }
2038
+ ],
2039
+ summary: {
2040
+ consoleErrorCount: checkpointEntries.length
2041
+ }
2042
+ };
2043
+ },
2044
+ async teardown({ page }) {
2045
+ const state = consoleStates.get(page);
2046
+ if (!state) {
2047
+ return;
2048
+ }
2049
+ page.off("console", state.recordConsoleMessage);
2050
+ page.off("pageerror", state.recordPageError);
2051
+ consoleStates.delete(page);
2052
+ }
2053
+ };
2054
+ }
2055
+ });
2056
+
2057
+ // src/collectors/dom-stats.ts
2058
+ var import_promises8, import_node_path9, domStatsCollector;
2059
+ var init_dom_stats = __esm({
2060
+ "src/collectors/dom-stats.ts"() {
2061
+ "use strict";
2062
+ import_promises8 = __toESM(require("fs/promises"), 1);
2063
+ import_node_path9 = __toESM(require("path"), 1);
2064
+ domStatsCollector = {
2065
+ name: "dom-stats",
2066
+ defaultEnabled: false,
2067
+ async collect(ctx) {
2068
+ const stats = await ctx.page.evaluate(() => {
2069
+ const allNodes = document.querySelectorAll("*");
2070
+ const maxDepthFrom = (root) => {
2071
+ if (!root) {
2072
+ return 0;
2073
+ }
2074
+ let maxDepth = 1;
2075
+ const queue = [{ node: root, depth: 1 }];
2076
+ while (queue.length > 0) {
2077
+ const current = queue.shift();
2078
+ if (!current) {
2079
+ continue;
2080
+ }
2081
+ maxDepth = Math.max(maxDepth, current.depth);
2082
+ for (const child of Array.from(current.node.children)) {
2083
+ queue.push({ node: child, depth: current.depth + 1 });
2084
+ }
2085
+ }
2086
+ return maxDepth;
2087
+ };
2088
+ const maybeGetEventListeners = globalThis.getEventListeners;
2089
+ let eventListenerCount = null;
2090
+ if (typeof maybeGetEventListeners === "function") {
2091
+ eventListenerCount = 0;
2092
+ const targets = [window, document, ...Array.from(allNodes)];
2093
+ for (const target of targets) {
2094
+ try {
2095
+ const listeners = maybeGetEventListeners(target) ?? {};
2096
+ for (const entries of Object.values(listeners)) {
2097
+ eventListenerCount += Array.isArray(entries) ? entries.length : 0;
2098
+ }
2099
+ } catch {
2100
+ }
2101
+ }
2102
+ }
2103
+ return {
2104
+ nodeCount: allNodes.length,
2105
+ maxDepth: maxDepthFrom(document.documentElement),
2106
+ formCount: document.querySelectorAll("form").length,
2107
+ imageCount: document.querySelectorAll("img").length,
2108
+ scriptCount: document.querySelectorAll("script").length,
2109
+ stylesheetCount: document.styleSheets.length,
2110
+ eventListenerCount
2111
+ };
2112
+ });
2113
+ const data = {
2114
+ nodeCount: stats.nodeCount,
2115
+ maxDepth: stats.maxDepth,
2116
+ formCount: stats.formCount,
2117
+ imageCount: stats.imageCount,
2118
+ scriptCount: stats.scriptCount,
2119
+ stylesheetCount: stats.stylesheetCount,
2120
+ eventListenerCount: stats.eventListenerCount
2121
+ };
2122
+ const outputPath = import_node_path9.default.join(ctx.checkpointDir, "dom-stats.json");
2123
+ await import_promises8.default.writeFile(outputPath, `${JSON.stringify(data, null, 2)}
2124
+ `, "utf8");
2125
+ return {
2126
+ data,
2127
+ artifacts: [
2128
+ {
2129
+ name: "dom-stats",
2130
+ path: outputPath,
2131
+ contentType: "application/json"
2132
+ }
2133
+ ],
2134
+ summary: {
2135
+ nodeCount: data.nodeCount,
2136
+ maxDepth: data.maxDepth,
2137
+ formCount: data.formCount,
2138
+ imageCount: data.imageCount
2139
+ }
2140
+ };
2141
+ }
2142
+ };
2143
+ }
2144
+ });
2145
+
2146
+ // src/collectors/forms.ts
2147
+ function toRegex(pattern) {
2148
+ const trimmed = pattern.trim();
2149
+ if (!trimmed) {
2150
+ return null;
2151
+ }
2152
+ try {
2153
+ return new RegExp(trimmed, "i");
2154
+ } catch {
2155
+ return null;
2156
+ }
2157
+ }
2158
+ function redactionRegexes(ctx) {
2159
+ const fromConfig = Array.isArray(ctx.config.redact) ? ctx.config.redact.filter((entry) => typeof entry === "string") : [];
2160
+ return [...DEFAULT_REDACT_PATTERNS, ...ctx.redact, ...fromConfig].map((pattern) => toRegex(pattern)).filter((value) => value instanceof RegExp);
2161
+ }
2162
+ function shouldRedactText(value, regexes) {
2163
+ if (EMAIL_LIKE_REGEX.test(value.trim())) {
2164
+ return true;
2165
+ }
2166
+ return regexes.some((regex) => regex.test(value));
2167
+ }
2168
+ function fieldIdentifier(field) {
2169
+ return [field.type, field.name, field.id, field.label, field.placeholder].filter((value) => !!value).join(" ");
2170
+ }
2171
+ function redactValue(value) {
2172
+ if (value == null) {
2173
+ return value;
2174
+ }
2175
+ if (Array.isArray(value)) {
2176
+ return value.map(() => REDACTED);
2177
+ }
2178
+ return REDACTED;
2179
+ }
2180
+ function fieldNeedsRedaction(field, regexes) {
2181
+ if (shouldRedactText(fieldIdentifier(field), regexes)) {
2182
+ return true;
2183
+ }
2184
+ if (field.value == null) {
2185
+ return false;
2186
+ }
2187
+ if (Array.isArray(field.value)) {
2188
+ return field.value.some((entry) => shouldRedactText(entry, regexes));
2189
+ }
2190
+ return shouldRedactText(field.value, regexes);
2191
+ }
2192
+ var import_promises9, import_node_path10, REDACTED, DEFAULT_REDACT_PATTERNS, EMAIL_LIKE_REGEX, formsCollector;
2193
+ var init_forms = __esm({
2194
+ "src/collectors/forms.ts"() {
2195
+ "use strict";
2196
+ import_promises9 = __toESM(require("fs/promises"), 1);
2197
+ import_node_path10 = __toESM(require("path"), 1);
2198
+ REDACTED = "[REDACTED]";
2199
+ DEFAULT_REDACT_PATTERNS = ["password", "token", "secret", "api[_-]?key", "authorization", "bearer"];
2200
+ EMAIL_LIKE_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
2201
+ formsCollector = {
2202
+ name: "forms",
2203
+ defaultEnabled: false,
2204
+ async collect(ctx) {
2205
+ const rawFields = await ctx.page.evaluate(() => {
2206
+ const elements = Array.from(document.querySelectorAll("input, select, textarea"));
2207
+ const isVisible = (element) => {
2208
+ if (!(element instanceof HTMLElement)) {
2209
+ return false;
2210
+ }
2211
+ const inputType = element instanceof HTMLInputElement ? element.type.toLowerCase() : null;
2212
+ if (inputType === "hidden") {
2213
+ return false;
2214
+ }
2215
+ if (element.hasAttribute("hidden") || element.getAttribute("aria-hidden") === "true") {
2216
+ return false;
2217
+ }
2218
+ const style = window.getComputedStyle(element);
2219
+ if (style.display === "none" || style.visibility === "hidden" || Number(style.opacity) === 0) {
2220
+ return false;
2221
+ }
2222
+ const rect = element.getBoundingClientRect();
2223
+ return rect.width > 0 && rect.height > 0;
2224
+ };
2225
+ const readLabel = (element) => {
2226
+ if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement) {
2227
+ const fromLabels = element.labels && element.labels.length > 0 ? element.labels[0]?.textContent?.trim() : null;
2228
+ if (fromLabels) {
2229
+ return fromLabels;
2230
+ }
2231
+ }
2232
+ return element.getAttribute("aria-label")?.trim() ?? null;
2233
+ };
2234
+ const readValue = (element) => {
2235
+ if (element instanceof HTMLSelectElement) {
2236
+ if (element.multiple) {
2237
+ return {
2238
+ value: Array.from(element.selectedOptions).map((option) => option.value),
2239
+ checked: null,
2240
+ type: "select-multiple"
2241
+ };
2242
+ }
2243
+ return {
2244
+ value: element.value,
2245
+ checked: null,
2246
+ type: "select-one"
2247
+ };
2248
+ }
2249
+ if (element instanceof HTMLTextAreaElement) {
2250
+ return {
2251
+ value: element.value,
2252
+ checked: null,
2253
+ type: "textarea"
2254
+ };
2255
+ }
2256
+ if (element instanceof HTMLInputElement) {
2257
+ const inputType = element.type.toLowerCase();
2258
+ if (inputType === "checkbox" || inputType === "radio") {
2259
+ return {
2260
+ value: element.checked ? element.value || "on" : null,
2261
+ checked: element.checked,
2262
+ type: inputType
2263
+ };
2264
+ }
2265
+ if (inputType === "file") {
2266
+ return {
2267
+ value: element.files ? Array.from(element.files).map((file) => file.name) : [],
2268
+ checked: null,
2269
+ type: inputType
2270
+ };
2271
+ }
2272
+ return {
2273
+ value: element.value,
2274
+ checked: null,
2275
+ type: inputType || null
2276
+ };
2277
+ }
2278
+ return {
2279
+ value: null,
2280
+ checked: null,
2281
+ type: null
2282
+ };
2283
+ };
2284
+ return elements.filter((element) => isVisible(element)).map((element) => {
2285
+ const { value, checked, type } = readValue(element);
2286
+ return {
2287
+ tagName: element.tagName.toLowerCase(),
2288
+ type,
2289
+ name: element.getAttribute("name"),
2290
+ id: element.getAttribute("id"),
2291
+ label: readLabel(element),
2292
+ placeholder: element.getAttribute("placeholder"),
2293
+ value,
2294
+ checked,
2295
+ disabled: element.disabled,
2296
+ required: element.required
2297
+ };
2298
+ });
2299
+ });
2300
+ const regexes = redactionRegexes({ redact: ctx.redact, config: ctx.config });
2301
+ let redactedCount = 0;
2302
+ const fields = rawFields.map((field) => {
2303
+ const redacted = fieldNeedsRedaction(field, regexes);
2304
+ if (redacted) {
2305
+ redactedCount += 1;
2306
+ }
2307
+ return {
2308
+ ...field,
2309
+ redacted,
2310
+ value: redacted ? redactValue(field.value) : field.value
2311
+ };
2312
+ });
2313
+ const data = {
2314
+ fieldCount: fields.length,
2315
+ redactedCount,
2316
+ fields
2317
+ };
2318
+ const outputPath = import_node_path10.default.join(ctx.checkpointDir, "form-state.json");
2319
+ await import_promises9.default.writeFile(outputPath, `${JSON.stringify(data, null, 2)}
2320
+ `, "utf8");
2321
+ return {
2322
+ data,
2323
+ artifacts: [
2324
+ {
2325
+ name: "form-state",
2326
+ path: outputPath,
2327
+ contentType: "application/json"
2328
+ }
2329
+ ],
2330
+ summary: {
2331
+ fieldCount: data.fieldCount,
2332
+ redactedCount: data.redactedCount
2333
+ }
2334
+ };
2335
+ }
2336
+ };
2337
+ }
2338
+ });
2339
+
2340
+ // src/collectors/html.ts
2341
+ async function readPageContent(page) {
2342
+ try {
2343
+ await settlePage(page);
2344
+ return await page.content();
2345
+ } catch {
2346
+ await page.waitForTimeout(500);
2347
+ await settlePage(page);
2348
+ return await page.content();
2349
+ }
2350
+ }
2351
+ var import_promises10, import_node_path11, htmlCollector;
2352
+ var init_html = __esm({
2353
+ "src/collectors/html.ts"() {
2354
+ "use strict";
2355
+ import_promises10 = __toESM(require("fs/promises"), 1);
2356
+ import_node_path11 = __toESM(require("path"), 1);
2357
+ init_page_utils();
2358
+ htmlCollector = {
2359
+ name: "html",
2360
+ defaultEnabled: true,
2361
+ async collect(ctx) {
2362
+ const htmlPath = import_node_path11.default.join(ctx.checkpointDir, "page.html");
2363
+ const html = await readPageContent(ctx.page);
2364
+ await import_promises10.default.writeFile(htmlPath, html, "utf8");
2365
+ return {
2366
+ data: {
2367
+ contentLength: html.length
2368
+ },
2369
+ artifacts: [
2370
+ {
2371
+ name: "html",
2372
+ path: htmlPath,
2373
+ contentType: "text/html"
2374
+ }
2375
+ ],
2376
+ summary: {
2377
+ htmlPath: "page.html"
2378
+ }
2379
+ };
2380
+ }
2381
+ };
2382
+ }
2383
+ });
2384
+
2385
+ // src/collectors/metadata.ts
2386
+ function normalizeStructuredData(scriptContents) {
2387
+ const values = [];
2388
+ for (const content of scriptContents) {
2389
+ const value = content?.trim();
2390
+ if (!value) {
2391
+ continue;
2392
+ }
2393
+ try {
2394
+ values.push(JSON.parse(value));
2395
+ } catch {
2396
+ values.push({
2397
+ parseError: "Invalid JSON-LD",
2398
+ raw: value
2399
+ });
2400
+ }
2401
+ }
2402
+ return values;
2403
+ }
2404
+ var import_promises11, import_node_path12, metadataCollector;
2405
+ var init_metadata = __esm({
2406
+ "src/collectors/metadata.ts"() {
2407
+ "use strict";
2408
+ import_promises11 = __toESM(require("fs/promises"), 1);
2409
+ import_node_path12 = __toESM(require("path"), 1);
2410
+ metadataCollector = {
2411
+ name: "metadata",
2412
+ defaultEnabled: true,
2413
+ async collect(ctx) {
2414
+ const metadata = await ctx.page.evaluate(() => {
2415
+ const meta = (selector) => document.querySelector(selector)?.getAttribute("content") ?? null;
2416
+ const canonicalLink = document.querySelector('link[rel="canonical"]');
2417
+ const html = document.documentElement;
2418
+ const structuredDataScripts = Array.from(document.querySelectorAll('script[type="application/ld+json"]')).map((script) => script.textContent ?? null);
2419
+ return {
2420
+ url: location.href,
2421
+ title: document.title,
2422
+ description: meta('meta[name="description"]'),
2423
+ openGraph: {
2424
+ title: meta('meta[property="og:title"]'),
2425
+ description: meta('meta[property="og:description"]'),
2426
+ image: meta('meta[property="og:image"]')
2427
+ },
2428
+ canonicalUrl: canonicalLink?.getAttribute("href") ?? null,
2429
+ lang: html.getAttribute("lang"),
2430
+ viewport: meta('meta[name="viewport"]'),
2431
+ structuredDataScripts
2432
+ };
2433
+ });
2434
+ const normalizedMetadata = {
2435
+ url: metadata.url,
2436
+ title: metadata.title,
2437
+ description: metadata.description,
2438
+ openGraph: metadata.openGraph,
2439
+ canonicalUrl: metadata.canonicalUrl,
2440
+ lang: metadata.lang,
2441
+ viewport: metadata.viewport,
2442
+ structuredData: normalizeStructuredData(metadata.structuredDataScripts)
2443
+ };
2444
+ const outputPath = import_node_path12.default.join(ctx.checkpointDir, "metadata.json");
2445
+ await import_promises11.default.writeFile(outputPath, `${JSON.stringify(normalizedMetadata, null, 2)}
2446
+ `, "utf8");
2447
+ return {
2448
+ data: normalizedMetadata,
2449
+ artifacts: [
2450
+ {
2451
+ name: "metadata",
2452
+ path: outputPath,
2453
+ contentType: "application/json"
2454
+ }
2455
+ ],
2456
+ summary: {
2457
+ url: normalizedMetadata.url,
2458
+ title: normalizedMetadata.title,
2459
+ lang: normalizedMetadata.lang
2460
+ }
2461
+ };
2462
+ }
2463
+ };
2464
+ }
2465
+ });
2466
+
2467
+ // src/collectors/network.ts
2468
+ var import_promises12, import_node_path13, networkStates, networkCollector;
2469
+ var init_network = __esm({
2470
+ "src/collectors/network.ts"() {
2471
+ "use strict";
2472
+ import_promises12 = __toESM(require("fs/promises"), 1);
2473
+ import_node_path13 = __toESM(require("path"), 1);
2474
+ networkStates = /* @__PURE__ */ new WeakMap();
2475
+ networkCollector = {
2476
+ name: "network",
2477
+ defaultEnabled: true,
2478
+ async setup({ page }) {
2479
+ if (networkStates.has(page)) {
2480
+ return;
2481
+ }
2482
+ const entries = [];
2483
+ const recordRequestFailure = (request) => {
2484
+ entries.push({
2485
+ kind: "requestfailed",
2486
+ url: request.url(),
2487
+ method: request.method(),
2488
+ status: null,
2489
+ statusText: null,
2490
+ failureText: request.failure()?.errorText ?? null,
2491
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2492
+ });
2493
+ };
2494
+ const recordHttpError = (response) => {
2495
+ if (response.status() < 400) {
2496
+ return;
2497
+ }
2498
+ entries.push({
2499
+ kind: "http-error",
2500
+ url: response.url(),
2501
+ method: response.request().method(),
2502
+ status: response.status(),
2503
+ statusText: response.statusText(),
2504
+ failureText: null,
2505
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2506
+ });
2507
+ };
2508
+ page.on("requestfailed", recordRequestFailure);
2509
+ page.on("response", recordHttpError);
2510
+ networkStates.set(page, {
2511
+ entries,
2512
+ offset: 0,
2513
+ recordRequestFailure,
2514
+ recordHttpError
2515
+ });
2516
+ },
2517
+ async collect(ctx) {
2518
+ const state = networkStates.get(ctx.page);
2519
+ const checkpointEntries = state ? state.entries.slice(state.offset) : [];
2520
+ if (state) {
2521
+ state.offset = state.entries.length;
2522
+ }
2523
+ const outputPath = import_node_path13.default.join(ctx.checkpointDir, "failed-requests.json");
2524
+ await import_promises12.default.writeFile(outputPath, `${JSON.stringify(checkpointEntries, null, 2)}
2525
+ `, "utf8");
2526
+ return {
2527
+ data: checkpointEntries,
2528
+ artifacts: [
2529
+ {
2530
+ name: "failed-requests",
2531
+ path: outputPath,
2532
+ contentType: "application/json"
2533
+ }
2534
+ ],
2535
+ summary: {
2536
+ failedRequestCount: checkpointEntries.length
2537
+ }
2538
+ };
2539
+ },
2540
+ async teardown({ page }) {
2541
+ const state = networkStates.get(page);
2542
+ if (!state) {
2543
+ return;
2544
+ }
2545
+ page.off("requestfailed", state.recordRequestFailure);
2546
+ page.off("response", state.recordHttpError);
2547
+ networkStates.delete(page);
2548
+ }
2549
+ };
2550
+ }
2551
+ });
2552
+
2553
+ // src/collectors/network-timing.ts
2554
+ function maybeDuration(start, end) {
2555
+ if (start <= 0 || end <= 0 || end < start) {
2556
+ return null;
2557
+ }
2558
+ return end - start;
2559
+ }
2560
+ function toNetworkRecord(response, timing) {
2561
+ return {
2562
+ url: response.url,
2563
+ status: response.status,
2564
+ statusText: response.statusText,
2565
+ resourceType: response.resourceType,
2566
+ timestamp: response.timestamp,
2567
+ durationMs: timing ? timing.duration : null,
2568
+ transferSize: timing ? timing.transferSize : null,
2569
+ encodedBodySize: timing ? timing.encodedBodySize : null,
2570
+ decodedBodySize: timing ? timing.decodedBodySize : null,
2571
+ nextHopProtocol: timing ? timing.nextHopProtocol || null : null,
2572
+ timing: {
2573
+ startTimeMs: timing ? timing.startTime : null,
2574
+ redirectMs: timing ? maybeDuration(timing.redirectStart, timing.redirectEnd) : null,
2575
+ dnsMs: timing ? maybeDuration(timing.domainLookupStart, timing.domainLookupEnd) : null,
2576
+ connectMs: timing ? maybeDuration(timing.connectStart, timing.connectEnd) : null,
2577
+ tlsMs: timing ? maybeDuration(timing.secureConnectionStart, timing.connectEnd) : null,
2578
+ requestMs: timing ? maybeDuration(timing.requestStart, timing.responseStart) : null,
2579
+ responseMs: timing ? maybeDuration(timing.responseStart, timing.responseEnd) : null
2580
+ }
2581
+ };
2582
+ }
2583
+ var import_promises13, import_node_path14, timingStates, networkTimingCollector;
2584
+ var init_network_timing = __esm({
2585
+ "src/collectors/network-timing.ts"() {
2586
+ "use strict";
2587
+ import_promises13 = __toESM(require("fs/promises"), 1);
2588
+ import_node_path14 = __toESM(require("path"), 1);
2589
+ timingStates = /* @__PURE__ */ new WeakMap();
2590
+ networkTimingCollector = {
2591
+ name: "network-timing",
2592
+ defaultEnabled: false,
2593
+ async setup({ page }) {
2594
+ if (timingStates.has(page)) {
2595
+ return;
2596
+ }
2597
+ const responses = [];
2598
+ const recordResponse = (response) => {
2599
+ responses.push({
2600
+ url: response.url(),
2601
+ status: response.status(),
2602
+ statusText: response.statusText(),
2603
+ resourceType: response.request().resourceType(),
2604
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2605
+ });
2606
+ };
2607
+ page.on("response", recordResponse);
2608
+ timingStates.set(page, {
2609
+ responses,
2610
+ responseOffset: 0,
2611
+ resourceOffsetByUrl: /* @__PURE__ */ new Map(),
2612
+ recordResponse
2613
+ });
2614
+ },
2615
+ async collect(ctx) {
2616
+ const state = timingStates.get(ctx.page);
2617
+ const recentResponses = state ? state.responses.slice(state.responseOffset) : [];
2618
+ if (state) {
2619
+ state.responseOffset = state.responses.length;
2620
+ }
2621
+ const resourceTimings = await ctx.page.evaluate(() => {
2622
+ const entries = performance.getEntriesByType("resource");
2623
+ return entries.map((entry) => ({
2624
+ name: entry.name,
2625
+ duration: entry.duration,
2626
+ transferSize: entry.transferSize,
2627
+ encodedBodySize: entry.encodedBodySize,
2628
+ decodedBodySize: entry.decodedBodySize,
2629
+ nextHopProtocol: entry.nextHopProtocol,
2630
+ startTime: entry.startTime,
2631
+ redirectStart: entry.redirectStart,
2632
+ redirectEnd: entry.redirectEnd,
2633
+ domainLookupStart: entry.domainLookupStart,
2634
+ domainLookupEnd: entry.domainLookupEnd,
2635
+ connectStart: entry.connectStart,
2636
+ connectEnd: entry.connectEnd,
2637
+ secureConnectionStart: entry.secureConnectionStart,
2638
+ requestStart: entry.requestStart,
2639
+ responseStart: entry.responseStart,
2640
+ responseEnd: entry.responseEnd
2641
+ }));
2642
+ });
2643
+ const timingsByUrl = /* @__PURE__ */ new Map();
2644
+ for (const timing of resourceTimings) {
2645
+ const list = timingsByUrl.get(timing.name);
2646
+ if (list) {
2647
+ list.push(timing);
2648
+ } else {
2649
+ timingsByUrl.set(timing.name, [timing]);
2650
+ }
2651
+ }
2652
+ const requests = recentResponses.map((response) => {
2653
+ if (!state) {
2654
+ return toNetworkRecord(response, null);
2655
+ }
2656
+ const list = timingsByUrl.get(response.url) ?? [];
2657
+ const currentOffset = state.resourceOffsetByUrl.get(response.url) ?? 0;
2658
+ const match = list[currentOffset] ?? null;
2659
+ if (match) {
2660
+ state.resourceOffsetByUrl.set(response.url, currentOffset + 1);
2661
+ }
2662
+ return toNetworkRecord(response, match);
2663
+ });
2664
+ const totalBytes = requests.reduce((total, request) => {
2665
+ if (typeof request.transferSize !== "number" || request.transferSize < 0) {
2666
+ return total;
2667
+ }
2668
+ return total + request.transferSize;
2669
+ }, 0);
2670
+ const slowestRequestMs = requests.reduce((slowest, request) => {
2671
+ if (typeof request.durationMs !== "number") {
2672
+ return slowest;
2673
+ }
2674
+ return Math.max(slowest, request.durationMs);
2675
+ }, 0);
2676
+ const data = {
2677
+ requestCount: requests.length,
2678
+ totalBytes,
2679
+ slowestRequestMs,
2680
+ requests
2681
+ };
2682
+ const outputPath = import_node_path14.default.join(ctx.checkpointDir, "network-timing.json");
2683
+ await import_promises13.default.writeFile(outputPath, `${JSON.stringify(data, null, 2)}
2684
+ `, "utf8");
2685
+ return {
2686
+ data,
2687
+ artifacts: [
2688
+ {
2689
+ name: "network-timing",
2690
+ path: outputPath,
2691
+ contentType: "application/json"
2692
+ }
2693
+ ],
2694
+ summary: {
2695
+ requestCount: data.requestCount,
2696
+ totalBytes: data.totalBytes,
2697
+ slowestRequestMs: data.slowestRequestMs
2698
+ }
2699
+ };
2700
+ },
2701
+ async teardown({ page }) {
2702
+ const state = timingStates.get(page);
2703
+ if (!state) {
2704
+ return;
2705
+ }
2706
+ page.off("response", state.recordResponse);
2707
+ timingStates.delete(page);
2708
+ }
2709
+ };
2710
+ }
2711
+ });
2712
+
2713
+ // src/collectors/screenshot.ts
2714
+ function readPngSize(buffer) {
2715
+ if (buffer.length < 24 || !buffer.subarray(0, 8).equals(PNG_SIGNATURE)) {
2716
+ return null;
2717
+ }
2718
+ if (buffer.toString("ascii", 12, 16) !== "IHDR") {
2719
+ return null;
2720
+ }
2721
+ return {
2722
+ width: buffer.readUInt32BE(16),
2723
+ height: buffer.readUInt32BE(20)
2724
+ };
2725
+ }
2726
+ var import_node_path15, PNG_SIGNATURE, screenshotCollector;
2727
+ var init_screenshot = __esm({
2728
+ "src/collectors/screenshot.ts"() {
2729
+ "use strict";
2730
+ import_node_path15 = __toESM(require("path"), 1);
2731
+ PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
2732
+ screenshotCollector = {
2733
+ name: "screenshot",
2734
+ defaultEnabled: true,
2735
+ async collect(ctx) {
2736
+ const fullPage = ctx.options.fullPage ?? true;
2737
+ const screenshotPath = import_node_path15.default.join(ctx.checkpointDir, "page.png");
2738
+ const screenshotBuffer = await ctx.page.screenshot({ path: screenshotPath, fullPage });
2739
+ let highlightBounds = null;
2740
+ if (ctx.options.highlightSelector) {
2741
+ highlightBounds = await ctx.page.locator(ctx.options.highlightSelector).boundingBox().catch(() => null);
2742
+ }
2743
+ return {
2744
+ data: {
2745
+ fullPage,
2746
+ highlightBounds,
2747
+ highlightSelector: ctx.options.highlightSelector ?? null,
2748
+ imageSize: Buffer.isBuffer(screenshotBuffer) ? readPngSize(screenshotBuffer) : null
2749
+ },
2750
+ artifacts: [
2751
+ {
2752
+ name: "screenshot",
2753
+ path: screenshotPath,
2754
+ contentType: "image/png"
2755
+ }
2756
+ ],
2757
+ summary: {
2758
+ screenshotPath: "page.png"
2759
+ }
2760
+ };
2761
+ }
2762
+ };
2763
+ }
2764
+ });
2765
+
2766
+ // src/collectors/storage.ts
2767
+ function toRegex2(pattern) {
2768
+ const trimmed = pattern.trim();
2769
+ if (!trimmed) {
2770
+ return null;
2771
+ }
2772
+ try {
2773
+ return new RegExp(trimmed, "i");
2774
+ } catch {
2775
+ return null;
2776
+ }
2777
+ }
2778
+ function buildRedactionRegexes(ctx) {
2779
+ const fromConfig = Array.isArray(ctx.config.redact) ? ctx.config.redact.filter((entry) => typeof entry === "string") : [];
2780
+ return [...DEFAULT_REDACT_PATTERNS2, ...ctx.redact, ...fromConfig].map((pattern) => toRegex2(pattern)).filter((value) => value instanceof RegExp);
2781
+ }
2782
+ function shouldRedact(identifier, value, regexes) {
2783
+ if (regexes.some((regex) => regex.test(identifier))) {
2784
+ return true;
2785
+ }
2786
+ if (!value) {
2787
+ return false;
2788
+ }
2789
+ if (EMAIL_LIKE_REGEX2.test(value.trim())) {
2790
+ return true;
2791
+ }
2792
+ return regexes.some((regex) => regex.test(value));
2793
+ }
2794
+ var import_promises14, import_node_path16, REDACTED2, DEFAULT_REDACT_PATTERNS2, EMAIL_LIKE_REGEX2, storageCollector;
2795
+ var init_storage = __esm({
2796
+ "src/collectors/storage.ts"() {
2797
+ "use strict";
2798
+ import_promises14 = __toESM(require("fs/promises"), 1);
2799
+ import_node_path16 = __toESM(require("path"), 1);
2800
+ REDACTED2 = "[REDACTED]";
2801
+ DEFAULT_REDACT_PATTERNS2 = ["password", "token", "secret", "api[_-]?key", "authorization", "session", "email"];
2802
+ EMAIL_LIKE_REGEX2 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
2803
+ storageCollector = {
2804
+ name: "storage",
2805
+ defaultEnabled: false,
2806
+ async collect(ctx) {
2807
+ const includeCookieValues = ctx.config.includeCookieValues === true;
2808
+ const includeLocalStorageValues = ctx.config.includeLocalStorageValues === true;
2809
+ const redactValues = ctx.config.redactValues !== false;
2810
+ const regexes = buildRedactionRegexes({ redact: ctx.redact, config: ctx.config });
2811
+ const cookies = await ctx.page.context().cookies();
2812
+ const localStorageEntries = await ctx.page.evaluate(
2813
+ () => Object.keys(localStorage).map((key) => ({
2814
+ key,
2815
+ value: localStorage.getItem(key) ?? ""
2816
+ }))
2817
+ );
2818
+ const normalizedCookies = cookies.map((cookie) => {
2819
+ const rawValue = includeCookieValues ? cookie.value : null;
2820
+ const redacted = redactValues && shouldRedact(cookie.name, rawValue, regexes);
2821
+ return {
2822
+ name: cookie.name,
2823
+ domain: cookie.domain,
2824
+ path: cookie.path,
2825
+ value: rawValue == null ? null : redacted ? REDACTED2 : rawValue,
2826
+ redacted,
2827
+ expires: cookie.expires,
2828
+ httpOnly: cookie.httpOnly,
2829
+ secure: cookie.secure,
2830
+ sameSite: cookie.sameSite
2831
+ };
2832
+ });
2833
+ const normalizedLocalStorage = localStorageEntries.map((entry) => {
2834
+ const rawValue = includeLocalStorageValues ? entry.value : null;
2835
+ const redacted = redactValues && shouldRedact(entry.key, rawValue, regexes);
2836
+ return {
2837
+ key: entry.key,
2838
+ value: rawValue == null ? null : redacted ? REDACTED2 : rawValue,
2839
+ redacted
2840
+ };
2841
+ });
2842
+ const data = {
2843
+ cookieCount: normalizedCookies.length,
2844
+ localStorageKeyCount: normalizedLocalStorage.length,
2845
+ cookies: normalizedCookies,
2846
+ localStorage: normalizedLocalStorage
2847
+ };
2848
+ const outputPath = import_node_path16.default.join(ctx.checkpointDir, "storage-state.json");
2849
+ await import_promises14.default.writeFile(outputPath, `${JSON.stringify(data, null, 2)}
2850
+ `, "utf8");
2851
+ return {
2852
+ data,
2853
+ artifacts: [
2854
+ {
2855
+ name: "storage-state",
2856
+ path: outputPath,
2857
+ contentType: "application/json"
2858
+ }
2859
+ ],
2860
+ summary: {
2861
+ cookieCount: data.cookieCount,
2862
+ localStorageKeyCount: data.localStorageKeyCount
2863
+ }
2864
+ };
2865
+ }
2866
+ };
2867
+ }
2868
+ });
2869
+
2870
+ // src/collectors/web-vitals.ts
2871
+ function rateMetric(value, thresholds) {
2872
+ if (value == null || Number.isNaN(value)) {
2873
+ return "unknown";
2874
+ }
2875
+ if (value <= thresholds.good) {
2876
+ return "good";
2877
+ }
2878
+ if (value <= thresholds.needsImprovement) {
2879
+ return "needs-improvement";
2880
+ }
2881
+ return "poor";
2882
+ }
2883
+ function metric(value, thresholds) {
2884
+ return {
2885
+ value,
2886
+ rating: rateMetric(value, thresholds)
2887
+ };
2888
+ }
2889
+ async function captureWebVitals(page) {
2890
+ const raw = await page.evaluate(() => {
2891
+ const globalState = globalThis;
2892
+ const state = globalState.__e2eWebVitals ?? {
2893
+ cls: 0,
2894
+ fcp: null,
2895
+ lcp: null,
2896
+ inp: null
2897
+ };
2898
+ const navigation = performance.getEntriesByType("navigation")[0];
2899
+ return {
2900
+ cls: state.cls,
2901
+ fcp: state.fcp,
2902
+ lcp: state.lcp,
2903
+ inp: state.inp,
2904
+ ttfb: navigation ? navigation.responseStart : null,
2905
+ domContentLoaded: navigation ? navigation.domContentLoadedEventEnd : null,
2906
+ loadEvent: navigation ? navigation.loadEventEnd : null,
2907
+ url: location.href
2908
+ };
2909
+ });
2910
+ return {
2911
+ url: raw.url,
2912
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
2913
+ cls: metric(raw.cls, { good: 0.1, needsImprovement: 0.25 }),
2914
+ fcpMs: metric(raw.fcp, { good: 1800, needsImprovement: 3e3 }),
2915
+ lcpMs: metric(raw.lcp, { good: 2500, needsImprovement: 4e3 }),
2916
+ inpMs: metric(raw.inp, { good: 200, needsImprovement: 500 }),
2917
+ ttfbMs: metric(raw.ttfb, { good: 800, needsImprovement: 1800 }),
2918
+ domContentLoadedMs: raw.domContentLoaded,
2919
+ loadEventMs: raw.loadEvent
2920
+ };
2921
+ }
2922
+ var import_promises15, import_node_path17, initializedPages, webVitalsCollector;
2923
+ var init_web_vitals = __esm({
2924
+ "src/collectors/web-vitals.ts"() {
2925
+ "use strict";
2926
+ import_promises15 = __toESM(require("fs/promises"), 1);
2927
+ import_node_path17 = __toESM(require("path"), 1);
2928
+ initializedPages = /* @__PURE__ */ new WeakSet();
2929
+ webVitalsCollector = {
2930
+ name: "web-vitals",
2931
+ defaultEnabled: true,
2932
+ async setup({ page }) {
2933
+ if (initializedPages.has(page)) {
2934
+ return;
2935
+ }
2936
+ initializedPages.add(page);
2937
+ await page.addInitScript(() => {
2938
+ const globalState = globalThis;
2939
+ if (!globalState.__e2eWebVitals) {
2940
+ globalState.__e2eWebVitals = {
2941
+ cls: 0,
2942
+ fcp: null,
2943
+ lcp: null,
2944
+ inp: null
2945
+ };
2946
+ }
2947
+ const state = globalState.__e2eWebVitals;
2948
+ try {
2949
+ const paintObserver = new PerformanceObserver((entryList) => {
2950
+ for (const entry of entryList.getEntries()) {
2951
+ if (entry.name === "first-contentful-paint") {
2952
+ state.fcp = entry.startTime;
2953
+ }
2954
+ }
2955
+ });
2956
+ paintObserver.observe({ type: "paint", buffered: true });
2957
+ } catch {
2958
+ }
2959
+ try {
2960
+ const lcpObserver = new PerformanceObserver((entryList) => {
2961
+ const entries = entryList.getEntries();
2962
+ const lastEntry = entries[entries.length - 1];
2963
+ if (lastEntry) {
2964
+ state.lcp = lastEntry.startTime;
2965
+ }
2966
+ });
2967
+ lcpObserver.observe({ type: "largest-contentful-paint", buffered: true });
2968
+ addEventListener("pagehide", () => lcpObserver.disconnect(), { once: true });
2969
+ } catch {
2970
+ }
2971
+ try {
2972
+ const clsObserver = new PerformanceObserver((entryList) => {
2973
+ for (const entry of entryList.getEntries()) {
2974
+ if (!entry.hadRecentInput) {
2975
+ state.cls += entry.value ?? 0;
2976
+ }
2977
+ }
2978
+ });
2979
+ clsObserver.observe({ type: "layout-shift", buffered: true });
2980
+ addEventListener("pagehide", () => clsObserver.disconnect(), { once: true });
2981
+ } catch {
2982
+ }
2983
+ try {
2984
+ const inpObserver = new PerformanceObserver((entryList) => {
2985
+ for (const entry of entryList.getEntries()) {
2986
+ const duration = entry.duration ?? 0;
2987
+ if (state.inp == null || duration > state.inp) {
2988
+ state.inp = duration;
2989
+ }
2990
+ }
2991
+ });
2992
+ inpObserver.observe({ type: "event", buffered: true, durationThreshold: 40 });
2993
+ addEventListener("pagehide", () => inpObserver.disconnect(), { once: true });
2994
+ } catch {
2995
+ }
2996
+ });
2997
+ },
2998
+ async collect(ctx) {
2999
+ const snapshot = await captureWebVitals(ctx.page);
3000
+ const outputPath = import_node_path17.default.join(ctx.checkpointDir, "web-vitals.json");
3001
+ await import_promises15.default.writeFile(outputPath, `${JSON.stringify(snapshot, null, 2)}
3002
+ `, "utf8");
3003
+ return {
3004
+ data: snapshot,
3005
+ artifacts: [
3006
+ {
3007
+ name: "web-vitals",
3008
+ path: outputPath,
3009
+ contentType: "application/json"
3010
+ }
3011
+ ],
3012
+ summary: {
3013
+ cls: snapshot.cls,
3014
+ fcp: snapshot.fcpMs,
3015
+ lcp: snapshot.lcpMs,
3016
+ inp: snapshot.inpMs,
3017
+ ttfb: snapshot.ttfbMs
3018
+ }
3019
+ };
3020
+ },
3021
+ async teardown({ page }) {
3022
+ initializedPages.delete(page);
3023
+ }
3024
+ };
3025
+ }
3026
+ });
3027
+
3028
+ // src/collectors/builtin-collectors.ts
3029
+ var builtinCollectors;
3030
+ var init_builtin_collectors = __esm({
3031
+ "src/collectors/builtin-collectors.ts"() {
3032
+ "use strict";
3033
+ init_aria_snapshot();
3034
+ init_axe();
3035
+ init_console();
3036
+ init_dom_stats();
3037
+ init_forms();
3038
+ init_html();
3039
+ init_metadata();
3040
+ init_network();
3041
+ init_network_timing();
3042
+ init_screenshot();
3043
+ init_storage();
3044
+ init_web_vitals();
3045
+ builtinCollectors = [
3046
+ screenshotCollector,
3047
+ htmlCollector,
3048
+ axeCollector,
3049
+ webVitalsCollector,
3050
+ consoleCollector,
3051
+ networkCollector,
3052
+ metadataCollector,
3053
+ ariaSnapshotCollector,
3054
+ domStatsCollector,
3055
+ formsCollector,
3056
+ storageCollector,
3057
+ networkTimingCollector
3058
+ ];
3059
+ }
3060
+ });
3061
+
3062
+ // src/collectors/registry.ts
3063
+ function registerBuiltinCollector(collector) {
3064
+ builtinCollectors2.set(collector.name, collector);
3065
+ }
3066
+ function registerBuiltinCollectors(collectors) {
3067
+ if (builtinsRegistered) {
3068
+ return;
3069
+ }
3070
+ for (const collector of collectors) {
3071
+ registerBuiltinCollector(collector);
3072
+ }
3073
+ builtinsRegistered = true;
3074
+ }
3075
+ function getBuiltinCollectors() {
3076
+ return new Map(builtinCollectors2);
3077
+ }
3078
+ var builtinCollectors2, builtinsRegistered;
3079
+ var init_registry = __esm({
3080
+ "src/collectors/registry.ts"() {
3081
+ "use strict";
3082
+ builtinCollectors2 = /* @__PURE__ */ new Map();
3083
+ builtinsRegistered = false;
3084
+ }
3085
+ });
3086
+
3087
+ // src/core.ts
3088
+ function cloneResolvedConfig(config) {
3089
+ return { ...config };
3090
+ }
3091
+ function cloneCollectorState(state) {
3092
+ return {
3093
+ enabled: state?.enabled ?? false,
3094
+ config: cloneResolvedConfig(state?.config ?? {})
3095
+ };
3096
+ }
3097
+ function applyCollectorInput(state, input) {
3098
+ const next = cloneCollectorState(state);
3099
+ if (input === void 0) {
3100
+ return next;
3101
+ }
3102
+ if (input === false) {
3103
+ return {
3104
+ enabled: false,
3105
+ config: {}
3106
+ };
3107
+ }
3108
+ if (input === true) {
3109
+ return {
3110
+ enabled: true,
3111
+ config: next.config
3112
+ };
3113
+ }
3114
+ return {
3115
+ enabled: true,
3116
+ config: {
3117
+ ...next.config,
3118
+ ...input
3119
+ }
3120
+ };
3121
+ }
3122
+ function collectorRegistryFor(config = {}) {
3123
+ const registry = getBuiltinCollectors();
3124
+ for (const collector of config.custom ?? []) {
3125
+ registry.set(collector.name, collector);
3126
+ }
3127
+ return registry;
3128
+ }
3129
+ function cloneCheckpointOptions(options) {
3130
+ return {
3131
+ ...options,
3132
+ ...options.collectors ? { collectors: { ...options.collectors } } : {}
3133
+ };
3134
+ }
3135
+ function warn(message, error) {
3136
+ if (error instanceof Error) {
3137
+ console.warn(`[playwright-checkpoint] ${message}`, error);
3138
+ return;
3139
+ }
3140
+ if (error !== void 0) {
3141
+ console.warn(`[playwright-checkpoint] ${message}`, String(error));
3142
+ return;
3143
+ }
3144
+ console.warn(`[playwright-checkpoint] ${message}`);
3145
+ }
3146
+ function sanitizeSegment(value) {
3147
+ return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "checkpoint";
3148
+ }
3149
+ function checkpointSlug(name, existing) {
3150
+ const base = sanitizeSegment(name);
3151
+ const existingSlugs = new Set(existing.map((record) => record.slug));
3152
+ if (!existingSlugs.has(base)) {
3153
+ return base;
3154
+ }
3155
+ let index = 2;
3156
+ let candidate = `${base}-${index}`;
3157
+ while (existingSlugs.has(candidate)) {
3158
+ index += 1;
3159
+ candidate = `${base}-${index}`;
3160
+ }
3161
+ return candidate;
3162
+ }
3163
+ async function attachArtifacts(testInfo, checkpointSlugValue, collectorName, artifacts) {
3164
+ const attach = testInfo?.attach;
3165
+ if (typeof attach !== "function") {
3166
+ return;
3167
+ }
3168
+ for (const artifact of artifacts) {
3169
+ try {
3170
+ await attach.call(testInfo, `${checkpointSlugValue}/${collectorName}/${artifact.name}`, {
3171
+ path: artifact.path,
3172
+ contentType: artifact.contentType
3173
+ });
3174
+ } catch (error) {
3175
+ warn(`Failed to attach artifact "${artifact.name}" from collector "${collectorName}".`, error);
3176
+ }
3177
+ }
3178
+ }
3179
+ async function collectPageTitle(page) {
3180
+ try {
3181
+ return await page.title();
3182
+ } catch {
3183
+ return "";
3184
+ }
3185
+ }
3186
+ async function runCollectorSetup(collectors, page, testInfo) {
3187
+ for (const collector of collectors) {
3188
+ if (!collector.setup) {
3189
+ continue;
3190
+ }
3191
+ try {
3192
+ await collector.setup({ page, testInfo });
3193
+ } catch (error) {
3194
+ warn(`Collector "${collector.name}" setup failed.`, error);
3195
+ }
3196
+ }
3197
+ }
3198
+ async function runCollectorTeardown(collectors, page, testInfo) {
3199
+ const collectorList = Array.from(collectors).reverse();
3200
+ for (const collector of collectorList) {
3201
+ if (!collector.teardown) {
3202
+ continue;
3203
+ }
3204
+ try {
3205
+ await collector.teardown({ page, testInfo });
3206
+ } catch (error) {
3207
+ warn(`Collector "${collector.name}" teardown failed.`, error);
3208
+ }
3209
+ }
3210
+ }
3211
+ function resolveCollectors(globalConfig = {}, testConfig = null, checkpointOptions = {}) {
3212
+ const registry = collectorRegistryFor(globalConfig);
3213
+ const states = /* @__PURE__ */ new Map();
3214
+ for (const collector of registry.values()) {
3215
+ states.set(collector.name, {
3216
+ enabled: collector.defaultEnabled,
3217
+ config: {}
3218
+ });
3219
+ }
3220
+ const levels = [globalConfig.collectors, testConfig?.collectors, checkpointOptions.collectors];
3221
+ for (const level of levels) {
3222
+ for (const [name, input] of Object.entries(level ?? {})) {
3223
+ states.set(name, applyCollectorInput(states.get(name), input));
3224
+ }
3225
+ }
3226
+ const resolved = /* @__PURE__ */ new Map();
3227
+ for (const [name, state] of states) {
3228
+ if (state.enabled) {
3229
+ resolved.set(name, cloneResolvedConfig(state.config));
3230
+ }
3231
+ }
3232
+ return resolved;
3233
+ }
3234
+ async function runCollectorPipeline(args) {
3235
+ const options = cloneCheckpointOptions(args.options ?? {});
3236
+ const slug = args.slug ?? checkpointSlug(args.name, args.manifest?.checkpoints ?? []);
3237
+ const checkpointDir = import_node_path18.default.join(args.outputDir, slug);
3238
+ const collectorResults = {};
3239
+ await import_promises16.default.mkdir(checkpointDir, { recursive: true });
3240
+ await settlePage(args.page);
3241
+ for (const [collectorName, collectorConfig] of args.resolvedCollectors) {
3242
+ const collector = args.registry.get(collectorName);
3243
+ if (!collector) {
3244
+ warn(`Collector "${collectorName}" is enabled but no implementation is registered.`);
3245
+ continue;
3246
+ }
3247
+ try {
3248
+ const result = await collector.collect({
3249
+ page: args.page,
3250
+ testInfo: args.testInfo,
3251
+ checkpointDir,
3252
+ checkpointName: args.name,
3253
+ checkpointSlug: slug,
3254
+ redact: [...args.redact ?? []],
3255
+ config: cloneResolvedConfig(collectorConfig),
3256
+ options,
3257
+ adjustTimeout: args.adjustTimeout
3258
+ });
3259
+ collectorResults[collectorName] = result;
3260
+ await attachArtifacts(args.testInfo, slug, collectorName, result.artifacts);
3261
+ } catch (error) {
3262
+ warn(`Collector "${collectorName}" failed during checkpoint "${args.name}".`, error);
3263
+ }
3264
+ }
3265
+ const record = {
3266
+ name: args.name,
3267
+ slug,
3268
+ url: args.page.url(),
3269
+ title: await collectPageTitle(args.page),
3270
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3271
+ ...options.description ? { description: options.description } : {},
3272
+ ...typeof options.step === "number" ? { step: options.step } : {},
3273
+ collectors: collectorResults
3274
+ };
3275
+ args.manifest?.checkpoints.push(record);
3276
+ return record;
3277
+ }
3278
+ async function captureCheckpoint(page, name, options) {
3279
+ const sessionConfig = {
3280
+ collectors: options.collectors,
3281
+ custom: options.custom,
3282
+ redact: options.redact
3283
+ };
3284
+ const registry = collectorRegistryFor(sessionConfig);
3285
+ const resolvedCollectors = resolveCollectors(sessionConfig, null, options);
3286
+ const enabledCollectors = Array.from(resolvedCollectors.keys()).map((collectorName) => registry.get(collectorName)).filter((collector) => Boolean(collector));
3287
+ await import_promises16.default.mkdir(options.outputDir, { recursive: true });
3288
+ await runCollectorSetup(enabledCollectors, page, options.testInfo);
3289
+ try {
3290
+ return await runCollectorPipeline({
3291
+ page,
3292
+ name,
3293
+ outputDir: options.outputDir,
3294
+ resolvedCollectors,
3295
+ registry,
3296
+ options,
3297
+ redact: options.redact,
3298
+ testInfo: options.testInfo,
3299
+ adjustTimeout: options.adjustTimeout,
3300
+ slug: checkpointSlug(name, [])
3301
+ });
3302
+ } finally {
3303
+ await runCollectorTeardown(enabledCollectors, page, options.testInfo);
3304
+ }
3305
+ }
3306
+ var import_promises16, import_node_path18;
3307
+ var init_core = __esm({
3308
+ "src/core.ts"() {
3309
+ "use strict";
3310
+ import_promises16 = __toESM(require("fs/promises"), 1);
3311
+ import_node_path18 = __toESM(require("path"), 1);
3312
+ init_builtin_collectors();
3313
+ init_registry();
3314
+ init_page_utils();
3315
+ registerBuiltinCollectors(builtinCollectors);
3316
+ }
3317
+ });
3318
+
3319
+ // src/mcp/tools.ts
3320
+ async function handleBrowserCheckpoint(args, ctx) {
3321
+ const slug = sanitizeSegment(args.name);
3322
+ const record = await captureCheckpoint(ctx.page, args.name, {
3323
+ outputDir: ctx.outputDir,
3324
+ highlightSelector: args.highlightSelector,
3325
+ fullPage: args.fullPage ?? true,
3326
+ description: args.description,
3327
+ collectors: args.collectors
3328
+ });
3329
+ void slug;
3330
+ return formatCheckpointSummary(record);
3331
+ }
3332
+ async function handleBrowserCheckpointReport(args, testResultsDir = "test-results") {
3333
+ const outputDir = args.outputDir ?? "report";
3334
+ const config = {
3335
+ reporters: {
3336
+ html: args.format === void 0 || args.format === "html",
3337
+ markdown: args.format === "markdown",
3338
+ mdx: args.format === "mdx"
3339
+ }
3340
+ };
3341
+ const { resolve } = await import("path");
3342
+ const results = await runReporters(
3343
+ config,
3344
+ resolve(process.cwd(), testResultsDir),
3345
+ resolve(process.cwd(), outputDir)
3346
+ );
3347
+ const lines = Object.entries(results).map(([name, result]) => `- ${name}: ${result.summary}`);
3348
+ if (lines.length === 0) {
3349
+ return "No reports generated.";
3350
+ }
3351
+ return `Report generation complete:
3352
+ ${lines.join("\n")}`;
3353
+ }
3354
+ function handleBrowserCheckpointCompare(_args) {
3355
+ return "browser_checkpoint_compare is not yet implemented.";
3356
+ }
3357
+ function formatCheckpointSummary(record) {
3358
+ const lines = [];
3359
+ lines.push(`Checkpoint "${record.name}" captured.`);
3360
+ lines.push(`URL: ${record.url}`);
3361
+ lines.push(`Title: ${record.title}`);
3362
+ lines.push("");
3363
+ const axeData = record.collectors["axe"];
3364
+ if (axeData?.data) {
3365
+ const axe = axeData.data;
3366
+ if (axe.skipped) {
3367
+ lines.push(`Accessibility: skipped (${axe.reason ?? "unknown reason"})`);
3368
+ } else {
3369
+ const violations = axe.violations ?? 0;
3370
+ lines.push(`Accessibility: ${violations} violation${violations !== 1 ? "s" : ""}`);
3371
+ }
3372
+ }
3373
+ const wvData = record.collectors["web-vitals"];
3374
+ if (wvData?.data) {
3375
+ const wv = wvData.data;
3376
+ lines.push("Web Vitals:");
3377
+ const metricLines = [];
3378
+ for (const [key, metric2] of Object.entries(wv)) {
3379
+ if (!metric2 || typeof metric2 !== "object") continue;
3380
+ const m = metric2;
3381
+ if (m.value === null) continue;
3382
+ const ratingIcon = m.rating === "good" ? "\u2705" : m.rating === "needs-improvement" ? "\u26A0\uFE0F" : m.rating === "poor" ? "\u274C" : "";
3383
+ const label = key.replace(/Ms$/, "").toUpperCase();
3384
+ const formatted = key.endsWith("Ms") ? `${Math.round(m.value)}ms` : m.value.toFixed ? m.value.toFixed(3) : String(m.value);
3385
+ metricLines.push(` ${label}: ${formatted} (${m.rating} ${ratingIcon})`.trim());
3386
+ }
3387
+ if (metricLines.length > 0) {
3388
+ lines.push(metricLines.join("\n"));
3389
+ }
3390
+ }
3391
+ const consoleData = record.collectors["console"];
3392
+ if (consoleData?.data) {
3393
+ const entries = consoleData.data;
3394
+ const errors = entries.filter((e) => e.type === "error" || e.type === "pageerror");
3395
+ if (errors.length > 0) {
3396
+ lines.push(`Console: ${errors.length} error${errors.length !== 1 ? "s" : ""}`);
3397
+ for (const err of errors.slice(0, 3)) {
3398
+ lines.push(` ${err.text}`);
3399
+ }
3400
+ if (errors.length > 3) {
3401
+ lines.push(` ... and ${errors.length - 3} more`);
3402
+ }
3403
+ } else {
3404
+ lines.push("Console: no errors");
3405
+ }
3406
+ }
3407
+ const netData = record.collectors["network"];
3408
+ if (netData?.data) {
3409
+ const requests = netData.data;
3410
+ const failed = requests.filter((r) => r.status === null || r.status >= 400 || r.failureText);
3411
+ if (failed.length > 0) {
3412
+ lines.push(`Network: ${failed.length} failed request${failed.length !== 1 ? "s" : ""}`);
3413
+ for (const req of failed.slice(0, 3)) {
3414
+ const reason = req.failureText ?? `${req.status} ${req.url}`;
3415
+ lines.push(` ${req.url} \u2192 ${reason}`);
3416
+ }
3417
+ if (failed.length > 3) {
3418
+ lines.push(` ... and ${failed.length - 3} more`);
3419
+ }
3420
+ } else {
3421
+ lines.push("Network: 0 failed requests");
3422
+ }
3423
+ }
3424
+ const ssData = record.collectors["screenshot"];
3425
+ if (ssData?.summary) {
3426
+ const ss = ssData.summary;
3427
+ if (ss.screenshotPath) {
3428
+ lines.push(`Screenshot: ${ss.screenshotPath}`);
3429
+ }
3430
+ }
3431
+ return lines.join("\n");
3432
+ }
3433
+ var browserCheckpointSchema, browserCheckpointReportSchema, browserCheckpointCompareSchema, CHECKPOINT_TOOL_NAME, REPORT_TOOL_NAME, COMPARE_TOOL_NAME, CHECKPOINT_TOOLS;
3434
+ var init_tools = __esm({
3435
+ "src/mcp/tools.ts"() {
3436
+ "use strict";
3437
+ init_core();
3438
+ init_report();
3439
+ browserCheckpointSchema = {
3440
+ type: "object",
3441
+ properties: {
3442
+ name: { type: "string", description: 'Unique name for this checkpoint (e.g. "homepage", "after-login").' },
3443
+ description: {
3444
+ type: "string",
3445
+ description: "Long-form description of what this checkpoint captures."
3446
+ },
3447
+ highlightSelector: {
3448
+ type: "string",
3449
+ description: "CSS selector for a region to highlight in the annotated screenshot."
3450
+ },
3451
+ fullPage: {
3452
+ type: "boolean",
3453
+ description: "Capture the full page (default: true). Set false for viewport-only."
3454
+ },
3455
+ collectors: {
3456
+ type: "object",
3457
+ description: "Per-collector overrides. Set to false to disable, or an options object to configure.",
3458
+ additionalProperties: true
3459
+ }
3460
+ },
3461
+ required: ["name"]
3462
+ };
3463
+ browserCheckpointReportSchema = {
3464
+ type: "object",
3465
+ properties: {
3466
+ outputDir: {
3467
+ type: "string",
3468
+ description: "Directory to write report files (default: ./report)."
3469
+ },
3470
+ format: {
3471
+ type: "string",
3472
+ enum: ["html", "markdown", "mdx"],
3473
+ description: "Report format(s) to generate."
3474
+ }
3475
+ }
3476
+ };
3477
+ browserCheckpointCompareSchema = {
3478
+ type: "object",
3479
+ properties: {
3480
+ baseline: { type: "string", description: "Path or name of the baseline checkpoint manifest." },
3481
+ current: { type: "string", description: "Path or name of the current checkpoint manifest." }
3482
+ },
3483
+ required: ["baseline", "current"]
3484
+ };
3485
+ CHECKPOINT_TOOL_NAME = "browser_checkpoint";
3486
+ REPORT_TOOL_NAME = "browser_checkpoint_report";
3487
+ COMPARE_TOOL_NAME = "browser_checkpoint_compare";
3488
+ CHECKPOINT_TOOLS = [
3489
+ {
3490
+ name: CHECKPOINT_TOOL_NAME,
3491
+ description: "Capture a structured snapshot of the current browser page: screenshot, accessibility violations, Web Vitals, console errors, and network failures. Returns an LLM-friendly summary plus artifact paths.",
3492
+ inputSchema: browserCheckpointSchema
3493
+ },
3494
+ {
3495
+ name: REPORT_TOOL_NAME,
3496
+ description: "Generate an HTML, Markdown, or MDX report from all checkpoint manifests in a directory.",
3497
+ inputSchema: browserCheckpointReportSchema
3498
+ },
3499
+ {
3500
+ name: COMPARE_TOOL_NAME,
3501
+ description: "Compare two checkpoint runs to surface differences (not yet implemented).",
3502
+ inputSchema: browserCheckpointCompareSchema
3503
+ }
3504
+ ];
3505
+ }
3506
+ });
3507
+
3508
+ // src/mcp/index.ts
3509
+ var mcp_exports = {};
3510
+ __export(mcp_exports, {
3511
+ startMcpProxy: () => startMcpProxy
3512
+ });
3513
+ function ensureMcpSdk() {
3514
+ try {
3515
+ require("@modelcontextprotocol/sdk");
3516
+ } catch {
3517
+ throw new Error(
3518
+ "playwright-checkpoint MCP mode requires @modelcontextprotocol/sdk.\nInstall it: npm install @modelcontextprotocol/sdk"
3519
+ );
3520
+ }
3521
+ }
3522
+ function getNodeModuleCreateRequire3() {
3523
+ const { createRequire } = require("module");
3524
+ return createRequire(import_meta2.url);
3525
+ }
3526
+ async function startMcpProxy(options = {}) {
3527
+ ensureMcpSdk();
3528
+ const req = getNodeModuleCreateRequire3();
3529
+ const sdk = req("@modelcontextprotocol/sdk");
3530
+ const sdkServer = req("@modelcontextprotocol/sdk/server");
3531
+ const ListToolsRequestSchema = sdk.ListToolsRequestSchema;
3532
+ const CallToolRequestSchema = sdk.CallToolRequestSchema;
3533
+ const Server = sdkServer.Server;
3534
+ const outputDir = options.outputDir ?? "./checkpoints";
3535
+ let upstreamConnection = null;
3536
+ const upstreamTools = [];
3537
+ const upstreamPkg = options.standalone ? null : resolveUpstream({ upstream: options.upstream });
3538
+ if (upstreamPkg) {
3539
+ console.error(`[playwright-checkpoint MCP] Proxying upstream: ${upstreamPkg}`);
3540
+ let passthroughArgs = options.passthrough ?? [];
3541
+ if (!options.cdpEndpoint) {
3542
+ passthroughArgs = injectDebugPort(passthroughArgs);
3543
+ }
3544
+ upstreamConnection = spawnUpstream(upstreamPkg, passthroughArgs);
3545
+ await new Promise((resolve) => setTimeout(resolve, 500));
3546
+ try {
3547
+ const result = await upstreamConnection.listTools();
3548
+ for (const tool of result.tools) {
3549
+ upstreamTools.push({
3550
+ name: tool.name,
3551
+ description: tool.description,
3552
+ inputSchema: tool.inputSchema
3553
+ });
3554
+ }
3555
+ } catch (err) {
3556
+ console.error("[playwright-checkpoint MCP] Warning: could not list upstream tools:", err);
3557
+ }
3558
+ } else {
3559
+ console.error("[playwright-checkpoint MCP] Running in standalone mode (no upstream).");
3560
+ }
3561
+ const checkpointTools = CHECKPOINT_TOOLS.map((t) => ({
3562
+ name: t.name,
3563
+ description: t.description,
3564
+ inputSchema: t.inputSchema
3565
+ }));
3566
+ const allTools = [...checkpointTools, ...upstreamTools];
3567
+ const server = new Server({ name: "playwright-checkpoint", version: "0.1.0-beta.0" }, { capabilities: { tools: {} } });
3568
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
3569
+ return { tools: allTools };
3570
+ });
3571
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3572
+ const { name, arguments: args = {} } = request.params;
3573
+ if (name === CHECKPOINT_TOOL_NAME) {
3574
+ const { page } = await getUpstreamPage(upstreamConnection?.process ?? null, {
3575
+ cdpEndpoint: options.cdpEndpoint,
3576
+ debugPort: 9222
3577
+ });
3578
+ const result = await handleBrowserCheckpoint(
3579
+ args,
3580
+ { page, outputDir }
3581
+ );
3582
+ return { content: [{ type: "text", text: result }] };
3583
+ }
3584
+ if (name === REPORT_TOOL_NAME) {
3585
+ const result = await handleBrowserCheckpointReport(
3586
+ args
3587
+ );
3588
+ return { content: [{ type: "text", text: result }] };
3589
+ }
3590
+ if (name === COMPARE_TOOL_NAME) {
3591
+ const result = handleBrowserCheckpointCompare(
3592
+ args
3593
+ );
3594
+ return { content: [{ type: "text", text: result }], isError: true };
3595
+ }
3596
+ if (upstreamConnection) {
3597
+ try {
3598
+ const result = await upstreamConnection.callTool(name, args);
3599
+ return {
3600
+ content: result.content,
3601
+ isError: result.isError,
3602
+ structuredContent: result.structuredContent
3603
+ };
3604
+ } catch (err) {
3605
+ return {
3606
+ content: [
3607
+ {
3608
+ type: "text",
3609
+ text: `Upstream tool "${name}" failed: ${err instanceof Error ? err.message : String(err)}`
3610
+ }
3611
+ ],
3612
+ isError: true
3613
+ };
3614
+ }
3615
+ }
3616
+ return { content: [{ type: "text", text: `Tool "${name}" is not available.` }], isError: true };
3617
+ });
3618
+ const transport = new StdioServerTransport();
3619
+ await server.connect(transport);
3620
+ const cleanup = () => {
3621
+ resetCachedConnection();
3622
+ upstreamConnection?.close();
3623
+ };
3624
+ process.on("SIGINT", cleanup);
3625
+ process.on("SIGTERM", cleanup);
3626
+ }
3627
+ var import_meta2;
3628
+ var init_mcp = __esm({
3629
+ "src/mcp/index.ts"() {
3630
+ "use strict";
3631
+ init_transport();
3632
+ init_upstream();
3633
+ init_browser_connect();
3634
+ init_tools();
3635
+ import_meta2 = {};
3636
+ }
3637
+ });
3638
+
3639
+ // src/cli/index.ts
3640
+ var import_node_path5 = __toESM(require("path"), 1);
3641
+ init_report();
3642
+ var DEFAULT_RESULTS_DIR = "test-results";
3643
+ var DEFAULT_REPORT_OUTPUT_DIR = "report";
3644
+ var DEFAULT_DOCS_OUTPUT_DIR = "docs";
3645
+ function printHelp(log) {
3646
+ log(`playwright-checkpoint
3647
+
3648
+ Usage:
3649
+ playwright-checkpoint report [--results-dir ./test-results] [--output-dir ./report] [--reporter html,markdown]
3650
+ playwright-checkpoint docs [--results-dir ./test-results] [--output-dir ./docs] [--format markdown|mdx] [--filter @user-journey]
3651
+
3652
+ Commands:
3653
+ report Generate reports from checkpoint manifests
3654
+ docs Generate Markdown or MDX help articles from checkpoint manifests
3655
+
3656
+ Options:
3657
+ --results-dir <path> Directory containing checkpoint manifests (default: ./${DEFAULT_RESULTS_DIR})
3658
+ --output-dir <path> Directory where generated output is written (defaults: ./${DEFAULT_REPORT_OUTPUT_DIR} for report, ./${DEFAULT_DOCS_OUTPUT_DIR} for docs)
3659
+ --reporter <names> Comma-separated reporters to run with the report command (default: html)
3660
+ --format <name> Docs output format: markdown or mdx (default: markdown)
3661
+ --filter <tags> Comma-separated tag filter for docs generation
3662
+ -h, --help Show this help text`);
3663
+ }
3664
+ function takeFlagValue(args, index, flag) {
3665
+ const value = args[index + 1];
3666
+ if (!value) {
3667
+ throw new Error(`Missing value for ${flag}.`);
3668
+ }
3669
+ return {
3670
+ value,
3671
+ nextIndex: index + 1
3672
+ };
3673
+ }
3674
+ function parseCommaSeparated(value) {
3675
+ return value.split(",").map((entry) => entry.trim()).filter(Boolean);
3676
+ }
3677
+ function parseCliArgs(argv2) {
3678
+ if (argv2.length === 0 || argv2.includes("--help") || argv2.includes("-h")) {
3679
+ return { command: "help" };
3680
+ }
3681
+ const [command, ...rest] = argv2;
3682
+ if (command !== "report" && command !== "docs") {
3683
+ throw new Error(`Unknown command "${command}".`);
3684
+ }
3685
+ let resultsDir = DEFAULT_RESULTS_DIR;
3686
+ let outputDir = command === "docs" ? DEFAULT_DOCS_OUTPUT_DIR : DEFAULT_REPORT_OUTPUT_DIR;
3687
+ let reporters = null;
3688
+ let format = "markdown";
3689
+ let filterTags = [];
3690
+ for (let index = 0; index < rest.length; index += 1) {
3691
+ const argument = rest[index];
3692
+ if (!argument) {
3693
+ continue;
3694
+ }
3695
+ if (argument === "--results-dir") {
3696
+ const { value, nextIndex } = takeFlagValue(rest, index, argument);
3697
+ resultsDir = value;
3698
+ index = nextIndex;
3699
+ continue;
3700
+ }
3701
+ if (argument.startsWith("--results-dir=")) {
3702
+ resultsDir = argument.slice("--results-dir=".length);
3703
+ continue;
3704
+ }
3705
+ if (argument === "--output-dir") {
3706
+ const { value, nextIndex } = takeFlagValue(rest, index, argument);
3707
+ outputDir = value;
3708
+ index = nextIndex;
3709
+ continue;
3710
+ }
3711
+ if (argument.startsWith("--output-dir=")) {
3712
+ outputDir = argument.slice("--output-dir=".length);
3713
+ continue;
3714
+ }
3715
+ if (command === "report") {
3716
+ if (argument === "--reporter") {
3717
+ const { value, nextIndex } = takeFlagValue(rest, index, argument);
3718
+ reporters = parseCommaSeparated(value);
3719
+ index = nextIndex;
3720
+ continue;
3721
+ }
3722
+ if (argument.startsWith("--reporter=")) {
3723
+ reporters = parseCommaSeparated(argument.slice("--reporter=".length));
3724
+ continue;
3725
+ }
3726
+ }
3727
+ if (command === "docs") {
3728
+ if (argument === "--format") {
3729
+ const { value, nextIndex } = takeFlagValue(rest, index, argument);
3730
+ if (value !== "markdown" && value !== "mdx") {
3731
+ throw new Error(`Unsupported docs format "${value}".`);
3732
+ }
3733
+ format = value;
3734
+ index = nextIndex;
3735
+ continue;
3736
+ }
3737
+ if (argument.startsWith("--format=")) {
3738
+ const value = argument.slice("--format=".length);
3739
+ if (value !== "markdown" && value !== "mdx") {
3740
+ throw new Error(`Unsupported docs format "${value}".`);
3741
+ }
3742
+ format = value;
3743
+ continue;
3744
+ }
3745
+ if (argument === "--filter") {
3746
+ const { value, nextIndex } = takeFlagValue(rest, index, argument);
3747
+ filterTags = parseCommaSeparated(value);
3748
+ index = nextIndex;
3749
+ continue;
3750
+ }
3751
+ if (argument.startsWith("--filter=")) {
3752
+ filterTags = parseCommaSeparated(argument.slice("--filter=".length));
3753
+ continue;
3754
+ }
3755
+ }
3756
+ throw new Error(`Unknown argument "${argument}".`);
3757
+ }
3758
+ if (command === "docs") {
3759
+ return {
3760
+ command,
3761
+ resultsDir,
3762
+ outputDir,
3763
+ format,
3764
+ filterTags
3765
+ };
3766
+ }
3767
+ return {
3768
+ command,
3769
+ resultsDir,
3770
+ outputDir,
3771
+ reporters
3772
+ };
3773
+ }
3774
+ function reporterConfig(reporters) {
3775
+ if (!reporters || reporters.length === 0) {
3776
+ return {};
3777
+ }
3778
+ const selected = new Set(reporters);
3779
+ return {
3780
+ reporters: {
3781
+ html: selected.has("html"),
3782
+ markdown: selected.has("markdown"),
3783
+ mdx: selected.has("mdx"),
3784
+ ...Object.fromEntries(reporters.map((name) => [name, true]))
3785
+ }
3786
+ };
3787
+ }
3788
+ function docsConfig(format, filterTags) {
3789
+ const reporterName = format === "mdx" ? "mdx" : "markdown";
3790
+ return {
3791
+ reporters: {
3792
+ html: false,
3793
+ markdown: false,
3794
+ mdx: false,
3795
+ [reporterName]: {
3796
+ ...filterTags.length > 0 ? { includeTags: filterTags } : {}
3797
+ }
3798
+ }
3799
+ };
3800
+ }
3801
+ async function runCli(argv2 = process.argv.slice(2), deps = {}) {
3802
+ const cwd = deps.cwd ?? process.cwd;
3803
+ const log = deps.log ?? console.log;
3804
+ const error = deps.error ?? console.error;
3805
+ const runReportersImpl = deps.runReportersImpl ?? runReporters;
3806
+ let parsed;
3807
+ try {
3808
+ parsed = parseCliArgs(argv2);
3809
+ } catch (caught) {
3810
+ error(`[playwright-checkpoint] ${caught instanceof Error ? caught.message : String(caught)}`);
3811
+ error("Run `playwright-checkpoint --help` for usage.");
3812
+ return 1;
3813
+ }
3814
+ if (parsed.command === "help") {
3815
+ printHelp(log);
3816
+ return 0;
3817
+ }
3818
+ const resultsDir = import_node_path5.default.resolve(cwd(), parsed.resultsDir);
3819
+ const outputDir = import_node_path5.default.resolve(cwd(), parsed.outputDir);
3820
+ try {
3821
+ const config = parsed.command === "docs" ? docsConfig(parsed.format, parsed.filterTags) : reporterConfig(parsed.reporters);
3822
+ const results = await runReportersImpl(config, resultsDir, outputDir);
3823
+ const summaryLines = Object.entries(results).map(([name, result]) => `- ${name}: ${result.summary}`);
3824
+ log(
3825
+ parsed.command === "docs" ? `[playwright-checkpoint] Generated ${parsed.format.toUpperCase()} docs from ${resultsDir} to ${outputDir}` : `[playwright-checkpoint] Generated reports from ${resultsDir} to ${outputDir}`
3826
+ );
3827
+ if (summaryLines.length > 0) {
3828
+ for (const line of summaryLines) {
3829
+ log(line);
3830
+ }
3831
+ } else {
3832
+ log("- No reporters were enabled.");
3833
+ }
3834
+ return 0;
3835
+ } catch (caught) {
3836
+ error("[playwright-checkpoint] Failed to generate reports.");
3837
+ error(caught instanceof Error ? caught.stack ?? caught.message : String(caught));
3838
+ return 1;
3839
+ }
3840
+ }
3841
+
3842
+ // src/cli/mcp-args.ts
3843
+ function parseMcpCliArgs(argv2) {
3844
+ const flags = {};
3845
+ const passthroughArgs = [];
3846
+ let doubleDashSeen = false;
3847
+ for (let i = 0; i < argv2.length; i++) {
3848
+ const arg = argv2[i] ?? "";
3849
+ if (doubleDashSeen) {
3850
+ passthroughArgs.push(arg);
3851
+ continue;
3852
+ }
3853
+ if (arg === "--") {
3854
+ doubleDashSeen = true;
3855
+ continue;
3856
+ }
3857
+ if (arg === "--upstream") {
3858
+ const value = argv2[i + 1];
3859
+ if (!value || value.startsWith("-")) {
3860
+ throw new Error("--upstream requires a package name argument.");
3861
+ }
3862
+ flags.upstream = value;
3863
+ i += 1;
3864
+ continue;
3865
+ }
3866
+ if (arg.startsWith("--upstream=")) {
3867
+ flags.upstream = arg.slice("--upstream=".length);
3868
+ continue;
3869
+ }
3870
+ if (arg === "--standalone") {
3871
+ flags.standalone = true;
3872
+ continue;
3873
+ }
3874
+ if (arg === "--cdp-endpoint") {
3875
+ const value = argv2[i + 1];
3876
+ if (!value || value.startsWith("-")) {
3877
+ throw new Error("--cdp-endpoint requires a URL argument.");
3878
+ }
3879
+ flags.cdpEndpoint = value;
3880
+ i += 1;
3881
+ continue;
3882
+ }
3883
+ if (arg.startsWith("--cdp-endpoint=")) {
3884
+ flags.cdpEndpoint = arg.slice("--cdp-endpoint=".length);
3885
+ continue;
3886
+ }
3887
+ if (arg === "--output-dir") {
3888
+ const value = argv2[i + 1];
3889
+ if (!value || value.startsWith("-")) {
3890
+ throw new Error("--output-dir requires a path argument.");
3891
+ }
3892
+ flags.outputDir = value;
3893
+ i += 1;
3894
+ continue;
3895
+ }
3896
+ if (arg.startsWith("--output-dir=")) {
3897
+ flags.outputDir = arg.slice("--output-dir=".length);
3898
+ continue;
3899
+ }
3900
+ if (arg === "--help" || arg === "-h") {
3901
+ flags.upstream = "__help__";
3902
+ return { flags, passthroughArgs: [] };
3903
+ }
3904
+ passthroughArgs.push(arg, ...argv2.slice(i + 1));
3905
+ break;
3906
+ }
3907
+ return { flags, passthroughArgs };
3908
+ }
3909
+ function printMcpHelp(log) {
3910
+ log(`playwright-checkpoint mcp
3911
+
3912
+ Start the playwright-checkpoint MCP proxy server.
3913
+
3914
+ Usage:
3915
+ playwright-checkpoint mcp [options] [-- <upstream-args>...]
3916
+
3917
+ Options:
3918
+ --upstream <pkg> Upstream MCP package to proxy (default: auto-detect)
3919
+ --standalone Run without an upstream MCP server
3920
+ --cdp-endpoint <url> Connect directly to a browser CDP endpoint
3921
+ --output-dir <path> Directory for checkpoint output (default: ./checkpoints)
3922
+ -h, --help Show this help text
3923
+
3924
+ All arguments after -- are passed through to the upstream MCP server.
3925
+
3926
+ Examples:
3927
+ # Auto-detect @playwright/mcp from node_modules
3928
+ playwright-checkpoint mcp
3929
+
3930
+ # Explicit upstream package
3931
+ playwright-checkpoint mcp --upstream @playwright/mcp
3932
+
3933
+ # Pass headless args to upstream
3934
+ playwright-checkpoint mcp -- --headless --browser chrome
3935
+
3936
+ # Standalone mode (no upstream, connect to existing browser)
3937
+ playwright-checkpoint mcp --standalone --cdp-endpoint http://localhost:9222
3938
+ `);
3939
+ }
3940
+
3941
+ // src/cli/bin.ts
3942
+ var argv = process.argv.slice(2);
3943
+ if (argv[0] === "mcp") {
3944
+ const { passthroughArgs, flags } = parseMcpCliArgs(argv.slice(1));
3945
+ if (flags.upstream === "__help__") {
3946
+ printMcpHelp(console.log);
3947
+ process.exit(0);
3948
+ }
3949
+ void (async () => {
3950
+ try {
3951
+ const { startMcpProxy: startMcpProxy2 } = await Promise.resolve().then(() => (init_mcp(), mcp_exports));
3952
+ await startMcpProxy2({
3953
+ upstream: flags.upstream,
3954
+ standalone: flags.standalone,
3955
+ cdpEndpoint: flags.cdpEndpoint,
3956
+ outputDir: flags.outputDir,
3957
+ passthrough: passthroughArgs
3958
+ });
3959
+ } catch (err) {
3960
+ console.error(
3961
+ "[playwright-checkpoint MCP]",
3962
+ err instanceof Error ? err.message : String(err)
3963
+ );
3964
+ process.exit(1);
3965
+ }
3966
+ })();
3967
+ } else {
3968
+ void runCli(argv).then((code) => {
3969
+ process.exitCode = code;
3970
+ });
3971
+ }
3972
+ //# sourceMappingURL=bin.cjs.map