@crvy/rprtr 0.0.5 → 0.0.8

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,13 +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;
25
36
  private copySnapshotBaselines;
26
- private saveAttachments;
27
- private sanitizeId;
28
- private writeOfflineReport;
29
- private writeStaticArtifact;
30
37
  onEnd(result: FullResult): Promise<void>;
31
38
  private send;
32
39
  }
@@ -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;AAgB9G,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;AAED,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;gBAEtB,OAAO,GAAE,gBAAqB;IAWpC,OAAO,CAAC,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC;IAO9D,OAAO,CAAC,OAAO;IA6Bf,OAAO,CAAC,iBAAiB;IAMzB,OAAO,CAAC,iBAAiB;IAOzB,OAAO,CAAC,iBAAiB;IAMzB,WAAW,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI;IAc3B,SAAS,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAuBlE,OAAO,CAAC,aAAa;YAqBP,qBAAqB;IAqB7B,KAAK,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAoB9C,OAAO,CAAC,IAAI;CAUb;AAED,eAAe,SAAS,CAAA"}
package/dist/reporter.js CHANGED
@@ -6,11 +6,27 @@ import {
6
6
  applyTestEndEvent,
7
7
  createMutableReportState,
8
8
  finalizeRunEvent,
9
+ resolveBaselineTargets,
9
10
  safeParse
10
- } from "./chunk-MWLM3HWS.js";
11
+ } from "./chunk-EJRLZZ22.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 { mkdir as mkdir3 } from "fs/promises";
16
+ import { dirname as dirname3, join as join2 } from "path";
17
+ import pLimit2 from "p-limit";
18
+
19
+ // src/debug-log.ts
20
+ var isDebug = () => process.env.DEBUG !== void 0 && process.env.DEBUG !== "";
21
+ var log = (...args) => {
22
+ if (isDebug()) console.log(...args);
23
+ };
24
+ var logError = (...args) => {
25
+ if (isDebug()) console.error(...args);
26
+ };
27
+
28
+ // src/reporter-artifact-ops.ts
29
+ import { copyFile, mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
14
30
  import { dirname as dirname2, join } from "path";
15
31
  import pLimit from "p-limit";
16
32
 
@@ -135,32 +151,129 @@ async function writeReportArtifact(options) {
135
151
  await writeFile(reportHtmlPath, html);
136
152
  }
137
153
 
154
+ // src/reporter-artifact-ops.ts
155
+ var CURRENT_DIRECTORY_ARTIFACT_SEGMENT = "+dot+";
156
+ var PARENT_DIRECTORY_ARTIFACT_SEGMENT = "+dotdot+";
157
+ var MAX_CONCURRENT_FILE_OPS = 5;
158
+ var SAFE_ARTIFACT_CHARACTER = /^[A-Za-z0-9._-]$/;
159
+ function encodeArtifactPathSegment(segment) {
160
+ if (segment === ".") return CURRENT_DIRECTORY_ARTIFACT_SEGMENT;
161
+ if (segment === "..") return PARENT_DIRECTORY_ARTIFACT_SEGMENT;
162
+ return Array.from(
163
+ segment,
164
+ (character) => SAFE_ARTIFACT_CHARACTER.test(character) ? character : encodeURIComponent(character)
165
+ ).join("");
166
+ }
167
+ function safeArtifactPath(name) {
168
+ return name.split("/").map(encodeArtifactPathSegment).join("/");
169
+ }
170
+ function sanitizeId(id) {
171
+ return id.replace(/[^a-zA-Z0-9-_]/g, "_");
172
+ }
173
+ async function copyResolvedBaseline(safeTestId, testScreenshotDir, target, savedAttachments) {
174
+ const attachmentName = `${target.attachmentBaseName}-expected.png`;
175
+ const artifactPath = safeArtifactPath(attachmentName);
176
+ const destPath = join(testScreenshotDir, artifactPath);
177
+ try {
178
+ await mkdir2(dirname2(destPath), { recursive: true });
179
+ await copyFile(target.snapshotPath, destPath);
180
+ savedAttachments.push({ name: attachmentName, path: `${safeTestId}/${artifactPath}`, contentType: "image/png" });
181
+ log(`[CrvyRprtr] Attached baseline: ${target.snapshotPath}`);
182
+ } catch {
183
+ }
184
+ }
185
+ async function saveAttachments(screenshotDir, testId, result) {
186
+ const savedAttachments = [];
187
+ const safeTestId = sanitizeId(testId);
188
+ const testScreenshotDir = join(screenshotDir, safeTestId);
189
+ const limit = pLimit(MAX_CONCURRENT_FILE_OPS);
190
+ await Promise.all(
191
+ result.attachments.filter(
192
+ (attachment) => attachment.contentType === "image/png" && attachment.path !== void 0
193
+ ).map(
194
+ (attachment) => limit(async () => {
195
+ try {
196
+ const artifactPath = safeArtifactPath(attachment.name);
197
+ const destPath = join(testScreenshotDir, artifactPath);
198
+ await mkdir2(dirname2(destPath), { recursive: true });
199
+ await copyFile(attachment.path, destPath);
200
+ savedAttachments.push({
201
+ name: attachment.name,
202
+ path: `${safeTestId}/${artifactPath}`,
203
+ contentType: attachment.contentType ?? "image/png"
204
+ });
205
+ log(`[CrvyRprtr] Saved screenshot: ${destPath}`);
206
+ } catch (error) {
207
+ logError(`[CrvyRprtr] Failed to save screenshot: ${attachment.path}`, error);
208
+ savedAttachments.push({
209
+ name: attachment.name,
210
+ path: attachment.path,
211
+ contentType: attachment.contentType ?? "image/png"
212
+ });
213
+ }
214
+ })
215
+ )
216
+ );
217
+ return savedAttachments;
218
+ }
219
+ async function writeOfflineReport(runEvents, offlineReportPath, workerIndex) {
220
+ if (runEvents.length === 0) {
221
+ log("[CrvyRprtr] No offline events to write");
222
+ return;
223
+ }
224
+ try {
225
+ const report = {
226
+ version: 1,
227
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
228
+ workers: workerIndex + 1,
229
+ events: runEvents.map((event) => ({ ...event, timestamp: Date.now(), workerIndex }))
230
+ };
231
+ await writeFile2(offlineReportPath, JSON.stringify(report, null, 2));
232
+ log(`[CrvyRprtr] Wrote offline report: ${offlineReportPath}`);
233
+ } catch (error) {
234
+ logError("[CrvyRprtr] Failed to write offline report:", error);
235
+ }
236
+ }
237
+ async function writeStaticArtifact(runEvents, screenshotDir, reportHtmlPath) {
238
+ try {
239
+ await writeReportArtifact({ events: runEvents, screenshotDir, reportHtmlPath });
240
+ log(`[CrvyRprtr] Wrote report artifact: ${reportHtmlPath}`);
241
+ } catch (error) {
242
+ logError("[CrvyRprtr] Failed to write report artifact:", error);
243
+ }
244
+ }
245
+
138
246
  // src/reporter-utils.ts
139
247
  var NAMED_SCREENSHOT_STEP_TITLE = /toHaveScreenshot\((.+?)\)/;
140
248
  var UNNAMED_SCREENSHOT_STEP_TITLE = /^Expect "toHaveScreenshot"(?:\s|$)/;
141
249
  var SYNTHETIC_SCREENSHOT_PREFIX = "__unnamed-screenshot-";
142
- function normalizeNamedScreenshot(titleMatch) {
250
+ function addVisualSuffix(visualName, occurrenceIndex) {
251
+ if (occurrenceIndex === 1) {
252
+ return visualName;
253
+ }
254
+ const slashIndex = visualName.lastIndexOf("/");
255
+ return slashIndex === -1 ? `${visualName}-${occurrenceIndex - 1}` : `${visualName.slice(0, slashIndex + 1)}${visualName.slice(slashIndex + 1)}-${occurrenceIndex - 1}`;
256
+ }
257
+ function normalizeNamedScreenshot(titleMatch, state) {
143
258
  const unquotedName = titleMatch.trim().replace(/^['"`]|['"`]$/g, "");
144
259
  if (unquotedName === "") {
145
260
  return null;
146
261
  }
147
262
  const normalizedPath = unquotedName.replace(/\\/g, "/");
148
- const normalizedName = normalizedPath.replace(/\.png$/, "");
149
- if (normalizedName === "") {
263
+ const declaredName = normalizedPath.replace(/\.png$/, "");
264
+ if (declaredName === "") {
150
265
  return null;
151
266
  }
267
+ const occurrenceIndex = (state.namedOccurrences.get(declaredName) ?? 0) + 1;
268
+ state.namedOccurrences.set(declaredName, occurrenceIndex);
152
269
  return {
153
- visualName: normalizedName,
154
- snapshotBaseName: normalizedName
270
+ visualName: addVisualSuffix(declaredName, occurrenceIndex),
271
+ kind: "named",
272
+ declaredName,
273
+ snapshotBaseName: declaredName,
274
+ occurrenceIndex
155
275
  };
156
276
  }
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
277
  function visitStep(step, state) {
165
278
  const declarationsBeforeChildren = state.declarations.length;
166
279
  for (const nestedStep of step.steps) {
@@ -168,16 +281,19 @@ function visitStep(step, state) {
168
281
  }
169
282
  const namedMatch = step.title.match(NAMED_SCREENSHOT_STEP_TITLE);
170
283
  if (namedMatch?.[1] !== void 0) {
171
- const declaration = normalizeNamedScreenshot(namedMatch[1]);
284
+ const declaration = normalizeNamedScreenshot(namedMatch[1], state);
172
285
  if (declaration !== null) {
173
- appendDeclaration(state, declaration);
286
+ state.declarations.push(declaration);
174
287
  return true;
175
288
  }
176
289
  }
177
290
  const hasNestedScreenshotDeclaration = state.declarations.length > declarationsBeforeChildren;
178
291
  if (UNNAMED_SCREENSHOT_STEP_TITLE.test(step.title) && !hasNestedScreenshotDeclaration) {
179
- appendDeclaration(state, {
180
- visualName: `${SYNTHETIC_SCREENSHOT_PREFIX}${state.nextUnnamedIndex}`
292
+ const occurrenceIndex = state.nextUnnamedIndex;
293
+ state.declarations.push({
294
+ visualName: `${SYNTHETIC_SCREENSHOT_PREFIX}${occurrenceIndex}`,
295
+ kind: "unnamed",
296
+ occurrenceIndex
181
297
  });
182
298
  state.nextUnnamedIndex += 1;
183
299
  return true;
@@ -187,7 +303,7 @@ function visitStep(step, state) {
187
303
  function extractScreenshotDeclarations(steps) {
188
304
  const state = {
189
305
  declarations: [],
190
- seenVisualNames: /* @__PURE__ */ new Set(),
306
+ namedOccurrences: /* @__PURE__ */ new Map(),
191
307
  nextUnnamedIndex: 1
192
308
  };
193
309
  for (const step of steps) {
@@ -197,7 +313,6 @@ function extractScreenshotDeclarations(steps) {
197
313
  }
198
314
 
199
315
  // src/reporter.ts
200
- var MAX_CONCURRENT_FILE_OPS = 5;
201
316
  var CrvyRprtr = class {
202
317
  ws = null;
203
318
  serverUrl;
@@ -206,6 +321,11 @@ var CrvyRprtr = class {
206
321
  workerIndex;
207
322
  offlineReportPath;
208
323
  reportHtmlPath;
324
+ configDir = process.cwd();
325
+ testMetadata = /* @__PURE__ */ new Map();
326
+ playwrightSnapshotDir;
327
+ playwrightSnapshotPathTemplate;
328
+ playwrightToHaveScreenshotPathTemplate;
209
329
  isOfflineMode = false;
210
330
  hadOfflineMode = false;
211
331
  runEvents = [];
@@ -215,198 +335,132 @@ var CrvyRprtr = class {
215
335
  this.workerIndex = parseInt(process.env.TEST_WORKER_INDEX ?? "0", 10) || 0;
216
336
  this.offlineReportPath = options.offlineReportPath ?? `./crvy-rprtr-${this.workerIndex}.json`;
217
337
  this.reportHtmlPath = options.reportHtmlPath ?? "./crvy-rprtr.html";
338
+ this.playwrightSnapshotDir = options.playwrightSnapshotDir;
339
+ this.playwrightSnapshotPathTemplate = options.playwrightSnapshotPathTemplate;
340
+ this.playwrightToHaveScreenshotPathTemplate = options.playwrightToHaveScreenshotPathTemplate;
218
341
  }
219
342
  async onBegin(config, suite) {
220
- console.log(`[CrvyRprtr] Starting run with ${suite.allTests().length} tests`);
221
- await mkdir2(this.screenshotDir, { recursive: true });
343
+ this.configDir = config.configFile === void 0 ? config.rootDir : dirname3(config.configFile);
344
+ log(`[CrvyRprtr] Starting run with ${suite.allTests().length} tests`);
345
+ await mkdir3(this.screenshotDir, { recursive: true });
222
346
  this.connect();
223
347
  }
224
348
  connect() {
225
349
  const WebSocketConstructor = globalThis.WebSocket;
226
350
  if (typeof WebSocketConstructor !== "function") {
227
- console.log("[CrvyRprtr] WebSocket unavailable in current runtime; offline mode enabled");
351
+ log("[CrvyRprtr] WebSocket unavailable in current runtime; offline mode enabled");
228
352
  this.enableOfflineMode();
229
353
  return;
230
354
  }
231
355
  try {
232
356
  this.ws = new WebSocketConstructor(this.serverUrl);
233
357
  this.ws.onopen = () => {
234
- console.log("[CrvyRprtr] Connected to Crvy Rprtr server");
358
+ log("[CrvyRprtr] Connected to Crvy Rprtr server");
235
359
  this.isOfflineMode = false;
236
- for (const msg of this.queue) this.ws.send(msg);
360
+ for (const message of this.queue) this.ws.send(message);
237
361
  this.queue = [];
238
362
  };
239
363
  this.ws.onerror = (error) => {
240
- console.error("[CrvyRprtr] WebSocket error:", error);
364
+ logError("[CrvyRprtr] WebSocket error:", error);
241
365
  this.enableOfflineMode();
242
366
  };
243
367
  this.ws.onclose = () => {
244
- console.log("[CrvyRprtr] Disconnected from Crvy Rprtr server");
368
+ log("[CrvyRprtr] Disconnected from Crvy Rprtr server");
245
369
  this.enableOfflineMode();
246
370
  };
247
- } catch (e) {
248
- console.error("[CrvyRprtr] Failed to connect:", e);
371
+ } catch (error) {
372
+ logError("[CrvyRprtr] Failed to connect:", error);
249
373
  this.enableOfflineMode();
250
374
  }
251
375
  }
252
376
  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
- }
377
+ if (this.isOfflineMode) return;
378
+ this.isOfflineMode = this.hadOfflineMode = true;
379
+ log("[CrvyRprtr] Offline mode enabled - events will be queued to file");
258
380
  }
259
- onTestBegin(test) {
381
+ describeTitlePath(test) {
260
382
  const titlePath = [];
261
- let suite = test.parent;
262
- while (suite && suite.type === "describe") {
383
+ for (let suite = test.parent; suite?.type === "describe"; suite = suite.parent)
263
384
  titlePath.unshift(suite.title);
264
- suite = suite.parent;
265
- }
385
+ return titlePath;
386
+ }
387
+ reporterTitlePath(test) {
388
+ return typeof test.titlePath === "function" ? test.titlePath() : ["", test.parent.project()?.name ?? "chromium", test.location.file, ...this.describeTitlePath(test), test.title];
389
+ }
390
+ onTestBegin(test) {
391
+ this.testMetadata.set(test.id, { reporterTitlePath: this.reporterTitlePath(test) });
266
392
  this.send({
267
393
  type: "test-begin",
268
394
  data: {
269
395
  id: test.id,
270
396
  title: test.title,
271
- titlePath,
397
+ titlePath: this.describeTitlePath(test),
272
398
  browser: test.parent.project()?.name ?? "chromium",
273
- location: {
274
- file: test.location.file,
275
- line: test.location.line
276
- }
399
+ location: { file: test.location.file, line: test.location.line }
277
400
  }
278
401
  });
279
402
  }
280
403
  async onTestEnd(test, result) {
281
404
  const screenshotDeclarations = extractScreenshotDeclarations(result.steps);
282
- const visualNames = screenshotDeclarations.map(({ visualName }) => visualName);
283
- 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
- });
297
- }
298
- 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));
307
- 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
- })
325
- );
326
- await Promise.all(copyPromises);
327
- }
328
- async saveAttachments(testId, result) {
329
- const savedAttachments = [];
330
- const testScreenshotDir = join(this.screenshotDir, this.sanitizeId(testId));
331
- 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
- })
358
- );
359
- await Promise.all(attachmentPromises);
360
- return savedAttachments;
361
- }
362
- sanitizeId(id) {
363
- return id.replace(/[^a-zA-Z0-9-_]/g, "_");
364
- }
365
- async writeOfflineReport() {
366
- if (this.runEvents.length === 0) {
367
- console.log("[CrvyRprtr] No offline events to write");
368
- return;
369
- }
370
- try {
371
- const report = {
372
- version: 1,
373
- generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
374
- workers: this.workerIndex + 1,
375
- events: this.runEvents.map((e) => ({
376
- ...e,
377
- timestamp: Date.now(),
378
- workerIndex: this.workerIndex
379
- }))
380
- };
381
- await writeFile2(this.offlineReportPath, JSON.stringify(report, null, 2));
382
- console.log(`[CrvyRprtr] Wrote offline report: ${this.offlineReportPath}`);
383
- } catch (e) {
384
- console.error("[CrvyRprtr] Failed to write offline report:", e);
385
- }
386
- }
387
- async writeStaticArtifact() {
405
+ const savedAttachments = await saveAttachments(this.screenshotDir, test.id, result);
388
406
  try {
389
- await writeReportArtifact({
390
- events: this.runEvents,
391
- screenshotDir: this.screenshotDir,
392
- reportHtmlPath: this.reportHtmlPath
407
+ await this.copySnapshotBaselines(test, result.status, screenshotDeclarations, savedAttachments);
408
+ this.send({
409
+ type: "test-end",
410
+ data: {
411
+ id: test.id,
412
+ title: test.title,
413
+ status: result.status,
414
+ attachments: savedAttachments,
415
+ visualNames: screenshotDeclarations.map(({ visualName }) => visualName),
416
+ visualDeclarations: screenshotDeclarations,
417
+ error: result.errors.length > 0 ? result.errors[0]?.message : void 0,
418
+ duration: result.duration
419
+ }
393
420
  });
394
- console.log(`[CrvyRprtr] Wrote report artifact: ${this.reportHtmlPath}`);
395
- } catch (e) {
396
- console.error("[CrvyRprtr] Failed to write report artifact:", e);
421
+ } finally {
422
+ this.testMetadata.delete(test.id);
397
423
  }
398
424
  }
425
+ baselineInput(test, shots) {
426
+ const project = test.parent.project();
427
+ const snapshotDir = this.playwrightSnapshotDir ?? project?.snapshotDir;
428
+ if (project === void 0 || typeof project.testDir !== "string" || typeof snapshotDir !== "string") return null;
429
+ return {
430
+ testFile: test.location.file,
431
+ reporterTitlePath: this.testMetadata.get(test.id)?.reporterTitlePath ?? this.reporterTitlePath(test),
432
+ declarations: shots,
433
+ config: {
434
+ configDir: this.configDir,
435
+ testDir: project.testDir,
436
+ snapshotDir,
437
+ projectName: project.name,
438
+ snapshotSuffix: process.platform,
439
+ snapshotPathTemplate: this.playwrightSnapshotPathTemplate,
440
+ toHaveScreenshotPathTemplate: this.playwrightToHaveScreenshotPathTemplate
441
+ },
442
+ snapshotPathExists: existsSync
443
+ };
444
+ }
445
+ async copySnapshotBaselines(test, status, screenshotDeclarations, savedAttachments) {
446
+ if (status !== "passed" || screenshotDeclarations.length === 0) return;
447
+ const input = this.baselineInput(test, screenshotDeclarations);
448
+ if (input === null) return;
449
+ const targets = resolveBaselineTargets(input);
450
+ if (targets.length === 0) return;
451
+ const safeTestId = sanitizeId(test.id);
452
+ const testScreenshotDir = join2(this.screenshotDir, safeTestId);
453
+ const limit = pLimit2(5);
454
+ await Promise.all(
455
+ targets.map(
456
+ (target) => limit(() => copyResolvedBaseline(safeTestId, testScreenshotDir, target, savedAttachments))
457
+ )
458
+ );
459
+ }
399
460
  async onEnd(result) {
400
- this.send({
401
- type: "run-end",
402
- data: {
403
- status: result.status
404
- }
405
- });
406
- await this.writeStaticArtifact();
407
- if (this.hadOfflineMode) {
408
- await this.writeOfflineReport();
409
- }
461
+ this.send({ type: "run-end", data: { status: result.status } });
462
+ await writeStaticArtifact(this.runEvents, this.screenshotDir, this.reportHtmlPath);
463
+ if (this.hadOfflineMode) await writeOfflineReport(this.runEvents, this.offlineReportPath, this.workerIndex);
410
464
  await new Promise((resolve2) => {
411
465
  if (!this.ws || this.ws.readyState === WebSocket.CLOSED) {
412
466
  resolve2();
@@ -422,21 +476,14 @@ var CrvyRprtr = class {
422
476
  this.ws.close();
423
477
  });
424
478
  }
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);
479
+ send(message) {
480
+ const event = message;
481
+ if (event.type === "test-begin" || event.type === "test-end" || event.type === "run-end")
482
+ this.runEvents.push({ type: event.type, data: event.data });
483
+ const payload = JSON.stringify(message);
434
484
  if (!this.isOfflineMode) {
435
- if (this.ws?.readyState === WebSocket.OPEN) {
436
- this.ws.send(payload);
437
- } else {
438
- this.queue.push(payload);
439
- }
485
+ if (this.ws?.readyState === WebSocket.OPEN) this.ws.send(payload);
486
+ else this.queue.push(payload);
440
487
  }
441
488
  }
442
489
  };