@crvy/rprtr 0.0.3 → 0.0.4

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.
@@ -0,0 +1,669 @@
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
+ !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/reporter.ts
31
+ var reporter_exports = {};
32
+ __export(reporter_exports, {
33
+ CrvyRprtr: () => CrvyRprtr,
34
+ default: () => reporter_default
35
+ });
36
+ module.exports = __toCommonJS(reporter_exports);
37
+ var import_promises2 = require("fs/promises");
38
+ var import_path2 = require("path");
39
+ var import_p_limit = __toESM(require("p-limit"), 1);
40
+
41
+ // src/report-artifact.ts
42
+ var import_promises = require("fs/promises");
43
+ var import_path = require("path");
44
+ var import_url = require("url");
45
+
46
+ // src/report-utils.ts
47
+ function normalizeScreenshotsBaseUrl(baseUrl) {
48
+ return baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
49
+ }
50
+ function attachmentsToImages(attachments, screenshotsBaseUrl = "/screenshots/") {
51
+ const images = {};
52
+ const baseUrl = normalizeScreenshotsBaseUrl(screenshotsBaseUrl);
53
+ for (const attachment of attachments) {
54
+ if (attachment.contentType !== "image/png") continue;
55
+ const match = attachment.name.match(/^(.+?)-(actual|expected|diff)(?:\.png)?$/);
56
+ if (match === null) continue;
57
+ const baseName = match[1];
58
+ const role = match[2];
59
+ if (baseName === null || baseName === void 0 || role === null || role === void 0) continue;
60
+ images[baseName] ??= { actual: "" };
61
+ const url = `${baseUrl}${attachment.path}`;
62
+ const img = images[baseName];
63
+ if (img !== null && img !== void 0) {
64
+ if (role === "actual") img.actual = url;
65
+ else if (role === "expected") img.expect = url;
66
+ else if (role === "diff") img.diff = url;
67
+ }
68
+ }
69
+ for (const key of Object.keys(images)) {
70
+ const img = images[key];
71
+ if (img?.actual !== null && img?.actual !== void 0 && img?.expect !== null && img?.expect !== void 0 && img?.diff === void 0)
72
+ delete img.expect;
73
+ }
74
+ return images;
75
+ }
76
+ function mapStatus(status) {
77
+ switch (status) {
78
+ case "passed":
79
+ return "success";
80
+ case "failed":
81
+ return "failed";
82
+ case "skipped":
83
+ return "pending";
84
+ default:
85
+ return "unknown";
86
+ }
87
+ }
88
+
89
+ // src/report-state.ts
90
+ function hasReviewablePassingImages(images) {
91
+ return Object.values(images).some(
92
+ (img) => img !== null && img !== void 0 && img.actual !== null && img.actual !== void 0 && img.diff === void 0
93
+ );
94
+ }
95
+ function preservePreviousPassingImages(test, status, images) {
96
+ if (status !== "passed" || Object.keys(images).length > 0) {
97
+ return images;
98
+ }
99
+ const previousImages = test.results?.[0]?.images ?? {};
100
+ return hasReviewablePassingImages(previousImages) ? previousImages : images;
101
+ }
102
+ function countDiffImages(images) {
103
+ return Object.values(images).filter((img) => img?.diff !== null && img?.diff !== void 0).length;
104
+ }
105
+ function createMutableReportState(screenshotDir = "./screenshots") {
106
+ return {
107
+ reportData: {
108
+ isRunning: false,
109
+ tests: {},
110
+ browsers: ["chromium"],
111
+ isUpdateMode: false,
112
+ screenshotDir
113
+ },
114
+ currentRunIds: /* @__PURE__ */ new Set()
115
+ };
116
+ }
117
+ function applyTestBeginEvent(state, data) {
118
+ const { id, title, titlePath, browser, location } = data;
119
+ state.currentRunIds.add(id);
120
+ state.reportData.tests[id] ??= {
121
+ id,
122
+ titlePath: titlePath ?? [],
123
+ browser: browser ?? "",
124
+ title: title ?? "",
125
+ location,
126
+ status: "running"
127
+ };
128
+ return state.reportData.tests[id];
129
+ }
130
+ function applyTestEndEvent(state, data, options = {}) {
131
+ const test = state.reportData.tests[data.id];
132
+ if (test === void 0) {
133
+ return null;
134
+ }
135
+ test.status = mapStatus(data.status);
136
+ const images = preservePreviousPassingImages(
137
+ test,
138
+ data.status,
139
+ attachmentsToImages(data.attachments, options.screenshotsBaseUrl)
140
+ );
141
+ const diffCount = countDiffImages(images);
142
+ const hasDiffs = diffCount > 0;
143
+ if (hasDiffs) {
144
+ test.approved = null;
145
+ }
146
+ test.results = [
147
+ {
148
+ status: data.status === "passed" ? "success" : "failed",
149
+ retries: 0,
150
+ images,
151
+ error: data.error,
152
+ duration: data.duration
153
+ }
154
+ ];
155
+ return {
156
+ test,
157
+ diffCount
158
+ };
159
+ }
160
+ function finalizeRunEvent(state) {
161
+ state.reportData.isRunning = false;
162
+ state.reportData.tests = Object.fromEntries(
163
+ Object.entries(state.reportData.tests).filter(([id]) => state.currentRunIds.has(id))
164
+ );
165
+ state.currentRunIds.clear();
166
+ const runTests = Object.values(state.reportData.tests).filter((test) => test !== void 0);
167
+ return {
168
+ passed: runTests.filter((test) => test.status === "success").length,
169
+ failed: runTests.filter((test) => test.status === "failed").length,
170
+ pending: runTests.filter((test) => test.status === "pending").length
171
+ };
172
+ }
173
+
174
+ // src/schemas.ts
175
+ var import_zod = require("zod");
176
+ var LocationSchema = import_zod.z.object({
177
+ file: import_zod.z.string(),
178
+ line: import_zod.z.number()
179
+ });
180
+ var ImagesSchema = import_zod.z.object({
181
+ actual: import_zod.z.string(),
182
+ expect: import_zod.z.string().optional(),
183
+ diff: import_zod.z.string().optional(),
184
+ error: import_zod.z.string().optional()
185
+ });
186
+ var AttachmentSchema = import_zod.z.object({
187
+ name: import_zod.z.string(),
188
+ path: import_zod.z.string(),
189
+ contentType: import_zod.z.string()
190
+ });
191
+ var TestStatusSchema = import_zod.z.enum(["unknown", "pending", "running", "failed", "approved", "success", "retrying"]);
192
+ var TestResultSchema = import_zod.z.object({
193
+ status: import_zod.z.enum(["failed", "success"]),
194
+ retries: import_zod.z.number(),
195
+ // eslint-disable-next-line typescript/no-unsafe-type-assertion
196
+ images: import_zod.z.record(import_zod.z.string(), ImagesSchema).optional(),
197
+ error: import_zod.z.string().optional(),
198
+ duration: import_zod.z.number().optional()
199
+ });
200
+ var TestDataSchema = import_zod.z.object({
201
+ id: import_zod.z.string(),
202
+ titlePath: import_zod.z.array(import_zod.z.string()),
203
+ browser: import_zod.z.string(),
204
+ title: import_zod.z.string(),
205
+ skip: import_zod.z.union([import_zod.z.boolean(), import_zod.z.string()]).optional(),
206
+ retries: import_zod.z.number().optional(),
207
+ status: TestStatusSchema.optional(),
208
+ results: import_zod.z.array(TestResultSchema).optional(),
209
+ approved: import_zod.z.record(import_zod.z.string(), import_zod.z.number()).nullable().optional(),
210
+ attachments: import_zod.z.array(AttachmentSchema).optional(),
211
+ location: LocationSchema.optional()
212
+ });
213
+ var CrvyRprtrTestSchema = TestDataSchema.extend({
214
+ checked: import_zod.z.boolean()
215
+ });
216
+ var CrvyRprtrSuiteSchema = import_zod.z.lazy(
217
+ () => import_zod.z.object({
218
+ path: import_zod.z.array(import_zod.z.string()),
219
+ skip: import_zod.z.boolean(),
220
+ status: TestStatusSchema.optional(),
221
+ opened: import_zod.z.boolean(),
222
+ checked: import_zod.z.boolean(),
223
+ indeterminate: import_zod.z.boolean(),
224
+ // eslint-disable-next-line typescript/no-unsafe-type-assertion
225
+ children: import_zod.z.record(import_zod.z.string(), import_zod.z.union([CrvyRprtrSuiteSchema, CrvyRprtrTestSchema])).optional()
226
+ })
227
+ );
228
+ var WebSocketMessageSchema = import_zod.z.object({
229
+ type: import_zod.z.enum(["test-begin", "test-end", "run-end", "approve", "sync"]),
230
+ data: import_zod.z.unknown()
231
+ });
232
+ var TestBeginDataSchema = import_zod.z.object({
233
+ id: import_zod.z.string(),
234
+ title: import_zod.z.string(),
235
+ titlePath: import_zod.z.array(import_zod.z.string()),
236
+ browser: import_zod.z.string(),
237
+ location: LocationSchema
238
+ });
239
+ var TestEndDataSchema = import_zod.z.object({
240
+ id: import_zod.z.string(),
241
+ status: import_zod.z.enum(["passed", "failed", "skipped"]),
242
+ attachments: import_zod.z.array(AttachmentSchema),
243
+ error: import_zod.z.string().optional(),
244
+ duration: import_zod.z.number().optional()
245
+ });
246
+ var ReportDataSchema = import_zod.z.object({
247
+ isRunning: import_zod.z.boolean(),
248
+ tests: import_zod.z.record(import_zod.z.string(), TestDataSchema),
249
+ browsers: import_zod.z.array(import_zod.z.string()),
250
+ isUpdateMode: import_zod.z.boolean(),
251
+ screenshotDir: import_zod.z.string()
252
+ });
253
+ var LoadedReportDataSchema = import_zod.z.object({
254
+ tests: import_zod.z.record(import_zod.z.string(), TestDataSchema).optional(),
255
+ isUpdateMode: import_zod.z.boolean().optional()
256
+ });
257
+ var OfflineEventSchema = import_zod.z.object({
258
+ type: import_zod.z.enum(["test-begin", "test-end", "run-end"]),
259
+ data: import_zod.z.unknown(),
260
+ timestamp: import_zod.z.number(),
261
+ workerIndex: import_zod.z.number()
262
+ });
263
+ var OfflineReportSchema = import_zod.z.object({
264
+ version: import_zod.z.number(),
265
+ generatedAt: import_zod.z.string(),
266
+ workers: import_zod.z.number(),
267
+ events: import_zod.z.array(OfflineEventSchema)
268
+ });
269
+ var ApproveRequestBodySchema = import_zod.z.object({
270
+ id: import_zod.z.string(),
271
+ retry: import_zod.z.number(),
272
+ image: import_zod.z.string()
273
+ });
274
+ var ReportApiResponseSchema = import_zod.z.object({
275
+ tests: import_zod.z.record(import_zod.z.string(), TestDataSchema),
276
+ isUpdateMode: import_zod.z.boolean().optional()
277
+ });
278
+ var ClientBootstrapDataSchema = import_zod.z.object({
279
+ report: ReportApiResponseSchema.extend({
280
+ isUpdateMode: import_zod.z.boolean()
281
+ }),
282
+ liveUpdates: import_zod.z.boolean(),
283
+ approvalEnabled: import_zod.z.boolean(),
284
+ approvalMessage: import_zod.z.string().optional()
285
+ });
286
+ var ImagesViewModeSchema = import_zod.z.enum(["side-by-side", "swap", "slide", "blend"]);
287
+ function safeParse(schema, data) {
288
+ const result = schema.safeParse(data);
289
+ if (result.success) {
290
+ return result.data;
291
+ }
292
+ return null;
293
+ }
294
+
295
+ // src/report-artifact.ts
296
+ var import_meta = { url: require("url").pathToFileURL(__filename).href };
297
+ var DEFAULT_REPORT_HTML_PATH = "./crvy-rprtr.html";
298
+ var STATIC_ARTIFACT_APPROVAL_MESSAGE = "This artifact is read-only. Open it with the Crvy Rprtr server to approve screenshots.";
299
+ function toBrowserPath(pathValue) {
300
+ const normalized = pathValue.split(import_path.sep).join("/");
301
+ if (normalized === "") return ".";
302
+ if (normalized.startsWith(".")) return normalized;
303
+ return `./${normalized}`;
304
+ }
305
+ function toBrowserDirPath(pathValue) {
306
+ const path = toBrowserPath(pathValue);
307
+ return path === "." ? "./" : `${path}/`;
308
+ }
309
+ function serializeBootstrapData(data) {
310
+ return JSON.stringify(data).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026");
311
+ }
312
+ function escapeInlineStyle(style) {
313
+ return style.replace(/<\/style>/gi, "<\\/style>");
314
+ }
315
+ function escapeInlineScript(script) {
316
+ return script.replace(/<\/script>/gi, "<\\/script>");
317
+ }
318
+ function renderArtifactHtml(template, bootstrapData, stylesheet, script) {
319
+ const stylesheetPlaceholder = '<link rel="stylesheet" href="/dist/index.css" />';
320
+ const scriptPlaceholder = '<script type="module" src="/dist/index.js"></script>';
321
+ if (!template.includes(stylesheetPlaceholder) || !template.includes(scriptPlaceholder)) {
322
+ throw new Error("Failed to find static asset placeholders in the HTML template");
323
+ }
324
+ const withStylesheet = template.replace(
325
+ stylesheetPlaceholder,
326
+ () => `<style>${escapeInlineStyle(stylesheet)}</style>`
327
+ );
328
+ const bootstrapScript = `<script id="crvy-rprtr-bootstrap" type="application/json">${serializeBootstrapData(bootstrapData)}</script>`;
329
+ return withStylesheet.replace(
330
+ scriptPlaceholder,
331
+ () => `${bootstrapScript}
332
+ <script type="module">${escapeInlineScript(script)}</script>`
333
+ );
334
+ }
335
+ async function getPackagedAssetPath(fileName) {
336
+ const currentDir = (0, import_path.dirname)((0, import_url.fileURLToPath)(import_meta.url));
337
+ const candidates = [currentDir, (0, import_path.resolve)(currentDir, "../dist")];
338
+ const resolvedCandidates = await Promise.all(
339
+ candidates.map(async (candidate) => {
340
+ const assetPath = (0, import_path.resolve)(candidate, fileName);
341
+ try {
342
+ await (0, import_promises.access)(assetPath);
343
+ return assetPath;
344
+ } catch {
345
+ return null;
346
+ }
347
+ })
348
+ );
349
+ const resolvedAssetPath = resolvedCandidates.find((assetPath) => assetPath !== null);
350
+ if (resolvedAssetPath !== void 0) {
351
+ return resolvedAssetPath;
352
+ }
353
+ throw new Error(
354
+ `Could not find packaged asset "${fileName}". Run "bun run build" before generating report artifacts.`
355
+ );
356
+ }
357
+ function buildStaticBootstrapData(events, screenshotDir, htmlPath) {
358
+ const screenshotBaseUrl = toBrowserDirPath((0, import_path.relative)((0, import_path.dirname)(htmlPath), (0, import_path.resolve)(screenshotDir)));
359
+ const state = createMutableReportState(screenshotDir);
360
+ for (const event of events) {
361
+ switch (event.type) {
362
+ case "test-begin": {
363
+ const parsed = safeParse(TestBeginDataSchema, event.data);
364
+ if (parsed !== null) {
365
+ applyTestBeginEvent(state, parsed);
366
+ }
367
+ break;
368
+ }
369
+ case "test-end": {
370
+ const parsed = safeParse(TestEndDataSchema, event.data);
371
+ if (parsed !== null) {
372
+ applyTestEndEvent(state, parsed, { screenshotsBaseUrl: screenshotBaseUrl });
373
+ }
374
+ break;
375
+ }
376
+ case "run-end":
377
+ finalizeRunEvent(state);
378
+ break;
379
+ }
380
+ }
381
+ const bootstrapData = {
382
+ report: {
383
+ tests: state.reportData.tests,
384
+ isUpdateMode: state.reportData.isUpdateMode
385
+ },
386
+ liveUpdates: false,
387
+ approvalEnabled: false,
388
+ approvalMessage: STATIC_ARTIFACT_APPROVAL_MESSAGE
389
+ };
390
+ const parsedBootstrapData = safeParse(ClientBootstrapDataSchema, bootstrapData);
391
+ if (parsedBootstrapData === null) {
392
+ throw new Error("Failed to build static report bootstrap data");
393
+ }
394
+ return parsedBootstrapData;
395
+ }
396
+ async function writeReportArtifact(options) {
397
+ const reportHtmlPath = (0, import_path.resolve)(options.reportHtmlPath ?? DEFAULT_REPORT_HTML_PATH);
398
+ const [indexHtmlPath, indexJsPath, indexCssPath] = await Promise.all([
399
+ getPackagedAssetPath("index.html"),
400
+ getPackagedAssetPath("index.js"),
401
+ getPackagedAssetPath("index.css")
402
+ ]);
403
+ await (0, import_promises.mkdir)((0, import_path.dirname)(reportHtmlPath), { recursive: true });
404
+ const [template, script, stylesheet] = await Promise.all([
405
+ (0, import_promises.readFile)(indexHtmlPath, "utf8"),
406
+ (0, import_promises.readFile)(indexJsPath, "utf8"),
407
+ (0, import_promises.readFile)(indexCssPath, "utf8")
408
+ ]);
409
+ const bootstrapData = buildStaticBootstrapData(options.events, options.screenshotDir, reportHtmlPath);
410
+ const html = renderArtifactHtml(template, bootstrapData, stylesheet, script);
411
+ await (0, import_promises.writeFile)(reportHtmlPath, html);
412
+ }
413
+
414
+ // src/reporter-utils.ts
415
+ function extractScreenshotNames(steps) {
416
+ const names = [];
417
+ for (const step of steps) {
418
+ const match = step.title.match(/toHaveScreenshot\((.+?)\)/);
419
+ if (match?.[1] !== void 0 && match[1] !== "") names.push(match[1]);
420
+ if (step.steps.length) names.push(...extractScreenshotNames(step.steps));
421
+ }
422
+ return names;
423
+ }
424
+
425
+ // src/reporter.ts
426
+ var MAX_CONCURRENT_FILE_OPS = 5;
427
+ var CrvyRprtr = class {
428
+ ws = null;
429
+ serverUrl;
430
+ screenshotDir;
431
+ queue = [];
432
+ workerIndex;
433
+ offlineReportPath;
434
+ reportHtmlPath;
435
+ isOfflineMode = false;
436
+ hadOfflineMode = false;
437
+ runEvents = [];
438
+ constructor(options = {}) {
439
+ this.serverUrl = options.serverUrl ?? "ws://localhost:3000";
440
+ this.screenshotDir = options.screenshotDir ?? "./screenshots";
441
+ this.workerIndex = parseInt(process.env.TEST_WORKER_INDEX ?? "0", 10) || 0;
442
+ this.offlineReportPath = options.offlineReportPath ?? `./crvy-rprtr-${this.workerIndex}.json`;
443
+ this.reportHtmlPath = options.reportHtmlPath ?? "./crvy-rprtr.html";
444
+ }
445
+ async onBegin(config, suite) {
446
+ console.log(`[CrvyRprtr] Starting run with ${suite.allTests().length} tests`);
447
+ await (0, import_promises2.mkdir)(this.screenshotDir, { recursive: true });
448
+ this.connect();
449
+ }
450
+ connect() {
451
+ const WebSocketConstructor = globalThis.WebSocket;
452
+ if (typeof WebSocketConstructor !== "function") {
453
+ console.log("[CrvyRprtr] WebSocket unavailable in current runtime; offline mode enabled");
454
+ this.enableOfflineMode();
455
+ return;
456
+ }
457
+ try {
458
+ this.ws = new WebSocketConstructor(this.serverUrl);
459
+ this.ws.onopen = () => {
460
+ console.log("[CrvyRprtr] Connected to Crvy Rprtr server");
461
+ this.isOfflineMode = false;
462
+ for (const msg of this.queue) this.ws.send(msg);
463
+ this.queue = [];
464
+ };
465
+ this.ws.onerror = (error) => {
466
+ console.error("[CrvyRprtr] WebSocket error:", error);
467
+ this.enableOfflineMode();
468
+ };
469
+ this.ws.onclose = () => {
470
+ console.log("[CrvyRprtr] Disconnected from Crvy Rprtr server");
471
+ this.enableOfflineMode();
472
+ };
473
+ } catch (e) {
474
+ console.error("[CrvyRprtr] Failed to connect:", e);
475
+ this.enableOfflineMode();
476
+ }
477
+ }
478
+ enableOfflineMode() {
479
+ if (!this.isOfflineMode) {
480
+ this.isOfflineMode = true;
481
+ this.hadOfflineMode = true;
482
+ console.log("[CrvyRprtr] Offline mode enabled - events will be queued to file");
483
+ }
484
+ }
485
+ onTestBegin(test) {
486
+ const titlePath = [];
487
+ let suite = test.parent;
488
+ while (suite && suite.type === "describe") {
489
+ titlePath.unshift(suite.title);
490
+ suite = suite.parent;
491
+ }
492
+ this.send({
493
+ type: "test-begin",
494
+ data: {
495
+ id: test.id,
496
+ title: test.title,
497
+ titlePath,
498
+ browser: test.parent.project()?.name ?? "chromium",
499
+ location: {
500
+ file: test.location.file,
501
+ line: test.location.line
502
+ }
503
+ }
504
+ });
505
+ }
506
+ async onTestEnd(test, result) {
507
+ const savedAttachments = await this.saveAttachments(test.id, result);
508
+ await this.copySnapshotBaselines(test, result, savedAttachments);
509
+ this.send({
510
+ type: "test-end",
511
+ data: {
512
+ id: test.id,
513
+ title: test.title,
514
+ status: result.status,
515
+ attachments: savedAttachments,
516
+ error: result.errors.length > 0 ? result.errors[0]?.message : void 0,
517
+ duration: result.duration
518
+ }
519
+ });
520
+ }
521
+ async copySnapshotBaselines(test, result, savedAttachments) {
522
+ if (result.status !== "passed") return;
523
+ const snapshotNames = extractScreenshotNames(result.steps);
524
+ if (snapshotNames.length === 0) return;
525
+ const projectName = test.parent.project()?.name ?? "chromium";
526
+ const snapshotDir = `${test.location.file}-snapshots`;
527
+ const testScreenshotDir = (0, import_path2.join)(this.screenshotDir, this.sanitizeId(test.id));
528
+ const limit = (0, import_p_limit.default)(MAX_CONCURRENT_FILE_OPS);
529
+ const copyPromises = snapshotNames.map(
530
+ (name) => limit(async () => {
531
+ const baseName = name.replace(/\.png$/, "");
532
+ const snapshotPath = (0, import_path2.join)(snapshotDir, `${baseName}-${projectName}-${process.platform}.png`);
533
+ const destName = `${baseName}-expected`;
534
+ const destPath = (0, import_path2.join)(testScreenshotDir, destName);
535
+ try {
536
+ await (0, import_promises2.mkdir)(testScreenshotDir, { recursive: true });
537
+ await (0, import_promises2.copyFile)(snapshotPath, destPath);
538
+ savedAttachments.push({
539
+ name: destName,
540
+ path: `${this.sanitizeId(test.id)}/${destName}`,
541
+ contentType: "image/png"
542
+ });
543
+ console.log(`[CrvyRprtr] Attached baseline: ${snapshotPath}`);
544
+ } catch {
545
+ }
546
+ })
547
+ );
548
+ await Promise.all(copyPromises);
549
+ }
550
+ async saveAttachments(testId, result) {
551
+ const savedAttachments = [];
552
+ const testScreenshotDir = (0, import_path2.join)(this.screenshotDir, this.sanitizeId(testId));
553
+ const limit = (0, import_p_limit.default)(MAX_CONCURRENT_FILE_OPS);
554
+ const attachmentPromises = result.attachments.filter(
555
+ (attachment) => attachment.contentType === "image/png" && attachment.path !== void 0
556
+ ).map(
557
+ (attachment) => limit(async () => {
558
+ try {
559
+ await (0, import_promises2.mkdir)(testScreenshotDir, { recursive: true });
560
+ const fileName = attachment.name;
561
+ const destPath = (0, import_path2.join)(testScreenshotDir, fileName);
562
+ await (0, import_promises2.copyFile)(attachment.path, destPath);
563
+ const attachmentData = {
564
+ name: attachment.name,
565
+ path: `${this.sanitizeId(testId)}/${fileName}`,
566
+ contentType: attachment.contentType
567
+ };
568
+ savedAttachments.push(attachmentData);
569
+ console.log(`[CrvyRprtr] Saved screenshot: ${destPath}`);
570
+ } catch (e) {
571
+ console.error(`[CrvyRprtr] Failed to save screenshot: ${attachment.path}`, e);
572
+ const fallbackData = {
573
+ name: attachment.name,
574
+ path: attachment.path,
575
+ contentType: attachment.contentType
576
+ };
577
+ savedAttachments.push(fallbackData);
578
+ }
579
+ })
580
+ );
581
+ await Promise.all(attachmentPromises);
582
+ return savedAttachments;
583
+ }
584
+ sanitizeId(id) {
585
+ return id.replace(/[^a-zA-Z0-9-_]/g, "_");
586
+ }
587
+ async writeOfflineReport() {
588
+ if (this.runEvents.length === 0) {
589
+ console.log("[CrvyRprtr] No offline events to write");
590
+ return;
591
+ }
592
+ try {
593
+ const report = {
594
+ version: 1,
595
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
596
+ workers: this.workerIndex + 1,
597
+ events: this.runEvents.map((e) => ({
598
+ ...e,
599
+ timestamp: Date.now(),
600
+ workerIndex: this.workerIndex
601
+ }))
602
+ };
603
+ await (0, import_promises2.writeFile)(this.offlineReportPath, JSON.stringify(report, null, 2));
604
+ console.log(`[CrvyRprtr] Wrote offline report: ${this.offlineReportPath}`);
605
+ } catch (e) {
606
+ console.error("[CrvyRprtr] Failed to write offline report:", e);
607
+ }
608
+ }
609
+ async writeStaticArtifact() {
610
+ try {
611
+ await writeReportArtifact({
612
+ events: this.runEvents,
613
+ screenshotDir: this.screenshotDir,
614
+ reportHtmlPath: this.reportHtmlPath
615
+ });
616
+ console.log(`[CrvyRprtr] Wrote report artifact: ${this.reportHtmlPath}`);
617
+ } catch (e) {
618
+ console.error("[CrvyRprtr] Failed to write report artifact:", e);
619
+ }
620
+ }
621
+ async onEnd(result) {
622
+ this.send({
623
+ type: "run-end",
624
+ data: {
625
+ status: result.status
626
+ }
627
+ });
628
+ await this.writeStaticArtifact();
629
+ if (this.hadOfflineMode) {
630
+ await this.writeOfflineReport();
631
+ }
632
+ await new Promise((resolve2) => {
633
+ if (!this.ws || this.ws.readyState === WebSocket.CLOSED) {
634
+ resolve2();
635
+ return;
636
+ }
637
+ this.ws.onclose = () => {
638
+ resolve2();
639
+ };
640
+ setTimeout(() => {
641
+ this.ws?.close();
642
+ resolve2();
643
+ }, 1e3);
644
+ this.ws.close();
645
+ });
646
+ }
647
+ send(msg) {
648
+ const msgObj = msg;
649
+ if (msgObj.type === "test-begin" || msgObj.type === "test-end" || msgObj.type === "run-end") {
650
+ this.runEvents.push({
651
+ type: msgObj.type,
652
+ data: msgObj.data
653
+ });
654
+ }
655
+ const payload = JSON.stringify(msg);
656
+ if (!this.isOfflineMode) {
657
+ if (this.ws?.readyState === WebSocket.OPEN) {
658
+ this.ws.send(payload);
659
+ } else {
660
+ this.queue.push(payload);
661
+ }
662
+ }
663
+ }
664
+ };
665
+ var reporter_default = CrvyRprtr;
666
+ // Annotate the CommonJS export names for ESM import in node:
667
+ 0 && (module.exports = {
668
+ CrvyRprtr
669
+ });