@crvy/rprtr 0.0.5 → 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;AAO9G,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;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,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;YAoBpD,qBAAqB;YA8CrB,eAAe;IAwC7B,OAAO,CAAC,UAAU;YAIJ,kBAAkB;YAyBlB,mBAAmB;IAa3B,KAAK,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IA8B9C,OAAO,CAAC,IAAI;CAkBb;AACD,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,11 +6,13 @@ import {
6
6
  applyTestEndEvent,
7
7
  createMutableReportState,
8
8
  finalizeRunEvent,
9
+ resolveBaselineTargets,
9
10
  safeParse
10
- } from "./chunk-MWLM3HWS.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 { existsSync } from "fs";
15
+ import { copyFile, mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
14
16
  import { dirname as dirname2, join } from "path";
15
17
  import pLimit from "p-limit";
16
18
 
@@ -139,28 +141,33 @@ async function writeReportArtifact(options) {
139
141
  var NAMED_SCREENSHOT_STEP_TITLE = /toHaveScreenshot\((.+?)\)/;
140
142
  var UNNAMED_SCREENSHOT_STEP_TITLE = /^Expect "toHaveScreenshot"(?:\s|$)/;
141
143
  var SYNTHETIC_SCREENSHOT_PREFIX = "__unnamed-screenshot-";
142
- function normalizeNamedScreenshot(titleMatch) {
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) {
143
152
  const unquotedName = titleMatch.trim().replace(/^['"`]|['"`]$/g, "");
144
153
  if (unquotedName === "") {
145
154
  return null;
146
155
  }
147
156
  const normalizedPath = unquotedName.replace(/\\/g, "/");
148
- const normalizedName = normalizedPath.replace(/\.png$/, "");
149
- if (normalizedName === "") {
157
+ const declaredName = normalizedPath.replace(/\.png$/, "");
158
+ if (declaredName === "") {
150
159
  return null;
151
160
  }
161
+ const occurrenceIndex = (state.namedOccurrences.get(declaredName) ?? 0) + 1;
162
+ state.namedOccurrences.set(declaredName, occurrenceIndex);
152
163
  return {
153
- visualName: normalizedName,
154
- snapshotBaseName: normalizedName
164
+ visualName: addVisualSuffix(declaredName, occurrenceIndex),
165
+ kind: "named",
166
+ declaredName,
167
+ snapshotBaseName: declaredName,
168
+ occurrenceIndex
155
169
  };
156
170
  }
157
- function appendDeclaration(state, declaration) {
158
- if (state.seenVisualNames.has(declaration.visualName)) {
159
- return;
160
- }
161
- state.seenVisualNames.add(declaration.visualName);
162
- state.declarations.push(declaration);
163
- }
164
171
  function visitStep(step, state) {
165
172
  const declarationsBeforeChildren = state.declarations.length;
166
173
  for (const nestedStep of step.steps) {
@@ -168,16 +175,19 @@ function visitStep(step, state) {
168
175
  }
169
176
  const namedMatch = step.title.match(NAMED_SCREENSHOT_STEP_TITLE);
170
177
  if (namedMatch?.[1] !== void 0) {
171
- const declaration = normalizeNamedScreenshot(namedMatch[1]);
178
+ const declaration = normalizeNamedScreenshot(namedMatch[1], state);
172
179
  if (declaration !== null) {
173
- appendDeclaration(state, declaration);
180
+ state.declarations.push(declaration);
174
181
  return true;
175
182
  }
176
183
  }
177
184
  const hasNestedScreenshotDeclaration = state.declarations.length > declarationsBeforeChildren;
178
185
  if (UNNAMED_SCREENSHOT_STEP_TITLE.test(step.title) && !hasNestedScreenshotDeclaration) {
179
- appendDeclaration(state, {
180
- visualName: `${SYNTHETIC_SCREENSHOT_PREFIX}${state.nextUnnamedIndex}`
186
+ const occurrenceIndex = state.nextUnnamedIndex;
187
+ state.declarations.push({
188
+ visualName: `${SYNTHETIC_SCREENSHOT_PREFIX}${occurrenceIndex}`,
189
+ kind: "unnamed",
190
+ occurrenceIndex
181
191
  });
182
192
  state.nextUnnamedIndex += 1;
183
193
  return true;
@@ -187,7 +197,7 @@ function visitStep(step, state) {
187
197
  function extractScreenshotDeclarations(steps) {
188
198
  const state = {
189
199
  declarations: [],
190
- seenVisualNames: /* @__PURE__ */ new Set(),
200
+ namedOccurrences: /* @__PURE__ */ new Map(),
191
201
  nextUnnamedIndex: 1
192
202
  };
193
203
  for (const step of steps) {
@@ -197,7 +207,19 @@ function extractScreenshotDeclarations(steps) {
197
207
  }
198
208
 
199
209
  // src/reporter.ts
210
+ var CURRENT_DIRECTORY_ARTIFACT_SEGMENT = "+dot+";
211
+ var PARENT_DIRECTORY_ARTIFACT_SEGMENT = "+dotdot+";
200
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("/");
201
223
  var CrvyRprtr = class {
202
224
  ws = null;
203
225
  serverUrl;
@@ -206,6 +228,11 @@ var CrvyRprtr = class {
206
228
  workerIndex;
207
229
  offlineReportPath;
208
230
  reportHtmlPath;
231
+ configDir = process.cwd();
232
+ testMetadata = /* @__PURE__ */ new Map();
233
+ playwrightSnapshotDir;
234
+ playwrightSnapshotPathTemplate;
235
+ playwrightToHaveScreenshotPathTemplate;
209
236
  isOfflineMode = false;
210
237
  hadOfflineMode = false;
211
238
  runEvents = [];
@@ -215,8 +242,12 @@ var CrvyRprtr = class {
215
242
  this.workerIndex = parseInt(process.env.TEST_WORKER_INDEX ?? "0", 10) || 0;
216
243
  this.offlineReportPath = options.offlineReportPath ?? `./crvy-rprtr-${this.workerIndex}.json`;
217
244
  this.reportHtmlPath = options.reportHtmlPath ?? "./crvy-rprtr.html";
245
+ this.playwrightSnapshotDir = options.playwrightSnapshotDir;
246
+ this.playwrightSnapshotPathTemplate = options.playwrightSnapshotPathTemplate;
247
+ this.playwrightToHaveScreenshotPathTemplate = options.playwrightToHaveScreenshotPathTemplate;
218
248
  }
219
249
  async onBegin(config, suite) {
250
+ this.configDir = config.configFile === void 0 ? config.rootDir : dirname2(config.configFile);
220
251
  console.log(`[CrvyRprtr] Starting run with ${suite.allTests().length} tests`);
221
252
  await mkdir2(this.screenshotDir, { recursive: true });
222
253
  this.connect();
@@ -233,7 +264,7 @@ var CrvyRprtr = class {
233
264
  this.ws.onopen = () => {
234
265
  console.log("[CrvyRprtr] Connected to Crvy Rprtr server");
235
266
  this.isOfflineMode = false;
236
- for (const msg of this.queue) this.ws.send(msg);
267
+ for (const message of this.queue) this.ws.send(message);
237
268
  this.queue = [];
238
269
  };
239
270
  this.ws.onerror = (error) => {
@@ -244,144 +275,158 @@ var CrvyRprtr = class {
244
275
  console.log("[CrvyRprtr] Disconnected from Crvy Rprtr server");
245
276
  this.enableOfflineMode();
246
277
  };
247
- } catch (e) {
248
- console.error("[CrvyRprtr] Failed to connect:", e);
278
+ } catch (error) {
279
+ console.error("[CrvyRprtr] Failed to connect:", error);
249
280
  this.enableOfflineMode();
250
281
  }
251
282
  }
252
283
  enableOfflineMode() {
253
- if (!this.isOfflineMode) {
254
- this.isOfflineMode = true;
255
- this.hadOfflineMode = true;
256
- console.log("[CrvyRprtr] Offline mode enabled - events will be queued to file");
257
- }
284
+ if (this.isOfflineMode) return;
285
+ this.isOfflineMode = this.hadOfflineMode = true;
286
+ console.log("[CrvyRprtr] Offline mode enabled - events will be queued to file");
258
287
  }
259
- onTestBegin(test) {
288
+ describeTitlePath(test) {
260
289
  const titlePath = [];
261
- let suite = test.parent;
262
- while (suite && suite.type === "describe") {
290
+ for (let suite = test.parent; suite?.type === "describe"; suite = suite.parent)
263
291
  titlePath.unshift(suite.title);
264
- suite = suite.parent;
265
- }
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) });
266
299
  this.send({
267
300
  type: "test-begin",
268
301
  data: {
269
302
  id: test.id,
270
303
  title: test.title,
271
- titlePath,
304
+ titlePath: this.describeTitlePath(test),
272
305
  browser: test.parent.project()?.name ?? "chromium",
273
- location: {
274
- file: test.location.file,
275
- line: test.location.line
276
- }
306
+ location: { file: test.location.file, line: test.location.line }
277
307
  }
278
308
  });
279
309
  }
280
310
  async onTestEnd(test, result) {
281
311
  const screenshotDeclarations = extractScreenshotDeclarations(result.steps);
282
- const visualNames = screenshotDeclarations.map(({ visualName }) => visualName);
283
312
  const savedAttachments = await this.saveAttachments(test.id, result);
284
- await this.copySnapshotBaselines(test, result.status, screenshotDeclarations, savedAttachments);
285
- this.send({
286
- type: "test-end",
287
- data: {
288
- id: test.id,
289
- title: test.title,
290
- status: result.status,
291
- attachments: savedAttachments,
292
- visualNames,
293
- error: result.errors.length > 0 ? result.errors[0]?.message : void 0,
294
- duration: result.duration
295
- }
296
- });
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
+ };
351
+ }
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
+ }
297
363
  }
298
364
  async copySnapshotBaselines(test, status, screenshotDeclarations, savedAttachments) {
299
- if (status !== "passed") return;
300
- const namedDeclarations = screenshotDeclarations.filter(
301
- (declaration) => declaration.snapshotBaseName !== void 0
302
- );
303
- if (namedDeclarations.length === 0) return;
304
- const projectName = test.parent.project()?.name;
305
- const snapshotDir = `${test.location.file}-snapshots`;
306
- const testScreenshotDir = join(this.screenshotDir, this.sanitizeId(test.id));
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);
307
372
  const limit = pLimit(MAX_CONCURRENT_FILE_OPS);
308
- const copyPromises = namedDeclarations.map(
309
- ({ visualName, snapshotBaseName }) => limit(async () => {
310
- const destName = `${visualName}-expected.png`;
311
- const destPath = join(testScreenshotDir, destName);
312
- const snapshotPath = projectName === "" ? join(snapshotDir, `${snapshotBaseName}-${process.platform}.png`) : join(snapshotDir, `${snapshotBaseName}-${projectName ?? "chromium"}-${process.platform}.png`);
313
- try {
314
- await mkdir2(dirname2(destPath), { recursive: true });
315
- await copyFile(snapshotPath, destPath);
316
- savedAttachments.push({
317
- name: destName,
318
- path: `${this.sanitizeId(test.id)}/${destName}`,
319
- contentType: "image/png"
320
- });
321
- console.log(`[CrvyRprtr] Attached baseline: ${snapshotPath}`);
322
- } catch {
323
- }
324
- })
373
+ await Promise.all(
374
+ targets.map(
375
+ (target) => limit(() => this.copyResolvedBaseline(safeTestId, testScreenshotDir, target, savedAttachments))
376
+ )
325
377
  );
326
- await Promise.all(copyPromises);
327
378
  }
328
379
  async saveAttachments(testId, result) {
329
380
  const savedAttachments = [];
330
- const testScreenshotDir = join(this.screenshotDir, this.sanitizeId(testId));
381
+ const safeTestId = this.sanitizeId(testId);
382
+ const testScreenshotDir = join(this.screenshotDir, safeTestId);
331
383
  const limit = pLimit(MAX_CONCURRENT_FILE_OPS);
332
- const attachmentPromises = result.attachments.filter(
333
- (attachment) => attachment.contentType === "image/png" && attachment.path !== void 0
334
- ).map(
335
- (attachment) => limit(async () => {
336
- try {
337
- await mkdir2(testScreenshotDir, { recursive: true });
338
- const fileName = attachment.name;
339
- const destPath = join(testScreenshotDir, fileName);
340
- await copyFile(attachment.path, destPath);
341
- const attachmentData = {
342
- name: attachment.name,
343
- path: `${this.sanitizeId(testId)}/${fileName}`,
344
- contentType: attachment.contentType
345
- };
346
- savedAttachments.push(attachmentData);
347
- console.log(`[CrvyRprtr] Saved screenshot: ${destPath}`);
348
- } catch (e) {
349
- console.error(`[CrvyRprtr] Failed to save screenshot: ${attachment.path}`, e);
350
- const fallbackData = {
351
- name: attachment.name,
352
- path: attachment.path,
353
- contentType: attachment.contentType
354
- };
355
- savedAttachments.push(fallbackData);
356
- }
357
- })
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
+ )
358
410
  );
359
- await Promise.all(attachmentPromises);
360
411
  return savedAttachments;
361
412
  }
362
413
  sanitizeId(id) {
363
414
  return id.replace(/[^a-zA-Z0-9-_]/g, "_");
364
415
  }
365
416
  async writeOfflineReport() {
366
- if (this.runEvents.length === 0) {
367
- console.log("[CrvyRprtr] No offline events to write");
368
- return;
369
- }
417
+ if (this.runEvents.length === 0) console.log("[CrvyRprtr] No offline events to write");
418
+ if (this.runEvents.length === 0) return;
370
419
  try {
371
420
  const report = {
372
421
  version: 1,
373
422
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
374
423
  workers: this.workerIndex + 1,
375
- events: this.runEvents.map((e) => ({
376
- ...e,
377
- timestamp: Date.now(),
378
- workerIndex: this.workerIndex
379
- }))
424
+ events: this.runEvents.map((event) => ({ ...event, timestamp: Date.now(), workerIndex: this.workerIndex }))
380
425
  };
381
426
  await writeFile2(this.offlineReportPath, JSON.stringify(report, null, 2));
382
427
  console.log(`[CrvyRprtr] Wrote offline report: ${this.offlineReportPath}`);
383
- } catch (e) {
384
- console.error("[CrvyRprtr] Failed to write offline report:", e);
428
+ } catch (error) {
429
+ console.error("[CrvyRprtr] Failed to write offline report:", error);
385
430
  }
386
431
  }
387
432
  async writeStaticArtifact() {
@@ -392,51 +437,35 @@ var CrvyRprtr = class {
392
437
  reportHtmlPath: this.reportHtmlPath
393
438
  });
394
439
  console.log(`[CrvyRprtr] Wrote report artifact: ${this.reportHtmlPath}`);
395
- } catch (e) {
396
- console.error("[CrvyRprtr] Failed to write report artifact:", e);
440
+ } catch (error) {
441
+ console.error("[CrvyRprtr] Failed to write report artifact:", error);
397
442
  }
398
443
  }
399
444
  async onEnd(result) {
400
- this.send({
401
- type: "run-end",
402
- data: {
403
- status: result.status
404
- }
405
- });
445
+ this.send({ type: "run-end", data: { status: result.status } });
406
446
  await this.writeStaticArtifact();
407
- if (this.hadOfflineMode) {
408
- await this.writeOfflineReport();
409
- }
447
+ if (this.hadOfflineMode) await this.writeOfflineReport();
410
448
  await new Promise((resolve2) => {
411
- if (!this.ws || this.ws.readyState === WebSocket.CLOSED) {
412
- resolve2();
413
- return;
414
- }
415
- this.ws.onclose = () => {
416
- resolve2();
417
- };
418
- setTimeout(() => {
419
- this.ws?.close();
420
- resolve2();
421
- }, 1e3);
422
- 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();
423
459
  });
424
460
  }
425
- send(msg) {
426
- const msgObj = msg;
427
- if (msgObj.type === "test-begin" || msgObj.type === "test-end" || msgObj.type === "run-end") {
428
- this.runEvents.push({
429
- type: msgObj.type,
430
- data: msgObj.data
431
- });
432
- }
433
- 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);
434
466
  if (!this.isOfflineMode) {
435
- if (this.ws?.readyState === WebSocket.OPEN) {
436
- this.ws.send(payload);
437
- } else {
438
- this.queue.push(payload);
439
- }
467
+ if (this.ws?.readyState === WebSocket.OPEN) this.ws.send(payload);
468
+ else this.queue.push(payload);
440
469
  }
441
470
  }
442
471
  };