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,1549 @@
1
+ // src/core.ts
2
+ import fs12 from "fs/promises";
3
+ import path13 from "path";
4
+
5
+ // src/collectors/aria-snapshot.ts
6
+ import fs from "fs/promises";
7
+ import path from "path";
8
+ function countSnapshotNodes(value) {
9
+ if (value == null) {
10
+ return 0;
11
+ }
12
+ if (Array.isArray(value)) {
13
+ return value.reduce((total, item) => total + countSnapshotNodes(item), 0);
14
+ }
15
+ if (typeof value !== "object") {
16
+ return 1;
17
+ }
18
+ const node = value;
19
+ const children = Array.isArray(node.children) ? node.children : [];
20
+ return 1 + children.reduce((total, child) => total + countSnapshotNodes(child), 0);
21
+ }
22
+ async function captureAriaSnapshot(page) {
23
+ try {
24
+ const root = page.locator(":root");
25
+ if (typeof root.ariaSnapshot === "function") {
26
+ const snapshot = await root.ariaSnapshot();
27
+ return snapshot ?? null;
28
+ }
29
+ } catch {
30
+ }
31
+ if (typeof page.accessibility?.snapshot === "function") {
32
+ try {
33
+ const snapshot = await page.accessibility.snapshot({ interestingOnly: false });
34
+ return snapshot ?? null;
35
+ } catch {
36
+ return null;
37
+ }
38
+ }
39
+ return null;
40
+ }
41
+ var ariaSnapshotCollector = {
42
+ name: "aria-snapshot",
43
+ defaultEnabled: false,
44
+ async collect(ctx) {
45
+ const snapshot = await captureAriaSnapshot(ctx.page);
46
+ const nodeCount = countSnapshotNodes(snapshot);
47
+ const outputPath = path.join(ctx.checkpointDir, "aria-snapshot.json");
48
+ const data = {
49
+ snapshot,
50
+ nodeCount
51
+ };
52
+ await fs.writeFile(outputPath, `${JSON.stringify(data, null, 2)}
53
+ `, "utf8");
54
+ return {
55
+ data,
56
+ artifacts: [
57
+ {
58
+ name: "aria-snapshot",
59
+ path: outputPath,
60
+ contentType: "application/json"
61
+ }
62
+ ],
63
+ summary: {
64
+ nodeCount
65
+ }
66
+ };
67
+ }
68
+ };
69
+
70
+ // src/collectors/axe.ts
71
+ import fs2 from "fs/promises";
72
+ import path2 from "path";
73
+
74
+ // src/page-utils.ts
75
+ async function settlePage(page) {
76
+ await page.waitForLoadState("domcontentloaded").catch(() => void 0);
77
+ await page.waitForLoadState("load", { timeout: 3e3 }).catch(() => void 0);
78
+ }
79
+
80
+ // src/collectors/axe.ts
81
+ var axeLoader = () => import("@axe-core/playwright");
82
+ var warnedAboutMissingAxe = false;
83
+ function warnOnce(message, error) {
84
+ if (warnedAboutMissingAxe) {
85
+ return;
86
+ }
87
+ warnedAboutMissingAxe = true;
88
+ if (error instanceof Error) {
89
+ console.warn(`[playwright-checkpoint] ${message}`, error);
90
+ return;
91
+ }
92
+ if (error !== void 0) {
93
+ console.warn(`[playwright-checkpoint] ${message}`, String(error));
94
+ return;
95
+ }
96
+ console.warn(`[playwright-checkpoint] ${message}`);
97
+ }
98
+ function resolveAxeBuilder(module) {
99
+ return module.default ?? module.AxeBuilder ?? null;
100
+ }
101
+ async function analyzeAccessibility(page, AxeBuilder) {
102
+ try {
103
+ await settlePage(page);
104
+ return await new AxeBuilder({ page }).analyze();
105
+ } catch {
106
+ await page.waitForTimeout(500);
107
+ await settlePage(page);
108
+ return await new AxeBuilder({ page }).analyze();
109
+ }
110
+ }
111
+ function skippedAxeResult(reason) {
112
+ return {
113
+ data: {
114
+ skipped: true,
115
+ reason,
116
+ violations: 0,
117
+ results: null
118
+ },
119
+ artifacts: [],
120
+ summary: {
121
+ violations: 0
122
+ }
123
+ };
124
+ }
125
+ function setAxeLoaderForTests(loader) {
126
+ axeLoader = loader ?? (() => import("@axe-core/playwright"));
127
+ warnedAboutMissingAxe = false;
128
+ }
129
+ var axeCollector = {
130
+ name: "axe",
131
+ defaultEnabled: true,
132
+ async collect(ctx) {
133
+ const timeoutBudgetMs = typeof ctx.config.timeoutMs === "number" ? ctx.config.timeoutMs : 5e3;
134
+ if (timeoutBudgetMs > 0) {
135
+ if (typeof ctx.adjustTimeout === "function") {
136
+ ctx.adjustTimeout(timeoutBudgetMs);
137
+ } else if (ctx.testInfo && typeof ctx.testInfo.setTimeout === "function") {
138
+ ctx.testInfo.setTimeout(ctx.testInfo.timeout + timeoutBudgetMs);
139
+ }
140
+ }
141
+ let module;
142
+ try {
143
+ module = await axeLoader();
144
+ } catch (error) {
145
+ warnOnce("Skipping axe collector because @axe-core/playwright is unavailable.", error);
146
+ return skippedAxeResult("@axe-core/playwright is unavailable");
147
+ }
148
+ const AxeBuilder = resolveAxeBuilder(module);
149
+ if (!AxeBuilder) {
150
+ warnOnce("Skipping axe collector because @axe-core/playwright did not expose an AxeBuilder export.");
151
+ return skippedAxeResult("@axe-core/playwright did not expose AxeBuilder");
152
+ }
153
+ const results = await analyzeAccessibility(ctx.page, AxeBuilder);
154
+ const violations = results && typeof results === "object" && Array.isArray(results.violations) ? results.violations.length : 0;
155
+ const axePath = path2.join(ctx.checkpointDir, "axe.json");
156
+ await fs2.writeFile(axePath, `${JSON.stringify(results, null, 2)}
157
+ `, "utf8");
158
+ return {
159
+ data: {
160
+ skipped: false,
161
+ reason: null,
162
+ violations,
163
+ results
164
+ },
165
+ artifacts: [
166
+ {
167
+ name: "axe",
168
+ path: axePath,
169
+ contentType: "application/json"
170
+ }
171
+ ],
172
+ summary: {
173
+ violations
174
+ }
175
+ };
176
+ }
177
+ };
178
+
179
+ // src/collectors/console.ts
180
+ import fs3 from "fs/promises";
181
+ import path3 from "path";
182
+ var consoleStates = /* @__PURE__ */ new WeakMap();
183
+ function getLocation(message) {
184
+ const location2 = message.location();
185
+ if (!location2.url && location2.lineNumber == null && location2.columnNumber == null) {
186
+ return null;
187
+ }
188
+ return {
189
+ ...location2.url ? { url: location2.url } : {},
190
+ ...location2.lineNumber == null ? {} : { lineNumber: location2.lineNumber },
191
+ ...location2.columnNumber == null ? {} : { columnNumber: location2.columnNumber }
192
+ };
193
+ }
194
+ var consoleCollector = {
195
+ name: "console",
196
+ defaultEnabled: true,
197
+ async setup({ page }) {
198
+ if (consoleStates.has(page)) {
199
+ return;
200
+ }
201
+ const entries = [];
202
+ const recordConsoleMessage = (message) => {
203
+ if (message.type() !== "error") {
204
+ return;
205
+ }
206
+ entries.push({
207
+ type: message.type(),
208
+ text: message.text(),
209
+ location: getLocation(message),
210
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
211
+ });
212
+ };
213
+ const recordPageError = (error) => {
214
+ entries.push({
215
+ type: "pageerror",
216
+ text: error.message,
217
+ location: null,
218
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
219
+ });
220
+ };
221
+ page.on("console", recordConsoleMessage);
222
+ page.on("pageerror", recordPageError);
223
+ consoleStates.set(page, {
224
+ entries,
225
+ offset: 0,
226
+ recordConsoleMessage,
227
+ recordPageError
228
+ });
229
+ },
230
+ async collect(ctx) {
231
+ const state = consoleStates.get(ctx.page);
232
+ const checkpointEntries = state ? state.entries.slice(state.offset) : [];
233
+ if (state) {
234
+ state.offset = state.entries.length;
235
+ }
236
+ const outputPath = path3.join(ctx.checkpointDir, "console-errors.json");
237
+ await fs3.writeFile(outputPath, `${JSON.stringify(checkpointEntries, null, 2)}
238
+ `, "utf8");
239
+ return {
240
+ data: checkpointEntries,
241
+ artifacts: [
242
+ {
243
+ name: "console-errors",
244
+ path: outputPath,
245
+ contentType: "application/json"
246
+ }
247
+ ],
248
+ summary: {
249
+ consoleErrorCount: checkpointEntries.length
250
+ }
251
+ };
252
+ },
253
+ async teardown({ page }) {
254
+ const state = consoleStates.get(page);
255
+ if (!state) {
256
+ return;
257
+ }
258
+ page.off("console", state.recordConsoleMessage);
259
+ page.off("pageerror", state.recordPageError);
260
+ consoleStates.delete(page);
261
+ }
262
+ };
263
+
264
+ // src/collectors/dom-stats.ts
265
+ import fs4 from "fs/promises";
266
+ import path4 from "path";
267
+ var domStatsCollector = {
268
+ name: "dom-stats",
269
+ defaultEnabled: false,
270
+ async collect(ctx) {
271
+ const stats = await ctx.page.evaluate(() => {
272
+ const allNodes = document.querySelectorAll("*");
273
+ const maxDepthFrom = (root) => {
274
+ if (!root) {
275
+ return 0;
276
+ }
277
+ let maxDepth = 1;
278
+ const queue = [{ node: root, depth: 1 }];
279
+ while (queue.length > 0) {
280
+ const current = queue.shift();
281
+ if (!current) {
282
+ continue;
283
+ }
284
+ maxDepth = Math.max(maxDepth, current.depth);
285
+ for (const child of Array.from(current.node.children)) {
286
+ queue.push({ node: child, depth: current.depth + 1 });
287
+ }
288
+ }
289
+ return maxDepth;
290
+ };
291
+ const maybeGetEventListeners = globalThis.getEventListeners;
292
+ let eventListenerCount = null;
293
+ if (typeof maybeGetEventListeners === "function") {
294
+ eventListenerCount = 0;
295
+ const targets = [window, document, ...Array.from(allNodes)];
296
+ for (const target of targets) {
297
+ try {
298
+ const listeners = maybeGetEventListeners(target) ?? {};
299
+ for (const entries of Object.values(listeners)) {
300
+ eventListenerCount += Array.isArray(entries) ? entries.length : 0;
301
+ }
302
+ } catch {
303
+ }
304
+ }
305
+ }
306
+ return {
307
+ nodeCount: allNodes.length,
308
+ maxDepth: maxDepthFrom(document.documentElement),
309
+ formCount: document.querySelectorAll("form").length,
310
+ imageCount: document.querySelectorAll("img").length,
311
+ scriptCount: document.querySelectorAll("script").length,
312
+ stylesheetCount: document.styleSheets.length,
313
+ eventListenerCount
314
+ };
315
+ });
316
+ const data = {
317
+ nodeCount: stats.nodeCount,
318
+ maxDepth: stats.maxDepth,
319
+ formCount: stats.formCount,
320
+ imageCount: stats.imageCount,
321
+ scriptCount: stats.scriptCount,
322
+ stylesheetCount: stats.stylesheetCount,
323
+ eventListenerCount: stats.eventListenerCount
324
+ };
325
+ const outputPath = path4.join(ctx.checkpointDir, "dom-stats.json");
326
+ await fs4.writeFile(outputPath, `${JSON.stringify(data, null, 2)}
327
+ `, "utf8");
328
+ return {
329
+ data,
330
+ artifacts: [
331
+ {
332
+ name: "dom-stats",
333
+ path: outputPath,
334
+ contentType: "application/json"
335
+ }
336
+ ],
337
+ summary: {
338
+ nodeCount: data.nodeCount,
339
+ maxDepth: data.maxDepth,
340
+ formCount: data.formCount,
341
+ imageCount: data.imageCount
342
+ }
343
+ };
344
+ }
345
+ };
346
+
347
+ // src/collectors/forms.ts
348
+ import fs5 from "fs/promises";
349
+ import path5 from "path";
350
+ var REDACTED = "[REDACTED]";
351
+ var DEFAULT_REDACT_PATTERNS = ["password", "token", "secret", "api[_-]?key", "authorization", "bearer"];
352
+ var EMAIL_LIKE_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
353
+ function toRegex(pattern) {
354
+ const trimmed = pattern.trim();
355
+ if (!trimmed) {
356
+ return null;
357
+ }
358
+ try {
359
+ return new RegExp(trimmed, "i");
360
+ } catch {
361
+ return null;
362
+ }
363
+ }
364
+ function redactionRegexes(ctx) {
365
+ const fromConfig = Array.isArray(ctx.config.redact) ? ctx.config.redact.filter((entry) => typeof entry === "string") : [];
366
+ return [...DEFAULT_REDACT_PATTERNS, ...ctx.redact, ...fromConfig].map((pattern) => toRegex(pattern)).filter((value) => value instanceof RegExp);
367
+ }
368
+ function shouldRedactText(value, regexes) {
369
+ if (EMAIL_LIKE_REGEX.test(value.trim())) {
370
+ return true;
371
+ }
372
+ return regexes.some((regex) => regex.test(value));
373
+ }
374
+ function fieldIdentifier(field) {
375
+ return [field.type, field.name, field.id, field.label, field.placeholder].filter((value) => !!value).join(" ");
376
+ }
377
+ function redactValue(value) {
378
+ if (value == null) {
379
+ return value;
380
+ }
381
+ if (Array.isArray(value)) {
382
+ return value.map(() => REDACTED);
383
+ }
384
+ return REDACTED;
385
+ }
386
+ function fieldNeedsRedaction(field, regexes) {
387
+ if (shouldRedactText(fieldIdentifier(field), regexes)) {
388
+ return true;
389
+ }
390
+ if (field.value == null) {
391
+ return false;
392
+ }
393
+ if (Array.isArray(field.value)) {
394
+ return field.value.some((entry) => shouldRedactText(entry, regexes));
395
+ }
396
+ return shouldRedactText(field.value, regexes);
397
+ }
398
+ var formsCollector = {
399
+ name: "forms",
400
+ defaultEnabled: false,
401
+ async collect(ctx) {
402
+ const rawFields = await ctx.page.evaluate(() => {
403
+ const elements = Array.from(document.querySelectorAll("input, select, textarea"));
404
+ const isVisible = (element) => {
405
+ if (!(element instanceof HTMLElement)) {
406
+ return false;
407
+ }
408
+ const inputType = element instanceof HTMLInputElement ? element.type.toLowerCase() : null;
409
+ if (inputType === "hidden") {
410
+ return false;
411
+ }
412
+ if (element.hasAttribute("hidden") || element.getAttribute("aria-hidden") === "true") {
413
+ return false;
414
+ }
415
+ const style = window.getComputedStyle(element);
416
+ if (style.display === "none" || style.visibility === "hidden" || Number(style.opacity) === 0) {
417
+ return false;
418
+ }
419
+ const rect = element.getBoundingClientRect();
420
+ return rect.width > 0 && rect.height > 0;
421
+ };
422
+ const readLabel = (element) => {
423
+ if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement) {
424
+ const fromLabels = element.labels && element.labels.length > 0 ? element.labels[0]?.textContent?.trim() : null;
425
+ if (fromLabels) {
426
+ return fromLabels;
427
+ }
428
+ }
429
+ return element.getAttribute("aria-label")?.trim() ?? null;
430
+ };
431
+ const readValue = (element) => {
432
+ if (element instanceof HTMLSelectElement) {
433
+ if (element.multiple) {
434
+ return {
435
+ value: Array.from(element.selectedOptions).map((option) => option.value),
436
+ checked: null,
437
+ type: "select-multiple"
438
+ };
439
+ }
440
+ return {
441
+ value: element.value,
442
+ checked: null,
443
+ type: "select-one"
444
+ };
445
+ }
446
+ if (element instanceof HTMLTextAreaElement) {
447
+ return {
448
+ value: element.value,
449
+ checked: null,
450
+ type: "textarea"
451
+ };
452
+ }
453
+ if (element instanceof HTMLInputElement) {
454
+ const inputType = element.type.toLowerCase();
455
+ if (inputType === "checkbox" || inputType === "radio") {
456
+ return {
457
+ value: element.checked ? element.value || "on" : null,
458
+ checked: element.checked,
459
+ type: inputType
460
+ };
461
+ }
462
+ if (inputType === "file") {
463
+ return {
464
+ value: element.files ? Array.from(element.files).map((file) => file.name) : [],
465
+ checked: null,
466
+ type: inputType
467
+ };
468
+ }
469
+ return {
470
+ value: element.value,
471
+ checked: null,
472
+ type: inputType || null
473
+ };
474
+ }
475
+ return {
476
+ value: null,
477
+ checked: null,
478
+ type: null
479
+ };
480
+ };
481
+ return elements.filter((element) => isVisible(element)).map((element) => {
482
+ const { value, checked, type } = readValue(element);
483
+ return {
484
+ tagName: element.tagName.toLowerCase(),
485
+ type,
486
+ name: element.getAttribute("name"),
487
+ id: element.getAttribute("id"),
488
+ label: readLabel(element),
489
+ placeholder: element.getAttribute("placeholder"),
490
+ value,
491
+ checked,
492
+ disabled: element.disabled,
493
+ required: element.required
494
+ };
495
+ });
496
+ });
497
+ const regexes = redactionRegexes({ redact: ctx.redact, config: ctx.config });
498
+ let redactedCount = 0;
499
+ const fields = rawFields.map((field) => {
500
+ const redacted = fieldNeedsRedaction(field, regexes);
501
+ if (redacted) {
502
+ redactedCount += 1;
503
+ }
504
+ return {
505
+ ...field,
506
+ redacted,
507
+ value: redacted ? redactValue(field.value) : field.value
508
+ };
509
+ });
510
+ const data = {
511
+ fieldCount: fields.length,
512
+ redactedCount,
513
+ fields
514
+ };
515
+ const outputPath = path5.join(ctx.checkpointDir, "form-state.json");
516
+ await fs5.writeFile(outputPath, `${JSON.stringify(data, null, 2)}
517
+ `, "utf8");
518
+ return {
519
+ data,
520
+ artifacts: [
521
+ {
522
+ name: "form-state",
523
+ path: outputPath,
524
+ contentType: "application/json"
525
+ }
526
+ ],
527
+ summary: {
528
+ fieldCount: data.fieldCount,
529
+ redactedCount: data.redactedCount
530
+ }
531
+ };
532
+ }
533
+ };
534
+
535
+ // src/collectors/html.ts
536
+ import fs6 from "fs/promises";
537
+ import path6 from "path";
538
+ async function readPageContent(page) {
539
+ try {
540
+ await settlePage(page);
541
+ return await page.content();
542
+ } catch {
543
+ await page.waitForTimeout(500);
544
+ await settlePage(page);
545
+ return await page.content();
546
+ }
547
+ }
548
+ var htmlCollector = {
549
+ name: "html",
550
+ defaultEnabled: true,
551
+ async collect(ctx) {
552
+ const htmlPath = path6.join(ctx.checkpointDir, "page.html");
553
+ const html = await readPageContent(ctx.page);
554
+ await fs6.writeFile(htmlPath, html, "utf8");
555
+ return {
556
+ data: {
557
+ contentLength: html.length
558
+ },
559
+ artifacts: [
560
+ {
561
+ name: "html",
562
+ path: htmlPath,
563
+ contentType: "text/html"
564
+ }
565
+ ],
566
+ summary: {
567
+ htmlPath: "page.html"
568
+ }
569
+ };
570
+ }
571
+ };
572
+
573
+ // src/collectors/metadata.ts
574
+ import fs7 from "fs/promises";
575
+ import path7 from "path";
576
+ function normalizeStructuredData(scriptContents) {
577
+ const values = [];
578
+ for (const content of scriptContents) {
579
+ const value = content?.trim();
580
+ if (!value) {
581
+ continue;
582
+ }
583
+ try {
584
+ values.push(JSON.parse(value));
585
+ } catch {
586
+ values.push({
587
+ parseError: "Invalid JSON-LD",
588
+ raw: value
589
+ });
590
+ }
591
+ }
592
+ return values;
593
+ }
594
+ var metadataCollector = {
595
+ name: "metadata",
596
+ defaultEnabled: true,
597
+ async collect(ctx) {
598
+ const metadata = await ctx.page.evaluate(() => {
599
+ const meta = (selector) => document.querySelector(selector)?.getAttribute("content") ?? null;
600
+ const canonicalLink = document.querySelector('link[rel="canonical"]');
601
+ const html = document.documentElement;
602
+ const structuredDataScripts = Array.from(document.querySelectorAll('script[type="application/ld+json"]')).map((script) => script.textContent ?? null);
603
+ return {
604
+ url: location.href,
605
+ title: document.title,
606
+ description: meta('meta[name="description"]'),
607
+ openGraph: {
608
+ title: meta('meta[property="og:title"]'),
609
+ description: meta('meta[property="og:description"]'),
610
+ image: meta('meta[property="og:image"]')
611
+ },
612
+ canonicalUrl: canonicalLink?.getAttribute("href") ?? null,
613
+ lang: html.getAttribute("lang"),
614
+ viewport: meta('meta[name="viewport"]'),
615
+ structuredDataScripts
616
+ };
617
+ });
618
+ const normalizedMetadata = {
619
+ url: metadata.url,
620
+ title: metadata.title,
621
+ description: metadata.description,
622
+ openGraph: metadata.openGraph,
623
+ canonicalUrl: metadata.canonicalUrl,
624
+ lang: metadata.lang,
625
+ viewport: metadata.viewport,
626
+ structuredData: normalizeStructuredData(metadata.structuredDataScripts)
627
+ };
628
+ const outputPath = path7.join(ctx.checkpointDir, "metadata.json");
629
+ await fs7.writeFile(outputPath, `${JSON.stringify(normalizedMetadata, null, 2)}
630
+ `, "utf8");
631
+ return {
632
+ data: normalizedMetadata,
633
+ artifacts: [
634
+ {
635
+ name: "metadata",
636
+ path: outputPath,
637
+ contentType: "application/json"
638
+ }
639
+ ],
640
+ summary: {
641
+ url: normalizedMetadata.url,
642
+ title: normalizedMetadata.title,
643
+ lang: normalizedMetadata.lang
644
+ }
645
+ };
646
+ }
647
+ };
648
+
649
+ // src/collectors/network.ts
650
+ import fs8 from "fs/promises";
651
+ import path8 from "path";
652
+ var networkStates = /* @__PURE__ */ new WeakMap();
653
+ var networkCollector = {
654
+ name: "network",
655
+ defaultEnabled: true,
656
+ async setup({ page }) {
657
+ if (networkStates.has(page)) {
658
+ return;
659
+ }
660
+ const entries = [];
661
+ const recordRequestFailure = (request) => {
662
+ entries.push({
663
+ kind: "requestfailed",
664
+ url: request.url(),
665
+ method: request.method(),
666
+ status: null,
667
+ statusText: null,
668
+ failureText: request.failure()?.errorText ?? null,
669
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
670
+ });
671
+ };
672
+ const recordHttpError = (response) => {
673
+ if (response.status() < 400) {
674
+ return;
675
+ }
676
+ entries.push({
677
+ kind: "http-error",
678
+ url: response.url(),
679
+ method: response.request().method(),
680
+ status: response.status(),
681
+ statusText: response.statusText(),
682
+ failureText: null,
683
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
684
+ });
685
+ };
686
+ page.on("requestfailed", recordRequestFailure);
687
+ page.on("response", recordHttpError);
688
+ networkStates.set(page, {
689
+ entries,
690
+ offset: 0,
691
+ recordRequestFailure,
692
+ recordHttpError
693
+ });
694
+ },
695
+ async collect(ctx) {
696
+ const state = networkStates.get(ctx.page);
697
+ const checkpointEntries = state ? state.entries.slice(state.offset) : [];
698
+ if (state) {
699
+ state.offset = state.entries.length;
700
+ }
701
+ const outputPath = path8.join(ctx.checkpointDir, "failed-requests.json");
702
+ await fs8.writeFile(outputPath, `${JSON.stringify(checkpointEntries, null, 2)}
703
+ `, "utf8");
704
+ return {
705
+ data: checkpointEntries,
706
+ artifacts: [
707
+ {
708
+ name: "failed-requests",
709
+ path: outputPath,
710
+ contentType: "application/json"
711
+ }
712
+ ],
713
+ summary: {
714
+ failedRequestCount: checkpointEntries.length
715
+ }
716
+ };
717
+ },
718
+ async teardown({ page }) {
719
+ const state = networkStates.get(page);
720
+ if (!state) {
721
+ return;
722
+ }
723
+ page.off("requestfailed", state.recordRequestFailure);
724
+ page.off("response", state.recordHttpError);
725
+ networkStates.delete(page);
726
+ }
727
+ };
728
+
729
+ // src/collectors/network-timing.ts
730
+ import fs9 from "fs/promises";
731
+ import path9 from "path";
732
+ var timingStates = /* @__PURE__ */ new WeakMap();
733
+ function maybeDuration(start, end) {
734
+ if (start <= 0 || end <= 0 || end < start) {
735
+ return null;
736
+ }
737
+ return end - start;
738
+ }
739
+ function toNetworkRecord(response, timing) {
740
+ return {
741
+ url: response.url,
742
+ status: response.status,
743
+ statusText: response.statusText,
744
+ resourceType: response.resourceType,
745
+ timestamp: response.timestamp,
746
+ durationMs: timing ? timing.duration : null,
747
+ transferSize: timing ? timing.transferSize : null,
748
+ encodedBodySize: timing ? timing.encodedBodySize : null,
749
+ decodedBodySize: timing ? timing.decodedBodySize : null,
750
+ nextHopProtocol: timing ? timing.nextHopProtocol || null : null,
751
+ timing: {
752
+ startTimeMs: timing ? timing.startTime : null,
753
+ redirectMs: timing ? maybeDuration(timing.redirectStart, timing.redirectEnd) : null,
754
+ dnsMs: timing ? maybeDuration(timing.domainLookupStart, timing.domainLookupEnd) : null,
755
+ connectMs: timing ? maybeDuration(timing.connectStart, timing.connectEnd) : null,
756
+ tlsMs: timing ? maybeDuration(timing.secureConnectionStart, timing.connectEnd) : null,
757
+ requestMs: timing ? maybeDuration(timing.requestStart, timing.responseStart) : null,
758
+ responseMs: timing ? maybeDuration(timing.responseStart, timing.responseEnd) : null
759
+ }
760
+ };
761
+ }
762
+ var networkTimingCollector = {
763
+ name: "network-timing",
764
+ defaultEnabled: false,
765
+ async setup({ page }) {
766
+ if (timingStates.has(page)) {
767
+ return;
768
+ }
769
+ const responses = [];
770
+ const recordResponse = (response) => {
771
+ responses.push({
772
+ url: response.url(),
773
+ status: response.status(),
774
+ statusText: response.statusText(),
775
+ resourceType: response.request().resourceType(),
776
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
777
+ });
778
+ };
779
+ page.on("response", recordResponse);
780
+ timingStates.set(page, {
781
+ responses,
782
+ responseOffset: 0,
783
+ resourceOffsetByUrl: /* @__PURE__ */ new Map(),
784
+ recordResponse
785
+ });
786
+ },
787
+ async collect(ctx) {
788
+ const state = timingStates.get(ctx.page);
789
+ const recentResponses = state ? state.responses.slice(state.responseOffset) : [];
790
+ if (state) {
791
+ state.responseOffset = state.responses.length;
792
+ }
793
+ const resourceTimings = await ctx.page.evaluate(() => {
794
+ const entries = performance.getEntriesByType("resource");
795
+ return entries.map((entry) => ({
796
+ name: entry.name,
797
+ duration: entry.duration,
798
+ transferSize: entry.transferSize,
799
+ encodedBodySize: entry.encodedBodySize,
800
+ decodedBodySize: entry.decodedBodySize,
801
+ nextHopProtocol: entry.nextHopProtocol,
802
+ startTime: entry.startTime,
803
+ redirectStart: entry.redirectStart,
804
+ redirectEnd: entry.redirectEnd,
805
+ domainLookupStart: entry.domainLookupStart,
806
+ domainLookupEnd: entry.domainLookupEnd,
807
+ connectStart: entry.connectStart,
808
+ connectEnd: entry.connectEnd,
809
+ secureConnectionStart: entry.secureConnectionStart,
810
+ requestStart: entry.requestStart,
811
+ responseStart: entry.responseStart,
812
+ responseEnd: entry.responseEnd
813
+ }));
814
+ });
815
+ const timingsByUrl = /* @__PURE__ */ new Map();
816
+ for (const timing of resourceTimings) {
817
+ const list = timingsByUrl.get(timing.name);
818
+ if (list) {
819
+ list.push(timing);
820
+ } else {
821
+ timingsByUrl.set(timing.name, [timing]);
822
+ }
823
+ }
824
+ const requests = recentResponses.map((response) => {
825
+ if (!state) {
826
+ return toNetworkRecord(response, null);
827
+ }
828
+ const list = timingsByUrl.get(response.url) ?? [];
829
+ const currentOffset = state.resourceOffsetByUrl.get(response.url) ?? 0;
830
+ const match = list[currentOffset] ?? null;
831
+ if (match) {
832
+ state.resourceOffsetByUrl.set(response.url, currentOffset + 1);
833
+ }
834
+ return toNetworkRecord(response, match);
835
+ });
836
+ const totalBytes = requests.reduce((total, request) => {
837
+ if (typeof request.transferSize !== "number" || request.transferSize < 0) {
838
+ return total;
839
+ }
840
+ return total + request.transferSize;
841
+ }, 0);
842
+ const slowestRequestMs = requests.reduce((slowest, request) => {
843
+ if (typeof request.durationMs !== "number") {
844
+ return slowest;
845
+ }
846
+ return Math.max(slowest, request.durationMs);
847
+ }, 0);
848
+ const data = {
849
+ requestCount: requests.length,
850
+ totalBytes,
851
+ slowestRequestMs,
852
+ requests
853
+ };
854
+ const outputPath = path9.join(ctx.checkpointDir, "network-timing.json");
855
+ await fs9.writeFile(outputPath, `${JSON.stringify(data, null, 2)}
856
+ `, "utf8");
857
+ return {
858
+ data,
859
+ artifacts: [
860
+ {
861
+ name: "network-timing",
862
+ path: outputPath,
863
+ contentType: "application/json"
864
+ }
865
+ ],
866
+ summary: {
867
+ requestCount: data.requestCount,
868
+ totalBytes: data.totalBytes,
869
+ slowestRequestMs: data.slowestRequestMs
870
+ }
871
+ };
872
+ },
873
+ async teardown({ page }) {
874
+ const state = timingStates.get(page);
875
+ if (!state) {
876
+ return;
877
+ }
878
+ page.off("response", state.recordResponse);
879
+ timingStates.delete(page);
880
+ }
881
+ };
882
+
883
+ // src/collectors/screenshot.ts
884
+ import path10 from "path";
885
+ var PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
886
+ function readPngSize(buffer) {
887
+ if (buffer.length < 24 || !buffer.subarray(0, 8).equals(PNG_SIGNATURE)) {
888
+ return null;
889
+ }
890
+ if (buffer.toString("ascii", 12, 16) !== "IHDR") {
891
+ return null;
892
+ }
893
+ return {
894
+ width: buffer.readUInt32BE(16),
895
+ height: buffer.readUInt32BE(20)
896
+ };
897
+ }
898
+ var screenshotCollector = {
899
+ name: "screenshot",
900
+ defaultEnabled: true,
901
+ async collect(ctx) {
902
+ const fullPage = ctx.options.fullPage ?? true;
903
+ const screenshotPath = path10.join(ctx.checkpointDir, "page.png");
904
+ const screenshotBuffer = await ctx.page.screenshot({ path: screenshotPath, fullPage });
905
+ let highlightBounds = null;
906
+ if (ctx.options.highlightSelector) {
907
+ highlightBounds = await ctx.page.locator(ctx.options.highlightSelector).boundingBox().catch(() => null);
908
+ }
909
+ return {
910
+ data: {
911
+ fullPage,
912
+ highlightBounds,
913
+ highlightSelector: ctx.options.highlightSelector ?? null,
914
+ imageSize: Buffer.isBuffer(screenshotBuffer) ? readPngSize(screenshotBuffer) : null
915
+ },
916
+ artifacts: [
917
+ {
918
+ name: "screenshot",
919
+ path: screenshotPath,
920
+ contentType: "image/png"
921
+ }
922
+ ],
923
+ summary: {
924
+ screenshotPath: "page.png"
925
+ }
926
+ };
927
+ }
928
+ };
929
+
930
+ // src/collectors/storage.ts
931
+ import fs10 from "fs/promises";
932
+ import path11 from "path";
933
+ var REDACTED2 = "[REDACTED]";
934
+ var DEFAULT_REDACT_PATTERNS2 = ["password", "token", "secret", "api[_-]?key", "authorization", "session", "email"];
935
+ var EMAIL_LIKE_REGEX2 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
936
+ function toRegex2(pattern) {
937
+ const trimmed = pattern.trim();
938
+ if (!trimmed) {
939
+ return null;
940
+ }
941
+ try {
942
+ return new RegExp(trimmed, "i");
943
+ } catch {
944
+ return null;
945
+ }
946
+ }
947
+ function buildRedactionRegexes(ctx) {
948
+ const fromConfig = Array.isArray(ctx.config.redact) ? ctx.config.redact.filter((entry) => typeof entry === "string") : [];
949
+ return [...DEFAULT_REDACT_PATTERNS2, ...ctx.redact, ...fromConfig].map((pattern) => toRegex2(pattern)).filter((value) => value instanceof RegExp);
950
+ }
951
+ function shouldRedact(identifier, value, regexes) {
952
+ if (regexes.some((regex) => regex.test(identifier))) {
953
+ return true;
954
+ }
955
+ if (!value) {
956
+ return false;
957
+ }
958
+ if (EMAIL_LIKE_REGEX2.test(value.trim())) {
959
+ return true;
960
+ }
961
+ return regexes.some((regex) => regex.test(value));
962
+ }
963
+ var storageCollector = {
964
+ name: "storage",
965
+ defaultEnabled: false,
966
+ async collect(ctx) {
967
+ const includeCookieValues = ctx.config.includeCookieValues === true;
968
+ const includeLocalStorageValues = ctx.config.includeLocalStorageValues === true;
969
+ const redactValues = ctx.config.redactValues !== false;
970
+ const regexes = buildRedactionRegexes({ redact: ctx.redact, config: ctx.config });
971
+ const cookies = await ctx.page.context().cookies();
972
+ const localStorageEntries = await ctx.page.evaluate(
973
+ () => Object.keys(localStorage).map((key) => ({
974
+ key,
975
+ value: localStorage.getItem(key) ?? ""
976
+ }))
977
+ );
978
+ const normalizedCookies = cookies.map((cookie) => {
979
+ const rawValue = includeCookieValues ? cookie.value : null;
980
+ const redacted = redactValues && shouldRedact(cookie.name, rawValue, regexes);
981
+ return {
982
+ name: cookie.name,
983
+ domain: cookie.domain,
984
+ path: cookie.path,
985
+ value: rawValue == null ? null : redacted ? REDACTED2 : rawValue,
986
+ redacted,
987
+ expires: cookie.expires,
988
+ httpOnly: cookie.httpOnly,
989
+ secure: cookie.secure,
990
+ sameSite: cookie.sameSite
991
+ };
992
+ });
993
+ const normalizedLocalStorage = localStorageEntries.map((entry) => {
994
+ const rawValue = includeLocalStorageValues ? entry.value : null;
995
+ const redacted = redactValues && shouldRedact(entry.key, rawValue, regexes);
996
+ return {
997
+ key: entry.key,
998
+ value: rawValue == null ? null : redacted ? REDACTED2 : rawValue,
999
+ redacted
1000
+ };
1001
+ });
1002
+ const data = {
1003
+ cookieCount: normalizedCookies.length,
1004
+ localStorageKeyCount: normalizedLocalStorage.length,
1005
+ cookies: normalizedCookies,
1006
+ localStorage: normalizedLocalStorage
1007
+ };
1008
+ const outputPath = path11.join(ctx.checkpointDir, "storage-state.json");
1009
+ await fs10.writeFile(outputPath, `${JSON.stringify(data, null, 2)}
1010
+ `, "utf8");
1011
+ return {
1012
+ data,
1013
+ artifacts: [
1014
+ {
1015
+ name: "storage-state",
1016
+ path: outputPath,
1017
+ contentType: "application/json"
1018
+ }
1019
+ ],
1020
+ summary: {
1021
+ cookieCount: data.cookieCount,
1022
+ localStorageKeyCount: data.localStorageKeyCount
1023
+ }
1024
+ };
1025
+ }
1026
+ };
1027
+
1028
+ // src/collectors/web-vitals.ts
1029
+ import fs11 from "fs/promises";
1030
+ import path12 from "path";
1031
+ var initializedPages = /* @__PURE__ */ new WeakSet();
1032
+ function rateMetric(value, thresholds) {
1033
+ if (value == null || Number.isNaN(value)) {
1034
+ return "unknown";
1035
+ }
1036
+ if (value <= thresholds.good) {
1037
+ return "good";
1038
+ }
1039
+ if (value <= thresholds.needsImprovement) {
1040
+ return "needs-improvement";
1041
+ }
1042
+ return "poor";
1043
+ }
1044
+ function metric(value, thresholds) {
1045
+ return {
1046
+ value,
1047
+ rating: rateMetric(value, thresholds)
1048
+ };
1049
+ }
1050
+ async function captureWebVitals(page) {
1051
+ const raw = await page.evaluate(() => {
1052
+ const globalState = globalThis;
1053
+ const state = globalState.__e2eWebVitals ?? {
1054
+ cls: 0,
1055
+ fcp: null,
1056
+ lcp: null,
1057
+ inp: null
1058
+ };
1059
+ const navigation = performance.getEntriesByType("navigation")[0];
1060
+ return {
1061
+ cls: state.cls,
1062
+ fcp: state.fcp,
1063
+ lcp: state.lcp,
1064
+ inp: state.inp,
1065
+ ttfb: navigation ? navigation.responseStart : null,
1066
+ domContentLoaded: navigation ? navigation.domContentLoadedEventEnd : null,
1067
+ loadEvent: navigation ? navigation.loadEventEnd : null,
1068
+ url: location.href
1069
+ };
1070
+ });
1071
+ return {
1072
+ url: raw.url,
1073
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
1074
+ cls: metric(raw.cls, { good: 0.1, needsImprovement: 0.25 }),
1075
+ fcpMs: metric(raw.fcp, { good: 1800, needsImprovement: 3e3 }),
1076
+ lcpMs: metric(raw.lcp, { good: 2500, needsImprovement: 4e3 }),
1077
+ inpMs: metric(raw.inp, { good: 200, needsImprovement: 500 }),
1078
+ ttfbMs: metric(raw.ttfb, { good: 800, needsImprovement: 1800 }),
1079
+ domContentLoadedMs: raw.domContentLoaded,
1080
+ loadEventMs: raw.loadEvent
1081
+ };
1082
+ }
1083
+ var webVitalsCollector = {
1084
+ name: "web-vitals",
1085
+ defaultEnabled: true,
1086
+ async setup({ page }) {
1087
+ if (initializedPages.has(page)) {
1088
+ return;
1089
+ }
1090
+ initializedPages.add(page);
1091
+ await page.addInitScript(() => {
1092
+ const globalState = globalThis;
1093
+ if (!globalState.__e2eWebVitals) {
1094
+ globalState.__e2eWebVitals = {
1095
+ cls: 0,
1096
+ fcp: null,
1097
+ lcp: null,
1098
+ inp: null
1099
+ };
1100
+ }
1101
+ const state = globalState.__e2eWebVitals;
1102
+ try {
1103
+ const paintObserver = new PerformanceObserver((entryList) => {
1104
+ for (const entry of entryList.getEntries()) {
1105
+ if (entry.name === "first-contentful-paint") {
1106
+ state.fcp = entry.startTime;
1107
+ }
1108
+ }
1109
+ });
1110
+ paintObserver.observe({ type: "paint", buffered: true });
1111
+ } catch {
1112
+ }
1113
+ try {
1114
+ const lcpObserver = new PerformanceObserver((entryList) => {
1115
+ const entries = entryList.getEntries();
1116
+ const lastEntry = entries[entries.length - 1];
1117
+ if (lastEntry) {
1118
+ state.lcp = lastEntry.startTime;
1119
+ }
1120
+ });
1121
+ lcpObserver.observe({ type: "largest-contentful-paint", buffered: true });
1122
+ addEventListener("pagehide", () => lcpObserver.disconnect(), { once: true });
1123
+ } catch {
1124
+ }
1125
+ try {
1126
+ const clsObserver = new PerformanceObserver((entryList) => {
1127
+ for (const entry of entryList.getEntries()) {
1128
+ if (!entry.hadRecentInput) {
1129
+ state.cls += entry.value ?? 0;
1130
+ }
1131
+ }
1132
+ });
1133
+ clsObserver.observe({ type: "layout-shift", buffered: true });
1134
+ addEventListener("pagehide", () => clsObserver.disconnect(), { once: true });
1135
+ } catch {
1136
+ }
1137
+ try {
1138
+ const inpObserver = new PerformanceObserver((entryList) => {
1139
+ for (const entry of entryList.getEntries()) {
1140
+ const duration = entry.duration ?? 0;
1141
+ if (state.inp == null || duration > state.inp) {
1142
+ state.inp = duration;
1143
+ }
1144
+ }
1145
+ });
1146
+ inpObserver.observe({ type: "event", buffered: true, durationThreshold: 40 });
1147
+ addEventListener("pagehide", () => inpObserver.disconnect(), { once: true });
1148
+ } catch {
1149
+ }
1150
+ });
1151
+ },
1152
+ async collect(ctx) {
1153
+ const snapshot = await captureWebVitals(ctx.page);
1154
+ const outputPath = path12.join(ctx.checkpointDir, "web-vitals.json");
1155
+ await fs11.writeFile(outputPath, `${JSON.stringify(snapshot, null, 2)}
1156
+ `, "utf8");
1157
+ return {
1158
+ data: snapshot,
1159
+ artifacts: [
1160
+ {
1161
+ name: "web-vitals",
1162
+ path: outputPath,
1163
+ contentType: "application/json"
1164
+ }
1165
+ ],
1166
+ summary: {
1167
+ cls: snapshot.cls,
1168
+ fcp: snapshot.fcpMs,
1169
+ lcp: snapshot.lcpMs,
1170
+ inp: snapshot.inpMs,
1171
+ ttfb: snapshot.ttfbMs
1172
+ }
1173
+ };
1174
+ },
1175
+ async teardown({ page }) {
1176
+ initializedPages.delete(page);
1177
+ }
1178
+ };
1179
+
1180
+ // src/collectors/builtin-collectors.ts
1181
+ var builtinCollectors = [
1182
+ screenshotCollector,
1183
+ htmlCollector,
1184
+ axeCollector,
1185
+ webVitalsCollector,
1186
+ consoleCollector,
1187
+ networkCollector,
1188
+ metadataCollector,
1189
+ ariaSnapshotCollector,
1190
+ domStatsCollector,
1191
+ formsCollector,
1192
+ storageCollector,
1193
+ networkTimingCollector
1194
+ ];
1195
+
1196
+ // src/collectors/registry.ts
1197
+ var builtinCollectors2 = /* @__PURE__ */ new Map();
1198
+ var builtinsRegistered = false;
1199
+ function registerBuiltinCollector(collector) {
1200
+ builtinCollectors2.set(collector.name, collector);
1201
+ }
1202
+ function registerBuiltinCollectors(collectors) {
1203
+ if (builtinsRegistered) {
1204
+ return;
1205
+ }
1206
+ for (const collector of collectors) {
1207
+ registerBuiltinCollector(collector);
1208
+ }
1209
+ builtinsRegistered = true;
1210
+ }
1211
+ function getBuiltinCollectors() {
1212
+ return new Map(builtinCollectors2);
1213
+ }
1214
+
1215
+ // src/core.ts
1216
+ registerBuiltinCollectors(builtinCollectors);
1217
+ function cloneResolvedConfig(config) {
1218
+ return { ...config };
1219
+ }
1220
+ function cloneCollectorState(state) {
1221
+ return {
1222
+ enabled: state?.enabled ?? false,
1223
+ config: cloneResolvedConfig(state?.config ?? {})
1224
+ };
1225
+ }
1226
+ function applyCollectorInput(state, input) {
1227
+ const next = cloneCollectorState(state);
1228
+ if (input === void 0) {
1229
+ return next;
1230
+ }
1231
+ if (input === false) {
1232
+ return {
1233
+ enabled: false,
1234
+ config: {}
1235
+ };
1236
+ }
1237
+ if (input === true) {
1238
+ return {
1239
+ enabled: true,
1240
+ config: next.config
1241
+ };
1242
+ }
1243
+ return {
1244
+ enabled: true,
1245
+ config: {
1246
+ ...next.config,
1247
+ ...input
1248
+ }
1249
+ };
1250
+ }
1251
+ function collectorRegistryFor(config = {}) {
1252
+ const registry = getBuiltinCollectors();
1253
+ for (const collector of config.custom ?? []) {
1254
+ registry.set(collector.name, collector);
1255
+ }
1256
+ return registry;
1257
+ }
1258
+ function cloneCheckpointOptions(options) {
1259
+ return {
1260
+ ...options,
1261
+ ...options.collectors ? { collectors: { ...options.collectors } } : {}
1262
+ };
1263
+ }
1264
+ function defaultManifestEnvironment() {
1265
+ return process.env.PLAYWRIGHT_CHECKPOINT_ENV || process.env.NODE_ENV || "test";
1266
+ }
1267
+ function createManifest(sessionMetadata) {
1268
+ return {
1269
+ environment: sessionMetadata?.environment ?? defaultManifestEnvironment(),
1270
+ project: sessionMetadata?.project ?? "",
1271
+ testId: sessionMetadata?.testId ?? "",
1272
+ title: sessionMetadata?.title ?? "",
1273
+ tags: [...sessionMetadata?.tags ?? []],
1274
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
1275
+ checkpoints: []
1276
+ };
1277
+ }
1278
+ async function writeManifestFile(manifestPath, manifest) {
1279
+ await fs12.mkdir(path13.dirname(manifestPath), { recursive: true });
1280
+ await fs12.writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}
1281
+ `, "utf8");
1282
+ return manifestPath;
1283
+ }
1284
+ function warn(message, error) {
1285
+ if (error instanceof Error) {
1286
+ console.warn(`[playwright-checkpoint] ${message}`, error);
1287
+ return;
1288
+ }
1289
+ if (error !== void 0) {
1290
+ console.warn(`[playwright-checkpoint] ${message}`, String(error));
1291
+ return;
1292
+ }
1293
+ console.warn(`[playwright-checkpoint] ${message}`);
1294
+ }
1295
+ function sanitizeSegment(value) {
1296
+ return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "checkpoint";
1297
+ }
1298
+ function checkpointSlug(name, existing) {
1299
+ const base = sanitizeSegment(name);
1300
+ const existingSlugs = new Set(existing.map((record) => record.slug));
1301
+ if (!existingSlugs.has(base)) {
1302
+ return base;
1303
+ }
1304
+ let index = 2;
1305
+ let candidate = `${base}-${index}`;
1306
+ while (existingSlugs.has(candidate)) {
1307
+ index += 1;
1308
+ candidate = `${base}-${index}`;
1309
+ }
1310
+ return candidate;
1311
+ }
1312
+ async function attachArtifacts(testInfo, checkpointSlugValue, collectorName, artifacts) {
1313
+ const attach = testInfo?.attach;
1314
+ if (typeof attach !== "function") {
1315
+ return;
1316
+ }
1317
+ for (const artifact of artifacts) {
1318
+ try {
1319
+ await attach.call(testInfo, `${checkpointSlugValue}/${collectorName}/${artifact.name}`, {
1320
+ path: artifact.path,
1321
+ contentType: artifact.contentType
1322
+ });
1323
+ } catch (error) {
1324
+ warn(`Failed to attach artifact "${artifact.name}" from collector "${collectorName}".`, error);
1325
+ }
1326
+ }
1327
+ }
1328
+ async function collectPageTitle(page) {
1329
+ try {
1330
+ return await page.title();
1331
+ } catch {
1332
+ return "";
1333
+ }
1334
+ }
1335
+ async function runCollectorSetup(collectors, page, testInfo) {
1336
+ for (const collector of collectors) {
1337
+ if (!collector.setup) {
1338
+ continue;
1339
+ }
1340
+ try {
1341
+ await collector.setup({ page, testInfo });
1342
+ } catch (error) {
1343
+ warn(`Collector "${collector.name}" setup failed.`, error);
1344
+ }
1345
+ }
1346
+ }
1347
+ async function runCollectorTeardown(collectors, page, testInfo) {
1348
+ const collectorList = Array.from(collectors).reverse();
1349
+ for (const collector of collectorList) {
1350
+ if (!collector.teardown) {
1351
+ continue;
1352
+ }
1353
+ try {
1354
+ await collector.teardown({ page, testInfo });
1355
+ } catch (error) {
1356
+ warn(`Collector "${collector.name}" teardown failed.`, error);
1357
+ }
1358
+ }
1359
+ }
1360
+ function resolveCollectors(globalConfig = {}, testConfig = null, checkpointOptions = {}) {
1361
+ const registry = collectorRegistryFor(globalConfig);
1362
+ const states = /* @__PURE__ */ new Map();
1363
+ for (const collector of registry.values()) {
1364
+ states.set(collector.name, {
1365
+ enabled: collector.defaultEnabled,
1366
+ config: {}
1367
+ });
1368
+ }
1369
+ const levels = [globalConfig.collectors, testConfig?.collectors, checkpointOptions.collectors];
1370
+ for (const level of levels) {
1371
+ for (const [name, input] of Object.entries(level ?? {})) {
1372
+ states.set(name, applyCollectorInput(states.get(name), input));
1373
+ }
1374
+ }
1375
+ const resolved = /* @__PURE__ */ new Map();
1376
+ for (const [name, state] of states) {
1377
+ if (state.enabled) {
1378
+ resolved.set(name, cloneResolvedConfig(state.config));
1379
+ }
1380
+ }
1381
+ return resolved;
1382
+ }
1383
+ async function runCollectorPipeline(args) {
1384
+ const options = cloneCheckpointOptions(args.options ?? {});
1385
+ const slug = args.slug ?? checkpointSlug(args.name, args.manifest?.checkpoints ?? []);
1386
+ const checkpointDir = path13.join(args.outputDir, slug);
1387
+ const collectorResults = {};
1388
+ await fs12.mkdir(checkpointDir, { recursive: true });
1389
+ await settlePage(args.page);
1390
+ for (const [collectorName, collectorConfig] of args.resolvedCollectors) {
1391
+ const collector = args.registry.get(collectorName);
1392
+ if (!collector) {
1393
+ warn(`Collector "${collectorName}" is enabled but no implementation is registered.`);
1394
+ continue;
1395
+ }
1396
+ try {
1397
+ const result = await collector.collect({
1398
+ page: args.page,
1399
+ testInfo: args.testInfo,
1400
+ checkpointDir,
1401
+ checkpointName: args.name,
1402
+ checkpointSlug: slug,
1403
+ redact: [...args.redact ?? []],
1404
+ config: cloneResolvedConfig(collectorConfig),
1405
+ options,
1406
+ adjustTimeout: args.adjustTimeout
1407
+ });
1408
+ collectorResults[collectorName] = result;
1409
+ await attachArtifacts(args.testInfo, slug, collectorName, result.artifacts);
1410
+ } catch (error) {
1411
+ warn(`Collector "${collectorName}" failed during checkpoint "${args.name}".`, error);
1412
+ }
1413
+ }
1414
+ const record = {
1415
+ name: args.name,
1416
+ slug,
1417
+ url: args.page.url(),
1418
+ title: await collectPageTitle(args.page),
1419
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1420
+ ...options.description ? { description: options.description } : {},
1421
+ ...typeof options.step === "number" ? { step: options.step } : {},
1422
+ collectors: collectorResults
1423
+ };
1424
+ args.manifest?.checkpoints.push(record);
1425
+ return record;
1426
+ }
1427
+ async function captureCheckpoint(page, name, options) {
1428
+ const sessionConfig = {
1429
+ collectors: options.collectors,
1430
+ custom: options.custom,
1431
+ redact: options.redact
1432
+ };
1433
+ const registry = collectorRegistryFor(sessionConfig);
1434
+ const resolvedCollectors = resolveCollectors(sessionConfig, null, options);
1435
+ const enabledCollectors = Array.from(resolvedCollectors.keys()).map((collectorName) => registry.get(collectorName)).filter((collector) => Boolean(collector));
1436
+ await fs12.mkdir(options.outputDir, { recursive: true });
1437
+ await runCollectorSetup(enabledCollectors, page, options.testInfo);
1438
+ try {
1439
+ return await runCollectorPipeline({
1440
+ page,
1441
+ name,
1442
+ outputDir: options.outputDir,
1443
+ resolvedCollectors,
1444
+ registry,
1445
+ options,
1446
+ redact: options.redact,
1447
+ testInfo: options.testInfo,
1448
+ adjustTimeout: options.adjustTimeout,
1449
+ slug: checkpointSlug(name, [])
1450
+ });
1451
+ } finally {
1452
+ await runCollectorTeardown(enabledCollectors, page, options.testInfo);
1453
+ }
1454
+ }
1455
+ async function createCheckpointSession(page, options) {
1456
+ const sessionConfig = {
1457
+ collectors: options.collectors,
1458
+ custom: options.custom,
1459
+ redact: options.redact
1460
+ };
1461
+ const outputDir = options.outputDir;
1462
+ const registry = collectorRegistryFor(sessionConfig);
1463
+ const manifest = options.manifest ?? createManifest(options.sessionMetadata);
1464
+ const setupCollectorNames = /* @__PURE__ */ new Set();
1465
+ const setupCollectors = [];
1466
+ let finalizePromise = null;
1467
+ async function ensureCollectorsSetup(resolvedCollectors) {
1468
+ for (const collectorName of resolvedCollectors.keys()) {
1469
+ if (setupCollectorNames.has(collectorName)) {
1470
+ continue;
1471
+ }
1472
+ const collector = registry.get(collectorName);
1473
+ if (!collector) {
1474
+ warn(`Collector "${collectorName}" is enabled but no implementation is registered.`);
1475
+ continue;
1476
+ }
1477
+ setupCollectorNames.add(collectorName);
1478
+ setupCollectors.push(collector);
1479
+ await runCollectorSetup([collector], page, options.testInfo);
1480
+ }
1481
+ }
1482
+ await fs12.mkdir(outputDir, { recursive: true });
1483
+ await ensureCollectorsSetup(resolveCollectors(sessionConfig));
1484
+ return {
1485
+ outputDir,
1486
+ manifest,
1487
+ async checkpoint(name, checkpointOptions = {}) {
1488
+ if (finalizePromise) {
1489
+ throw new Error("Checkpoint session has already been finalized.");
1490
+ }
1491
+ const resolvedCollectors = resolveCollectors(sessionConfig, null, checkpointOptions);
1492
+ await ensureCollectorsSetup(resolvedCollectors);
1493
+ return runCollectorPipeline({
1494
+ page,
1495
+ name,
1496
+ outputDir,
1497
+ resolvedCollectors,
1498
+ registry,
1499
+ options: checkpointOptions,
1500
+ manifest,
1501
+ redact: options.redact,
1502
+ testInfo: options.testInfo,
1503
+ adjustTimeout: options.adjustTimeout
1504
+ });
1505
+ },
1506
+ finalize() {
1507
+ if (!finalizePromise) {
1508
+ finalizePromise = (async () => {
1509
+ await runCollectorTeardown(setupCollectors, page, options.testInfo);
1510
+ await writeManifestFile(options.manifestPath ?? path13.join(outputDir, "checkpoint-manifest.json"), manifest);
1511
+ return manifest;
1512
+ })();
1513
+ }
1514
+ return finalizePromise;
1515
+ }
1516
+ };
1517
+ }
1518
+
1519
+ export {
1520
+ ariaSnapshotCollector,
1521
+ settlePage,
1522
+ setAxeLoaderForTests,
1523
+ axeCollector,
1524
+ consoleCollector,
1525
+ domStatsCollector,
1526
+ formsCollector,
1527
+ htmlCollector,
1528
+ metadataCollector,
1529
+ networkCollector,
1530
+ networkTimingCollector,
1531
+ screenshotCollector,
1532
+ storageCollector,
1533
+ webVitalsCollector,
1534
+ builtinCollectors,
1535
+ registerBuiltinCollector,
1536
+ registerBuiltinCollectors,
1537
+ getBuiltinCollectors,
1538
+ warn,
1539
+ sanitizeSegment,
1540
+ checkpointSlug,
1541
+ collectPageTitle,
1542
+ runCollectorSetup,
1543
+ runCollectorTeardown,
1544
+ resolveCollectors,
1545
+ runCollectorPipeline,
1546
+ captureCheckpoint,
1547
+ createCheckpointSession
1548
+ };
1549
+ //# sourceMappingURL=chunk-KG37WSYS.js.map