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