@crvy/rprtr 0.0.4 → 0.0.7

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.
@@ -1,9 +1,12 @@
1
- import type { Reporter, FullConfig, Suite, TestCase, TestResult, FullResult } from '@playwright/test/reporter';
1
+ import type { FullConfig, FullResult, Reporter, Suite, TestCase, TestResult } from '@playwright/test/reporter';
2
2
  export interface CrvyRprtrOptions {
3
3
  serverUrl?: string;
4
4
  screenshotDir?: string;
5
5
  offlineReportPath?: string;
6
6
  reportHtmlPath?: string;
7
+ playwrightSnapshotDir?: string;
8
+ playwrightSnapshotPathTemplate?: string;
9
+ playwrightToHaveScreenshotPathTemplate?: string;
7
10
  }
8
11
  export declare class CrvyRprtr implements Reporter {
9
12
  private ws;
@@ -13,6 +16,11 @@ export declare class CrvyRprtr implements Reporter {
13
16
  private workerIndex;
14
17
  private offlineReportPath;
15
18
  private reportHtmlPath;
19
+ private configDir;
20
+ private testMetadata;
21
+ private playwrightSnapshotDir?;
22
+ private playwrightSnapshotPathTemplate?;
23
+ private playwrightToHaveScreenshotPathTemplate?;
16
24
  private isOfflineMode;
17
25
  private hadOfflineMode;
18
26
  private runEvents;
@@ -20,8 +28,12 @@ export declare class CrvyRprtr implements Reporter {
20
28
  onBegin(config: FullConfig, suite: Suite): Promise<void>;
21
29
  private connect;
22
30
  private enableOfflineMode;
31
+ private describeTitlePath;
32
+ private reporterTitlePath;
23
33
  onTestBegin(test: TestCase): void;
24
34
  onTestEnd(test: TestCase, result: TestResult): Promise<void>;
35
+ private baselineInput;
36
+ private copyResolvedBaseline;
25
37
  private copySnapshotBaselines;
26
38
  private saveAttachments;
27
39
  private sanitizeId;
@@ -1 +1 @@
1
- {"version":3,"file":"reporter.d.ts","sourceRoot":"","sources":["../src/reporter.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAA;AAQ9G,MAAM,WAAW,gBAAgB;IAC/B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB;AAQD,qBAAa,SAAU,YAAW,QAAQ;IACxC,OAAO,CAAC,EAAE,CAAyB;IACnC,OAAO,CAAC,SAAS,CAAQ;IACzB,OAAO,CAAC,aAAa,CAAQ;IAC7B,OAAO,CAAC,KAAK,CAAe;IAC5B,OAAO,CAAC,WAAW,CAAQ;IAC3B,OAAO,CAAC,iBAAiB,CAAQ;IACjC,OAAO,CAAC,cAAc,CAAQ;IAC9B,OAAO,CAAC,aAAa,CAAQ;IAC7B,OAAO,CAAC,cAAc,CAAQ;IAC9B,OAAO,CAAC,SAAS,CAA4E;gBAEjF,OAAO,GAAE,gBAAqB;IAQpC,OAAO,CAAC,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC;IAM9D,OAAO,CAAC,OAAO;IA8Bf,OAAO,CAAC,iBAAiB;IAQzB,WAAW,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI;IAsB3B,SAAS,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;YAiBpD,qBAAqB;YAsCrB,eAAe;IAyC7B,OAAO,CAAC,UAAU;YAIJ,kBAAkB;YAyBlB,mBAAmB;IAa3B,KAAK,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IA8B9C,OAAO,CAAC,IAAI;CAkBb;AAED,eAAe,SAAS,CAAA"}
1
+ {"version":3,"file":"reporter.d.ts","sourceRoot":"","sources":["../src/reporter.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAA;AAqB9G,MAAM,WAAW,gBAAgB;IAC/B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,qBAAqB,CAAC,EAAE,MAAM,CAAA;IAC9B,8BAA8B,CAAC,EAAE,MAAM,CAAA;IACvC,sCAAsC,CAAC,EAAE,MAAM,CAAA;CAChD;AACD,qBAAa,SAAU,YAAW,QAAQ;IACxC,OAAO,CAAC,EAAE,CAAyB;IACnC,OAAO,CAAC,SAAS,CAAQ;IACzB,OAAO,CAAC,aAAa,CAAQ;IAC7B,OAAO,CAAC,KAAK,CAAe;IAC5B,OAAO,CAAC,WAAW,CAAQ;IAC3B,OAAO,CAAC,iBAAiB,CAAQ;IACjC,OAAO,CAAC,cAAc,CAAQ;IAC9B,OAAO,CAAC,SAAS,CAAgB;IACjC,OAAO,CAAC,YAAY,CAAqD;IACzE,OAAO,CAAC,qBAAqB,CAAC,CAAQ;IACtC,OAAO,CAAC,8BAA8B,CAAC,CAAQ;IAC/C,OAAO,CAAC,sCAAsC,CAAC,CAAQ;IACvD,OAAO,CAAC,aAAa,CAAQ;IAC7B,OAAO,CAAC,cAAc,CAAQ;IAC9B,OAAO,CAAC,SAAS,CAAiB;gBACtB,OAAO,GAAE,gBAAqB;IAUpC,OAAO,CAAC,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC;IAM9D,OAAO,CAAC,OAAO;IA4Bf,OAAO,CAAC,iBAAiB;IAKzB,OAAO,CAAC,iBAAiB;IAMzB,OAAO,CAAC,iBAAiB;IAKzB,WAAW,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI;IAa3B,SAAS,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAsBlE,OAAO,CAAC,aAAa;YAoBP,oBAAoB;YAkBpB,qBAAqB;YAoBrB,eAAe;IAqC7B,OAAO,CAAC,UAAU;YAGJ,kBAAkB;YAgBlB,mBAAmB;IAY3B,KAAK,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAiB9C,OAAO,CAAC,IAAI;CAUb;AACD,eAAe,SAAS,CAAA"}
package/dist/reporter.js CHANGED
@@ -6,12 +6,14 @@ import {
6
6
  applyTestEndEvent,
7
7
  createMutableReportState,
8
8
  finalizeRunEvent,
9
+ resolveBaselineTargets,
9
10
  safeParse
10
- } from "./chunk-IEYNAB6M.js";
11
+ } from "./chunk-JGS2VWQP.js";
11
12
 
12
13
  // src/reporter.ts
13
- import { mkdir as mkdir2, copyFile, writeFile as writeFile2 } from "fs/promises";
14
- import { join } from "path";
14
+ import { existsSync } from "fs";
15
+ import { copyFile, mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
16
+ import { dirname as dirname2, join } from "path";
15
17
  import pLimit from "p-limit";
16
18
 
17
19
  // src/report-artifact.ts
@@ -136,18 +138,88 @@ async function writeReportArtifact(options) {
136
138
  }
137
139
 
138
140
  // src/reporter-utils.ts
139
- function extractScreenshotNames(steps) {
140
- const names = [];
141
+ var NAMED_SCREENSHOT_STEP_TITLE = /toHaveScreenshot\((.+?)\)/;
142
+ var UNNAMED_SCREENSHOT_STEP_TITLE = /^Expect "toHaveScreenshot"(?:\s|$)/;
143
+ var SYNTHETIC_SCREENSHOT_PREFIX = "__unnamed-screenshot-";
144
+ function addVisualSuffix(visualName, occurrenceIndex) {
145
+ if (occurrenceIndex === 1) {
146
+ return visualName;
147
+ }
148
+ const slashIndex = visualName.lastIndexOf("/");
149
+ return slashIndex === -1 ? `${visualName}-${occurrenceIndex - 1}` : `${visualName.slice(0, slashIndex + 1)}${visualName.slice(slashIndex + 1)}-${occurrenceIndex - 1}`;
150
+ }
151
+ function normalizeNamedScreenshot(titleMatch, state) {
152
+ const unquotedName = titleMatch.trim().replace(/^['"`]|['"`]$/g, "");
153
+ if (unquotedName === "") {
154
+ return null;
155
+ }
156
+ const normalizedPath = unquotedName.replace(/\\/g, "/");
157
+ const declaredName = normalizedPath.replace(/\.png$/, "");
158
+ if (declaredName === "") {
159
+ return null;
160
+ }
161
+ const occurrenceIndex = (state.namedOccurrences.get(declaredName) ?? 0) + 1;
162
+ state.namedOccurrences.set(declaredName, occurrenceIndex);
163
+ return {
164
+ visualName: addVisualSuffix(declaredName, occurrenceIndex),
165
+ kind: "named",
166
+ declaredName,
167
+ snapshotBaseName: declaredName,
168
+ occurrenceIndex
169
+ };
170
+ }
171
+ function visitStep(step, state) {
172
+ const declarationsBeforeChildren = state.declarations.length;
173
+ for (const nestedStep of step.steps) {
174
+ visitStep(nestedStep, state);
175
+ }
176
+ const namedMatch = step.title.match(NAMED_SCREENSHOT_STEP_TITLE);
177
+ if (namedMatch?.[1] !== void 0) {
178
+ const declaration = normalizeNamedScreenshot(namedMatch[1], state);
179
+ if (declaration !== null) {
180
+ state.declarations.push(declaration);
181
+ return true;
182
+ }
183
+ }
184
+ const hasNestedScreenshotDeclaration = state.declarations.length > declarationsBeforeChildren;
185
+ if (UNNAMED_SCREENSHOT_STEP_TITLE.test(step.title) && !hasNestedScreenshotDeclaration) {
186
+ const occurrenceIndex = state.nextUnnamedIndex;
187
+ state.declarations.push({
188
+ visualName: `${SYNTHETIC_SCREENSHOT_PREFIX}${occurrenceIndex}`,
189
+ kind: "unnamed",
190
+ occurrenceIndex
191
+ });
192
+ state.nextUnnamedIndex += 1;
193
+ return true;
194
+ }
195
+ return hasNestedScreenshotDeclaration;
196
+ }
197
+ function extractScreenshotDeclarations(steps) {
198
+ const state = {
199
+ declarations: [],
200
+ namedOccurrences: /* @__PURE__ */ new Map(),
201
+ nextUnnamedIndex: 1
202
+ };
141
203
  for (const step of steps) {
142
- const match = step.title.match(/toHaveScreenshot\((.+?)\)/);
143
- if (match?.[1] !== void 0 && match[1] !== "") names.push(match[1]);
144
- if (step.steps.length) names.push(...extractScreenshotNames(step.steps));
204
+ visitStep(step, state);
145
205
  }
146
- return names;
206
+ return state.declarations;
147
207
  }
148
208
 
149
209
  // src/reporter.ts
210
+ var CURRENT_DIRECTORY_ARTIFACT_SEGMENT = "+dot+";
211
+ var PARENT_DIRECTORY_ARTIFACT_SEGMENT = "+dotdot+";
150
212
  var MAX_CONCURRENT_FILE_OPS = 5;
213
+ var SAFE_ARTIFACT_CHARACTER = /^[A-Za-z0-9._-]$/;
214
+ var encodeArtifactPathSegment = (segment) => {
215
+ if (segment === ".") return CURRENT_DIRECTORY_ARTIFACT_SEGMENT;
216
+ if (segment === "..") return PARENT_DIRECTORY_ARTIFACT_SEGMENT;
217
+ return Array.from(
218
+ segment,
219
+ (character) => SAFE_ARTIFACT_CHARACTER.test(character) ? character : encodeURIComponent(character)
220
+ ).join("");
221
+ };
222
+ var safeArtifactPath = (name) => name.split("/").map(encodeArtifactPathSegment).join("/");
151
223
  var CrvyRprtr = class {
152
224
  ws = null;
153
225
  serverUrl;
@@ -156,6 +228,11 @@ var CrvyRprtr = class {
156
228
  workerIndex;
157
229
  offlineReportPath;
158
230
  reportHtmlPath;
231
+ configDir = process.cwd();
232
+ testMetadata = /* @__PURE__ */ new Map();
233
+ playwrightSnapshotDir;
234
+ playwrightSnapshotPathTemplate;
235
+ playwrightToHaveScreenshotPathTemplate;
159
236
  isOfflineMode = false;
160
237
  hadOfflineMode = false;
161
238
  runEvents = [];
@@ -165,8 +242,12 @@ var CrvyRprtr = class {
165
242
  this.workerIndex = parseInt(process.env.TEST_WORKER_INDEX ?? "0", 10) || 0;
166
243
  this.offlineReportPath = options.offlineReportPath ?? `./crvy-rprtr-${this.workerIndex}.json`;
167
244
  this.reportHtmlPath = options.reportHtmlPath ?? "./crvy-rprtr.html";
245
+ this.playwrightSnapshotDir = options.playwrightSnapshotDir;
246
+ this.playwrightSnapshotPathTemplate = options.playwrightSnapshotPathTemplate;
247
+ this.playwrightToHaveScreenshotPathTemplate = options.playwrightToHaveScreenshotPathTemplate;
168
248
  }
169
249
  async onBegin(config, suite) {
250
+ this.configDir = config.configFile === void 0 ? config.rootDir : dirname2(config.configFile);
170
251
  console.log(`[CrvyRprtr] Starting run with ${suite.allTests().length} tests`);
171
252
  await mkdir2(this.screenshotDir, { recursive: true });
172
253
  this.connect();
@@ -183,7 +264,7 @@ var CrvyRprtr = class {
183
264
  this.ws.onopen = () => {
184
265
  console.log("[CrvyRprtr] Connected to Crvy Rprtr server");
185
266
  this.isOfflineMode = false;
186
- for (const msg of this.queue) this.ws.send(msg);
267
+ for (const message of this.queue) this.ws.send(message);
187
268
  this.queue = [];
188
269
  };
189
270
  this.ws.onerror = (error) => {
@@ -194,140 +275,158 @@ var CrvyRprtr = class {
194
275
  console.log("[CrvyRprtr] Disconnected from Crvy Rprtr server");
195
276
  this.enableOfflineMode();
196
277
  };
197
- } catch (e) {
198
- console.error("[CrvyRprtr] Failed to connect:", e);
278
+ } catch (error) {
279
+ console.error("[CrvyRprtr] Failed to connect:", error);
199
280
  this.enableOfflineMode();
200
281
  }
201
282
  }
202
283
  enableOfflineMode() {
203
- if (!this.isOfflineMode) {
204
- this.isOfflineMode = true;
205
- this.hadOfflineMode = true;
206
- console.log("[CrvyRprtr] Offline mode enabled - events will be queued to file");
207
- }
284
+ if (this.isOfflineMode) return;
285
+ this.isOfflineMode = this.hadOfflineMode = true;
286
+ console.log("[CrvyRprtr] Offline mode enabled - events will be queued to file");
208
287
  }
209
- onTestBegin(test) {
288
+ describeTitlePath(test) {
210
289
  const titlePath = [];
211
- let suite = test.parent;
212
- while (suite && suite.type === "describe") {
290
+ for (let suite = test.parent; suite?.type === "describe"; suite = suite.parent)
213
291
  titlePath.unshift(suite.title);
214
- suite = suite.parent;
215
- }
292
+ return titlePath;
293
+ }
294
+ reporterTitlePath(test) {
295
+ return typeof test.titlePath === "function" ? test.titlePath() : ["", test.parent.project()?.name ?? "chromium", test.location.file, ...this.describeTitlePath(test), test.title];
296
+ }
297
+ onTestBegin(test) {
298
+ this.testMetadata.set(test.id, { reporterTitlePath: this.reporterTitlePath(test) });
216
299
  this.send({
217
300
  type: "test-begin",
218
301
  data: {
219
302
  id: test.id,
220
303
  title: test.title,
221
- titlePath,
304
+ titlePath: this.describeTitlePath(test),
222
305
  browser: test.parent.project()?.name ?? "chromium",
223
- location: {
224
- file: test.location.file,
225
- line: test.location.line
226
- }
306
+ location: { file: test.location.file, line: test.location.line }
227
307
  }
228
308
  });
229
309
  }
230
310
  async onTestEnd(test, result) {
311
+ const screenshotDeclarations = extractScreenshotDeclarations(result.steps);
231
312
  const savedAttachments = await this.saveAttachments(test.id, result);
232
- await this.copySnapshotBaselines(test, result, savedAttachments);
233
- this.send({
234
- type: "test-end",
235
- data: {
236
- id: test.id,
237
- title: test.title,
238
- status: result.status,
239
- attachments: savedAttachments,
240
- error: result.errors.length > 0 ? result.errors[0]?.message : void 0,
241
- duration: result.duration
242
- }
243
- });
313
+ try {
314
+ await this.copySnapshotBaselines(test, result.status, screenshotDeclarations, savedAttachments);
315
+ this.send({
316
+ type: "test-end",
317
+ data: {
318
+ id: test.id,
319
+ title: test.title,
320
+ status: result.status,
321
+ attachments: savedAttachments,
322
+ visualNames: screenshotDeclarations.map(({ visualName }) => visualName),
323
+ visualDeclarations: screenshotDeclarations,
324
+ error: result.errors.length > 0 ? result.errors[0]?.message : void 0,
325
+ duration: result.duration
326
+ }
327
+ });
328
+ } finally {
329
+ this.testMetadata.delete(test.id);
330
+ }
331
+ }
332
+ baselineInput(test, shots) {
333
+ const project = test.parent.project();
334
+ const snapshotDir = this.playwrightSnapshotDir ?? project?.snapshotDir;
335
+ if (project === void 0 || typeof project.testDir !== "string" || typeof snapshotDir !== "string") return null;
336
+ return {
337
+ testFile: test.location.file,
338
+ reporterTitlePath: this.testMetadata.get(test.id)?.reporterTitlePath ?? this.reporterTitlePath(test),
339
+ declarations: shots,
340
+ config: {
341
+ configDir: this.configDir,
342
+ testDir: project.testDir,
343
+ snapshotDir,
344
+ projectName: project.name,
345
+ snapshotSuffix: process.platform,
346
+ snapshotPathTemplate: this.playwrightSnapshotPathTemplate,
347
+ toHaveScreenshotPathTemplate: this.playwrightToHaveScreenshotPathTemplate
348
+ },
349
+ snapshotPathExists: existsSync
350
+ };
244
351
  }
245
- async copySnapshotBaselines(test, result, savedAttachments) {
246
- if (result.status !== "passed") return;
247
- const snapshotNames = extractScreenshotNames(result.steps);
248
- if (snapshotNames.length === 0) return;
249
- const projectName = test.parent.project()?.name ?? "chromium";
250
- const snapshotDir = `${test.location.file}-snapshots`;
251
- const testScreenshotDir = join(this.screenshotDir, this.sanitizeId(test.id));
352
+ async copyResolvedBaseline(safeTestId, testScreenshotDir, target, savedAttachments) {
353
+ const attachmentName = `${target.attachmentBaseName}-expected.png`;
354
+ const artifactPath = safeArtifactPath(attachmentName);
355
+ const destPath = join(testScreenshotDir, artifactPath);
356
+ try {
357
+ await mkdir2(dirname2(destPath), { recursive: true });
358
+ await copyFile(target.snapshotPath, destPath);
359
+ savedAttachments.push({ name: attachmentName, path: `${safeTestId}/${artifactPath}`, contentType: "image/png" });
360
+ console.log(`[CrvyRprtr] Attached baseline: ${target.snapshotPath}`);
361
+ } catch {
362
+ }
363
+ }
364
+ async copySnapshotBaselines(test, status, screenshotDeclarations, savedAttachments) {
365
+ if (status !== "passed" || screenshotDeclarations.length === 0) return;
366
+ const input = this.baselineInput(test, screenshotDeclarations);
367
+ if (input === null) return;
368
+ const targets = resolveBaselineTargets(input);
369
+ if (targets.length === 0) return;
370
+ const safeTestId = this.sanitizeId(test.id);
371
+ const testScreenshotDir = join(this.screenshotDir, safeTestId);
252
372
  const limit = pLimit(MAX_CONCURRENT_FILE_OPS);
253
- const copyPromises = snapshotNames.map(
254
- (name) => limit(async () => {
255
- const baseName = name.replace(/\.png$/, "");
256
- const snapshotPath = join(snapshotDir, `${baseName}-${projectName}-${process.platform}.png`);
257
- const destName = `${baseName}-expected`;
258
- const destPath = join(testScreenshotDir, destName);
259
- try {
260
- await mkdir2(testScreenshotDir, { recursive: true });
261
- await copyFile(snapshotPath, destPath);
262
- savedAttachments.push({
263
- name: destName,
264
- path: `${this.sanitizeId(test.id)}/${destName}`,
265
- contentType: "image/png"
266
- });
267
- console.log(`[CrvyRprtr] Attached baseline: ${snapshotPath}`);
268
- } catch {
269
- }
270
- })
373
+ await Promise.all(
374
+ targets.map(
375
+ (target) => limit(() => this.copyResolvedBaseline(safeTestId, testScreenshotDir, target, savedAttachments))
376
+ )
271
377
  );
272
- await Promise.all(copyPromises);
273
378
  }
274
379
  async saveAttachments(testId, result) {
275
380
  const savedAttachments = [];
276
- const testScreenshotDir = join(this.screenshotDir, this.sanitizeId(testId));
381
+ const safeTestId = this.sanitizeId(testId);
382
+ const testScreenshotDir = join(this.screenshotDir, safeTestId);
277
383
  const limit = pLimit(MAX_CONCURRENT_FILE_OPS);
278
- const attachmentPromises = result.attachments.filter(
279
- (attachment) => attachment.contentType === "image/png" && attachment.path !== void 0
280
- ).map(
281
- (attachment) => limit(async () => {
282
- try {
283
- await mkdir2(testScreenshotDir, { recursive: true });
284
- const fileName = attachment.name;
285
- const destPath = join(testScreenshotDir, fileName);
286
- await copyFile(attachment.path, destPath);
287
- const attachmentData = {
288
- name: attachment.name,
289
- path: `${this.sanitizeId(testId)}/${fileName}`,
290
- contentType: attachment.contentType
291
- };
292
- savedAttachments.push(attachmentData);
293
- console.log(`[CrvyRprtr] Saved screenshot: ${destPath}`);
294
- } catch (e) {
295
- console.error(`[CrvyRprtr] Failed to save screenshot: ${attachment.path}`, e);
296
- const fallbackData = {
297
- name: attachment.name,
298
- path: attachment.path,
299
- contentType: attachment.contentType
300
- };
301
- savedAttachments.push(fallbackData);
302
- }
303
- })
384
+ await Promise.all(
385
+ result.attachments.filter(
386
+ (attachment) => attachment.contentType === "image/png" && attachment.path !== void 0
387
+ ).map(
388
+ (attachment) => limit(async () => {
389
+ try {
390
+ const artifactPath = safeArtifactPath(attachment.name);
391
+ const destPath = join(testScreenshotDir, artifactPath);
392
+ await mkdir2(dirname2(destPath), { recursive: true });
393
+ await copyFile(attachment.path, destPath);
394
+ savedAttachments.push({
395
+ name: attachment.name,
396
+ path: `${safeTestId}/${artifactPath}`,
397
+ contentType: attachment.contentType
398
+ });
399
+ console.log(`[CrvyRprtr] Saved screenshot: ${destPath}`);
400
+ } catch (error) {
401
+ console.error(`[CrvyRprtr] Failed to save screenshot: ${attachment.path}`, error);
402
+ savedAttachments.push({
403
+ name: attachment.name,
404
+ path: attachment.path,
405
+ contentType: attachment.contentType
406
+ });
407
+ }
408
+ })
409
+ )
304
410
  );
305
- await Promise.all(attachmentPromises);
306
411
  return savedAttachments;
307
412
  }
308
413
  sanitizeId(id) {
309
414
  return id.replace(/[^a-zA-Z0-9-_]/g, "_");
310
415
  }
311
416
  async writeOfflineReport() {
312
- if (this.runEvents.length === 0) {
313
- console.log("[CrvyRprtr] No offline events to write");
314
- return;
315
- }
417
+ if (this.runEvents.length === 0) console.log("[CrvyRprtr] No offline events to write");
418
+ if (this.runEvents.length === 0) return;
316
419
  try {
317
420
  const report = {
318
421
  version: 1,
319
422
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
320
423
  workers: this.workerIndex + 1,
321
- events: this.runEvents.map((e) => ({
322
- ...e,
323
- timestamp: Date.now(),
324
- workerIndex: this.workerIndex
325
- }))
424
+ events: this.runEvents.map((event) => ({ ...event, timestamp: Date.now(), workerIndex: this.workerIndex }))
326
425
  };
327
426
  await writeFile2(this.offlineReportPath, JSON.stringify(report, null, 2));
328
427
  console.log(`[CrvyRprtr] Wrote offline report: ${this.offlineReportPath}`);
329
- } catch (e) {
330
- console.error("[CrvyRprtr] Failed to write offline report:", e);
428
+ } catch (error) {
429
+ console.error("[CrvyRprtr] Failed to write offline report:", error);
331
430
  }
332
431
  }
333
432
  async writeStaticArtifact() {
@@ -338,51 +437,35 @@ var CrvyRprtr = class {
338
437
  reportHtmlPath: this.reportHtmlPath
339
438
  });
340
439
  console.log(`[CrvyRprtr] Wrote report artifact: ${this.reportHtmlPath}`);
341
- } catch (e) {
342
- console.error("[CrvyRprtr] Failed to write report artifact:", e);
440
+ } catch (error) {
441
+ console.error("[CrvyRprtr] Failed to write report artifact:", error);
343
442
  }
344
443
  }
345
444
  async onEnd(result) {
346
- this.send({
347
- type: "run-end",
348
- data: {
349
- status: result.status
350
- }
351
- });
445
+ this.send({ type: "run-end", data: { status: result.status } });
352
446
  await this.writeStaticArtifact();
353
- if (this.hadOfflineMode) {
354
- await this.writeOfflineReport();
355
- }
447
+ if (this.hadOfflineMode) await this.writeOfflineReport();
356
448
  await new Promise((resolve2) => {
357
- if (!this.ws || this.ws.readyState === WebSocket.CLOSED) {
358
- resolve2();
359
- return;
360
- }
361
- this.ws.onclose = () => {
362
- resolve2();
363
- };
364
- setTimeout(() => {
365
- this.ws?.close();
366
- resolve2();
367
- }, 1e3);
368
- this.ws.close();
449
+ if (this.ws && this.ws.readyState !== WebSocket.CLOSED) {
450
+ this.ws.onclose = () => {
451
+ resolve2();
452
+ };
453
+ setTimeout(() => {
454
+ this.ws?.close();
455
+ resolve2();
456
+ }, 1e3);
457
+ this.ws.close();
458
+ } else resolve2();
369
459
  });
370
460
  }
371
- send(msg) {
372
- const msgObj = msg;
373
- if (msgObj.type === "test-begin" || msgObj.type === "test-end" || msgObj.type === "run-end") {
374
- this.runEvents.push({
375
- type: msgObj.type,
376
- data: msgObj.data
377
- });
378
- }
379
- const payload = JSON.stringify(msg);
461
+ send(message) {
462
+ const event = message;
463
+ if (event.type === "test-begin" || event.type === "test-end" || event.type === "run-end")
464
+ this.runEvents.push({ type: event.type, data: event.data });
465
+ const payload = JSON.stringify(message);
380
466
  if (!this.isOfflineMode) {
381
- if (this.ws?.readyState === WebSocket.OPEN) {
382
- this.ws.send(payload);
383
- } else {
384
- this.queue.push(payload);
385
- }
467
+ if (this.ws?.readyState === WebSocket.OPEN) this.ws.send(payload);
468
+ else this.queue.push(payload);
386
469
  }
387
470
  }
388
471
  };