playwright-checkpoint 0.1.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +665 -0
- package/dist/chunk-DGUM43GV.js +11 -0
- package/dist/chunk-DGUM43GV.js.map +1 -0
- package/dist/chunk-F5A6XGLJ.js +104 -0
- package/dist/chunk-F5A6XGLJ.js.map +1 -0
- package/dist/chunk-K5DX32TO.js +214 -0
- package/dist/chunk-K5DX32TO.js.map +1 -0
- package/dist/chunk-KG37WSYS.js +1549 -0
- package/dist/chunk-KG37WSYS.js.map +1 -0
- package/dist/chunk-X5IPL32H.js +1484 -0
- package/dist/chunk-X5IPL32H.js.map +1 -0
- package/dist/cli/bin.cjs +3972 -0
- package/dist/cli/bin.cjs.map +1 -0
- package/dist/cli/bin.d.cts +1 -0
- package/dist/cli/bin.d.ts +1 -0
- package/dist/cli/bin.js +43 -0
- package/dist/cli/bin.js.map +1 -0
- package/dist/cli/index.cjs +1672 -0
- package/dist/cli/index.cjs.map +1 -0
- package/dist/cli/index.d.cts +31 -0
- package/dist/cli/index.d.ts +31 -0
- package/dist/cli/index.js +17 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/mcp-args.cjs +129 -0
- package/dist/cli/mcp-args.cjs.map +1 -0
- package/dist/cli/mcp-args.d.cts +32 -0
- package/dist/cli/mcp-args.d.ts +32 -0
- package/dist/cli/mcp-args.js +10 -0
- package/dist/cli/mcp-args.js.map +1 -0
- package/dist/components.cjs +53 -0
- package/dist/components.cjs.map +1 -0
- package/dist/components.d.cts +27 -0
- package/dist/components.d.ts +27 -0
- package/dist/components.js +26 -0
- package/dist/components.js.map +1 -0
- package/dist/core-CD4jHGgI.d.cts +51 -0
- package/dist/core-CZvnc0rE.d.ts +51 -0
- package/dist/core.cjs +1576 -0
- package/dist/core.cjs.map +1 -0
- package/dist/core.d.cts +3 -0
- package/dist/core.d.ts +3 -0
- package/dist/core.js +32 -0
- package/dist/core.js.map +1 -0
- package/dist/index-BjYQX_hK.d.ts +8 -0
- package/dist/index-Cabk31qi.d.cts +8 -0
- package/dist/index.cjs +3318 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +94 -0
- package/dist/index.d.ts +94 -0
- package/dist/index.js +285 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/index.cjs +3467 -0
- package/dist/mcp/index.cjs.map +1 -0
- package/dist/mcp/index.d.cts +26 -0
- package/dist/mcp/index.d.ts +26 -0
- package/dist/mcp/index.js +586 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/teardown.cjs +1509 -0
- package/dist/teardown.cjs.map +1 -0
- package/dist/teardown.d.cts +5 -0
- package/dist/teardown.d.ts +5 -0
- package/dist/teardown.js +52 -0
- package/dist/teardown.js.map +1 -0
- package/dist/types-G7w4n8kR.d.cts +359 -0
- package/dist/types-G7w4n8kR.d.ts +359 -0
- package/package.json +109 -0
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
|