@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.
package/dist/reporter.cjs CHANGED
@@ -34,8 +34,9 @@ __export(reporter_exports, {
34
34
  default: () => reporter_default
35
35
  });
36
36
  module.exports = __toCommonJS(reporter_exports);
37
+ var import_fs = require("fs");
37
38
  var import_promises2 = require("fs/promises");
38
- var import_path2 = require("path");
39
+ var import_path3 = require("path");
39
40
  var import_p_limit = __toESM(require("p-limit"), 1);
40
41
 
41
42
  // src/report-artifact.ts
@@ -62,6 +63,12 @@ function withImageSource(image) {
62
63
  source: classifyImage(image)
63
64
  };
64
65
  }
66
+ function getDeclaredVisualNames(visualNames, visualDeclarations) {
67
+ return visualDeclarations?.map(({ visualName }) => visualName) ?? visualNames;
68
+ }
69
+ function copyVisualDeclarations(visualDeclarations) {
70
+ return visualDeclarations?.map((visualDeclaration) => ({ ...visualDeclaration }));
71
+ }
65
72
  function mergeDeclaredImages(images, visualNames) {
66
73
  const names = /* @__PURE__ */ new Set([...Object.keys(images), ...visualNames]);
67
74
  return Object.fromEntries(
@@ -181,10 +188,14 @@ function applyTestEndEvent(state, data, options = {}) {
181
188
  }
182
189
  test.status = mapStatus(data.status);
183
190
  const resultStatus = data.status === "passed" ? "success" : data.status === "failed" ? "failed" : "pending";
191
+ const visualDeclarations = copyVisualDeclarations(data.visualDeclarations);
184
192
  const images = preservePreviousPassingImages(
185
193
  test,
186
194
  data.status,
187
- mergeDeclaredImages(attachmentsToImages(data.attachments, options.screenshotsBaseUrl), data.visualNames)
195
+ mergeDeclaredImages(
196
+ attachmentsToImages(data.attachments, options.screenshotsBaseUrl),
197
+ getDeclaredVisualNames(data.visualNames, visualDeclarations)
198
+ )
188
199
  );
189
200
  const diffCount = countDiffImages(images);
190
201
  const hasDiffs = diffCount > 0;
@@ -196,6 +207,7 @@ function applyTestEndEvent(state, data, options = {}) {
196
207
  status: resultStatus,
197
208
  retries: 0,
198
209
  images,
210
+ visualDeclarations,
199
211
  error: data.error,
200
212
  duration: data.duration
201
213
  }
@@ -233,6 +245,20 @@ var ImagesSchema = import_zod.z.object({
233
245
  error: import_zod.z.string().optional(),
234
246
  source: VisualSourceSchema.optional()
235
247
  });
248
+ var ScreenshotDeclarationSchema = import_zod.z.discriminatedUnion("kind", [
249
+ import_zod.z.object({
250
+ visualName: import_zod.z.string(),
251
+ kind: import_zod.z.literal("named"),
252
+ declaredName: import_zod.z.string(),
253
+ snapshotBaseName: import_zod.z.string(),
254
+ occurrenceIndex: import_zod.z.number()
255
+ }),
256
+ import_zod.z.object({
257
+ visualName: import_zod.z.string(),
258
+ kind: import_zod.z.literal("unnamed"),
259
+ occurrenceIndex: import_zod.z.number()
260
+ })
261
+ ]);
236
262
  var AttachmentSchema = import_zod.z.object({
237
263
  name: import_zod.z.string(),
238
264
  path: import_zod.z.string(),
@@ -244,6 +270,7 @@ var TestResultSchema = import_zod.z.object({
244
270
  status: TestResultStatusSchema,
245
271
  retries: import_zod.z.number(),
246
272
  images: import_zod.z.record(import_zod.z.string(), ImagesSchema).optional(),
273
+ visualDeclarations: import_zod.z.array(ScreenshotDeclarationSchema).optional(),
247
274
  error: import_zod.z.string().optional(),
248
275
  duration: import_zod.z.number().optional()
249
276
  });
@@ -290,6 +317,10 @@ var TestEndDataSchema = import_zod.z.object({
290
317
  status: import_zod.z.enum(["passed", "failed", "skipped"]),
291
318
  attachments: import_zod.z.array(AttachmentSchema),
292
319
  visualNames: import_zod.z.array(import_zod.z.string()).default([]),
320
+ visualDeclarations: import_zod.z.preprocess(
321
+ (value) => value === null ? void 0 : value,
322
+ import_zod.z.array(ScreenshotDeclarationSchema).optional()
323
+ ),
293
324
  error: import_zod.z.string().optional(),
294
325
  duration: import_zod.z.number().optional()
295
326
  });
@@ -465,28 +496,33 @@ async function writeReportArtifact(options) {
465
496
  var NAMED_SCREENSHOT_STEP_TITLE = /toHaveScreenshot\((.+?)\)/;
466
497
  var UNNAMED_SCREENSHOT_STEP_TITLE = /^Expect "toHaveScreenshot"(?:\s|$)/;
467
498
  var SYNTHETIC_SCREENSHOT_PREFIX = "__unnamed-screenshot-";
468
- function normalizeNamedScreenshot(titleMatch) {
499
+ function addVisualSuffix(visualName, occurrenceIndex) {
500
+ if (occurrenceIndex === 1) {
501
+ return visualName;
502
+ }
503
+ const slashIndex = visualName.lastIndexOf("/");
504
+ return slashIndex === -1 ? `${visualName}-${occurrenceIndex - 1}` : `${visualName.slice(0, slashIndex + 1)}${visualName.slice(slashIndex + 1)}-${occurrenceIndex - 1}`;
505
+ }
506
+ function normalizeNamedScreenshot(titleMatch, state) {
469
507
  const unquotedName = titleMatch.trim().replace(/^['"`]|['"`]$/g, "");
470
508
  if (unquotedName === "") {
471
509
  return null;
472
510
  }
473
511
  const normalizedPath = unquotedName.replace(/\\/g, "/");
474
- const normalizedName = normalizedPath.replace(/\.png$/, "");
475
- if (normalizedName === "") {
512
+ const declaredName = normalizedPath.replace(/\.png$/, "");
513
+ if (declaredName === "") {
476
514
  return null;
477
515
  }
516
+ const occurrenceIndex = (state.namedOccurrences.get(declaredName) ?? 0) + 1;
517
+ state.namedOccurrences.set(declaredName, occurrenceIndex);
478
518
  return {
479
- visualName: normalizedName,
480
- snapshotBaseName: normalizedName
519
+ visualName: addVisualSuffix(declaredName, occurrenceIndex),
520
+ kind: "named",
521
+ declaredName,
522
+ snapshotBaseName: declaredName,
523
+ occurrenceIndex
481
524
  };
482
525
  }
483
- function appendDeclaration(state, declaration) {
484
- if (state.seenVisualNames.has(declaration.visualName)) {
485
- return;
486
- }
487
- state.seenVisualNames.add(declaration.visualName);
488
- state.declarations.push(declaration);
489
- }
490
526
  function visitStep(step, state) {
491
527
  const declarationsBeforeChildren = state.declarations.length;
492
528
  for (const nestedStep of step.steps) {
@@ -494,16 +530,19 @@ function visitStep(step, state) {
494
530
  }
495
531
  const namedMatch = step.title.match(NAMED_SCREENSHOT_STEP_TITLE);
496
532
  if (namedMatch?.[1] !== void 0) {
497
- const declaration = normalizeNamedScreenshot(namedMatch[1]);
533
+ const declaration = normalizeNamedScreenshot(namedMatch[1], state);
498
534
  if (declaration !== null) {
499
- appendDeclaration(state, declaration);
535
+ state.declarations.push(declaration);
500
536
  return true;
501
537
  }
502
538
  }
503
539
  const hasNestedScreenshotDeclaration = state.declarations.length > declarationsBeforeChildren;
504
540
  if (UNNAMED_SCREENSHOT_STEP_TITLE.test(step.title) && !hasNestedScreenshotDeclaration) {
505
- appendDeclaration(state, {
506
- visualName: `${SYNTHETIC_SCREENSHOT_PREFIX}${state.nextUnnamedIndex}`
541
+ const occurrenceIndex = state.nextUnnamedIndex;
542
+ state.declarations.push({
543
+ visualName: `${SYNTHETIC_SCREENSHOT_PREFIX}${occurrenceIndex}`,
544
+ kind: "unnamed",
545
+ occurrenceIndex
507
546
  });
508
547
  state.nextUnnamedIndex += 1;
509
548
  return true;
@@ -513,7 +552,7 @@ function visitStep(step, state) {
513
552
  function extractScreenshotDeclarations(steps) {
514
553
  const state = {
515
554
  declarations: [],
516
- seenVisualNames: /* @__PURE__ */ new Set(),
555
+ namedOccurrences: /* @__PURE__ */ new Map(),
517
556
  nextUnnamedIndex: 1
518
557
  };
519
558
  for (const step of steps) {
@@ -522,8 +561,176 @@ function extractScreenshotDeclarations(steps) {
522
561
  return state.declarations;
523
562
  }
524
563
 
564
+ // src/snapshot-path-resolver.ts
565
+ var import_crypto = require("crypto");
566
+ var import_path2 = require("path");
567
+ var DEFAULT_SCREENSHOT_TEMPLATE = "{snapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}{-projectName}{-snapshotSuffix}{ext}";
568
+ var WINDOWS_FILESYSTEM_FRIENDLY_LENGTH = 60;
569
+ function isUnsafeFilePathCharacter(character) {
570
+ const codePoint = character.codePointAt(0);
571
+ return codePoint !== void 0 && (codePoint <= 44 || codePoint >= 46 && codePoint <= 47 || codePoint >= 58 && codePoint <= 64 || codePoint >= 91 && codePoint <= 96 || codePoint >= 123 && codePoint <= 127);
572
+ }
573
+ function sanitizeForFilePath(value) {
574
+ return Array.from(value).reduce(
575
+ (state, character) => {
576
+ const unsafeCharacter = isUnsafeFilePathCharacter(character);
577
+ return unsafeCharacter ? state.previousCharacterWasUnsafe ? state : {
578
+ value: `${state.value}-`,
579
+ previousCharacterWasUnsafe: true
580
+ } : {
581
+ value: `${state.value}${character}`,
582
+ previousCharacterWasUnsafe: false
583
+ };
584
+ },
585
+ {
586
+ value: "",
587
+ previousCharacterWasUnsafe: false
588
+ }
589
+ ).value;
590
+ }
591
+ function trimLongString(value, length = WINDOWS_FILESYSTEM_FRIENDLY_LENGTH) {
592
+ if (value.length <= length) {
593
+ return value;
594
+ }
595
+ const hash = (0, import_crypto.createHash)("sha1").update(value).digest("hex");
596
+ const middle = `-${hash.slice(0, 5)}-`;
597
+ const start = Math.floor((length - middle.length) / 2);
598
+ const end = length - middle.length - start;
599
+ return value.slice(0, start) + middle + value.slice(-end);
600
+ }
601
+ function sanitizeFilePathBeforeExtension(filePath, extension = (0, import_path2.extname)(filePath)) {
602
+ const base = filePath.slice(0, filePath.length - extension.length);
603
+ return sanitizeForFilePath(base) + extension;
604
+ }
605
+ function addSuffixToFilePath(filePath, suffix) {
606
+ const extension = (0, import_path2.extname)(filePath);
607
+ return filePath.slice(0, filePath.length - extension.length) + suffix + extension;
608
+ }
609
+ function normalizedSnapshotDir(config) {
610
+ return (0, import_path2.resolve)(config.configDir, config.snapshotDir);
611
+ }
612
+ function templateValue(template, token, value) {
613
+ return template.replace(
614
+ new RegExp(`\\{(.)?${token}\\}`, "g"),
615
+ (_, prefix) => value === "" ? "" : `${prefix ?? ""}${value}`
616
+ );
617
+ }
618
+ function applyTemplate(input, nameArgument, extension) {
619
+ const template = input.config.toHaveScreenshotPathTemplate ?? input.config.snapshotPathTemplate ?? DEFAULT_SCREENSHOT_TEMPLATE;
620
+ const relativeTestFilePath = (0, import_path2.relative)(input.config.testDir, input.testFile);
621
+ const parsed = (0, import_path2.parse)(relativeTestFilePath);
622
+ const tokens = [
623
+ ["testDir", input.config.testDir],
624
+ ["snapshotDir", normalizedSnapshotDir(input.config)],
625
+ ["snapshotSuffix", input.config.snapshotSuffix],
626
+ ["testFileDir", parsed.dir],
627
+ ["platform", process.platform],
628
+ ["projectName", sanitizeForFilePath(input.config.projectName)],
629
+ ["testName", ""],
630
+ ["testFileName", parsed.base],
631
+ ["testFilePath", relativeTestFilePath],
632
+ ["arg", nameArgument],
633
+ ["ext", extension]
634
+ ];
635
+ const snapshotPath = tokens.reduce(
636
+ (currentTemplate, [token, value]) => templateValue(currentTemplate, token, value),
637
+ template
638
+ );
639
+ return (0, import_path2.resolve)(input.config.configDir, snapshotPath);
640
+ }
641
+ function removeExtension(filePath, extension = (0, import_path2.extname)(filePath)) {
642
+ return filePath.slice(0, filePath.length - extension.length);
643
+ }
644
+ function snapshotNameParts(declaredName) {
645
+ const extension = (0, import_path2.extname)(declaredName) || ".png";
646
+ return {
647
+ extension,
648
+ filePath: (0, import_path2.extname)(declaredName) === "" ? `${declaredName}${extension}` : declaredName
649
+ };
650
+ }
651
+ function filePathForOccurrence(filePath, occurrenceIndex) {
652
+ return occurrenceIndex === 1 ? filePath : addSuffixToFilePath(filePath, `-${occurrenceIndex - 1}`);
653
+ }
654
+ function createResolvedBaselineTarget(input, declaration, nameArgument, extension) {
655
+ return {
656
+ visualName: declaration.visualName,
657
+ attachmentBaseName: declaration.visualName,
658
+ artifactBaseName: sanitizeForFilePath(declaration.visualName),
659
+ snapshotPath: applyTemplate(input, nameArgument, extension)
660
+ };
661
+ }
662
+ function resolveStringCallTarget(input, declaration) {
663
+ const { extension, filePath } = snapshotNameParts(declaration.declaredName);
664
+ const occurrenceFilePath = filePathForOccurrence(filePath, declaration.occurrenceIndex);
665
+ const sanitizedNameWithExtension = sanitizeFilePathBeforeExtension(occurrenceFilePath, extension);
666
+ const nameArgument = removeExtension(sanitizedNameWithExtension, extension);
667
+ return createResolvedBaselineTarget(input, declaration, nameArgument, extension);
668
+ }
669
+ function resolveArrayCallTarget(input, declaration) {
670
+ const { extension, filePath } = snapshotNameParts(declaration.declaredName);
671
+ const occurrenceFilePath = filePathForOccurrence(filePath, declaration.occurrenceIndex);
672
+ const nameArgument = (0, import_path2.join)((0, import_path2.dirname)(occurrenceFilePath), (0, import_path2.basename)(occurrenceFilePath, extension));
673
+ return createResolvedBaselineTarget(input, declaration, nameArgument, extension);
674
+ }
675
+ function resolveNamedTarget(input, declaration) {
676
+ if (!declaration.declaredName.includes("/")) {
677
+ return resolveStringCallTarget(input, declaration);
678
+ }
679
+ const stringCallTarget = resolveStringCallTarget(input, declaration);
680
+ const arrayCallTarget = resolveArrayCallTarget(input, declaration);
681
+ if (stringCallTarget.snapshotPath === arrayCallTarget.snapshotPath) {
682
+ return stringCallTarget;
683
+ }
684
+ if (input.snapshotPathExists === void 0) {
685
+ return void 0;
686
+ }
687
+ const stringCallTargetExists = input.snapshotPathExists(stringCallTarget.snapshotPath);
688
+ const arrayCallTargetExists = input.snapshotPathExists(arrayCallTarget.snapshotPath);
689
+ if (stringCallTargetExists === arrayCallTargetExists) {
690
+ return void 0;
691
+ }
692
+ return stringCallTargetExists ? stringCallTarget : arrayCallTarget;
693
+ }
694
+ function reporterTitlesWithoutProjectAndFile(reporterTitlePath) {
695
+ return reporterTitlePath.slice(3).filter((part) => part !== "");
696
+ }
697
+ function anonymousName(reporterTitlePath, occurrenceIndex) {
698
+ const rawAnonymousName = `${reporterTitlesWithoutProjectAndFile(reporterTitlePath).join(" ")} ${occurrenceIndex}.png`;
699
+ return sanitizeFilePathBeforeExtension(trimLongString(rawAnonymousName), ".png");
700
+ }
701
+ function resolveTarget(input, declaration) {
702
+ switch (declaration.kind) {
703
+ case "named":
704
+ return typeof declaration.declaredName === "string" && declaration.declaredName !== "" ? resolveNamedTarget(input, declaration) : void 0;
705
+ case "unnamed": {
706
+ const anonymousFileName = anonymousName(input.reporterTitlePath, declaration.occurrenceIndex);
707
+ const extension = ".png";
708
+ const nameArgument = (0, import_path2.join)((0, import_path2.dirname)(anonymousFileName), (0, import_path2.basename)(anonymousFileName, extension));
709
+ return createResolvedBaselineTarget(input, declaration, nameArgument, extension);
710
+ }
711
+ }
712
+ }
713
+ function resolveBaselineTargets(input) {
714
+ return input.declarations.flatMap((declaration) => {
715
+ const resolvedTarget = resolveTarget(input, declaration);
716
+ return resolvedTarget === void 0 ? [] : [resolvedTarget];
717
+ });
718
+ }
719
+
525
720
  // src/reporter.ts
721
+ var CURRENT_DIRECTORY_ARTIFACT_SEGMENT = "+dot+";
722
+ var PARENT_DIRECTORY_ARTIFACT_SEGMENT = "+dotdot+";
526
723
  var MAX_CONCURRENT_FILE_OPS = 5;
724
+ var SAFE_ARTIFACT_CHARACTER = /^[A-Za-z0-9._-]$/;
725
+ var encodeArtifactPathSegment = (segment) => {
726
+ if (segment === ".") return CURRENT_DIRECTORY_ARTIFACT_SEGMENT;
727
+ if (segment === "..") return PARENT_DIRECTORY_ARTIFACT_SEGMENT;
728
+ return Array.from(
729
+ segment,
730
+ (character) => SAFE_ARTIFACT_CHARACTER.test(character) ? character : encodeURIComponent(character)
731
+ ).join("");
732
+ };
733
+ var safeArtifactPath = (name) => name.split("/").map(encodeArtifactPathSegment).join("/");
527
734
  var CrvyRprtr = class {
528
735
  ws = null;
529
736
  serverUrl;
@@ -532,6 +739,11 @@ var CrvyRprtr = class {
532
739
  workerIndex;
533
740
  offlineReportPath;
534
741
  reportHtmlPath;
742
+ configDir = process.cwd();
743
+ testMetadata = /* @__PURE__ */ new Map();
744
+ playwrightSnapshotDir;
745
+ playwrightSnapshotPathTemplate;
746
+ playwrightToHaveScreenshotPathTemplate;
535
747
  isOfflineMode = false;
536
748
  hadOfflineMode = false;
537
749
  runEvents = [];
@@ -541,8 +753,12 @@ var CrvyRprtr = class {
541
753
  this.workerIndex = parseInt(process.env.TEST_WORKER_INDEX ?? "0", 10) || 0;
542
754
  this.offlineReportPath = options.offlineReportPath ?? `./crvy-rprtr-${this.workerIndex}.json`;
543
755
  this.reportHtmlPath = options.reportHtmlPath ?? "./crvy-rprtr.html";
756
+ this.playwrightSnapshotDir = options.playwrightSnapshotDir;
757
+ this.playwrightSnapshotPathTemplate = options.playwrightSnapshotPathTemplate;
758
+ this.playwrightToHaveScreenshotPathTemplate = options.playwrightToHaveScreenshotPathTemplate;
544
759
  }
545
760
  async onBegin(config, suite) {
761
+ this.configDir = config.configFile === void 0 ? config.rootDir : (0, import_path3.dirname)(config.configFile);
546
762
  console.log(`[CrvyRprtr] Starting run with ${suite.allTests().length} tests`);
547
763
  await (0, import_promises2.mkdir)(this.screenshotDir, { recursive: true });
548
764
  this.connect();
@@ -559,7 +775,7 @@ var CrvyRprtr = class {
559
775
  this.ws.onopen = () => {
560
776
  console.log("[CrvyRprtr] Connected to Crvy Rprtr server");
561
777
  this.isOfflineMode = false;
562
- for (const msg of this.queue) this.ws.send(msg);
778
+ for (const message of this.queue) this.ws.send(message);
563
779
  this.queue = [];
564
780
  };
565
781
  this.ws.onerror = (error) => {
@@ -570,144 +786,158 @@ var CrvyRprtr = class {
570
786
  console.log("[CrvyRprtr] Disconnected from Crvy Rprtr server");
571
787
  this.enableOfflineMode();
572
788
  };
573
- } catch (e) {
574
- console.error("[CrvyRprtr] Failed to connect:", e);
789
+ } catch (error) {
790
+ console.error("[CrvyRprtr] Failed to connect:", error);
575
791
  this.enableOfflineMode();
576
792
  }
577
793
  }
578
794
  enableOfflineMode() {
579
- if (!this.isOfflineMode) {
580
- this.isOfflineMode = true;
581
- this.hadOfflineMode = true;
582
- console.log("[CrvyRprtr] Offline mode enabled - events will be queued to file");
583
- }
795
+ if (this.isOfflineMode) return;
796
+ this.isOfflineMode = this.hadOfflineMode = true;
797
+ console.log("[CrvyRprtr] Offline mode enabled - events will be queued to file");
584
798
  }
585
- onTestBegin(test) {
799
+ describeTitlePath(test) {
586
800
  const titlePath = [];
587
- let suite = test.parent;
588
- while (suite && suite.type === "describe") {
801
+ for (let suite = test.parent; suite?.type === "describe"; suite = suite.parent)
589
802
  titlePath.unshift(suite.title);
590
- suite = suite.parent;
591
- }
803
+ return titlePath;
804
+ }
805
+ reporterTitlePath(test) {
806
+ return typeof test.titlePath === "function" ? test.titlePath() : ["", test.parent.project()?.name ?? "chromium", test.location.file, ...this.describeTitlePath(test), test.title];
807
+ }
808
+ onTestBegin(test) {
809
+ this.testMetadata.set(test.id, { reporterTitlePath: this.reporterTitlePath(test) });
592
810
  this.send({
593
811
  type: "test-begin",
594
812
  data: {
595
813
  id: test.id,
596
814
  title: test.title,
597
- titlePath,
815
+ titlePath: this.describeTitlePath(test),
598
816
  browser: test.parent.project()?.name ?? "chromium",
599
- location: {
600
- file: test.location.file,
601
- line: test.location.line
602
- }
817
+ location: { file: test.location.file, line: test.location.line }
603
818
  }
604
819
  });
605
820
  }
606
821
  async onTestEnd(test, result) {
607
822
  const screenshotDeclarations = extractScreenshotDeclarations(result.steps);
608
- const visualNames = screenshotDeclarations.map(({ visualName }) => visualName);
609
823
  const savedAttachments = await this.saveAttachments(test.id, result);
610
- await this.copySnapshotBaselines(test, result.status, screenshotDeclarations, savedAttachments);
611
- this.send({
612
- type: "test-end",
613
- data: {
614
- id: test.id,
615
- title: test.title,
616
- status: result.status,
617
- attachments: savedAttachments,
618
- visualNames,
619
- error: result.errors.length > 0 ? result.errors[0]?.message : void 0,
620
- duration: result.duration
621
- }
622
- });
824
+ try {
825
+ await this.copySnapshotBaselines(test, result.status, screenshotDeclarations, savedAttachments);
826
+ this.send({
827
+ type: "test-end",
828
+ data: {
829
+ id: test.id,
830
+ title: test.title,
831
+ status: result.status,
832
+ attachments: savedAttachments,
833
+ visualNames: screenshotDeclarations.map(({ visualName }) => visualName),
834
+ visualDeclarations: screenshotDeclarations,
835
+ error: result.errors.length > 0 ? result.errors[0]?.message : void 0,
836
+ duration: result.duration
837
+ }
838
+ });
839
+ } finally {
840
+ this.testMetadata.delete(test.id);
841
+ }
842
+ }
843
+ baselineInput(test, shots) {
844
+ const project = test.parent.project();
845
+ const snapshotDir = this.playwrightSnapshotDir ?? project?.snapshotDir;
846
+ if (project === void 0 || typeof project.testDir !== "string" || typeof snapshotDir !== "string") return null;
847
+ return {
848
+ testFile: test.location.file,
849
+ reporterTitlePath: this.testMetadata.get(test.id)?.reporterTitlePath ?? this.reporterTitlePath(test),
850
+ declarations: shots,
851
+ config: {
852
+ configDir: this.configDir,
853
+ testDir: project.testDir,
854
+ snapshotDir,
855
+ projectName: project.name,
856
+ snapshotSuffix: process.platform,
857
+ snapshotPathTemplate: this.playwrightSnapshotPathTemplate,
858
+ toHaveScreenshotPathTemplate: this.playwrightToHaveScreenshotPathTemplate
859
+ },
860
+ snapshotPathExists: import_fs.existsSync
861
+ };
862
+ }
863
+ async copyResolvedBaseline(safeTestId, testScreenshotDir, target, savedAttachments) {
864
+ const attachmentName = `${target.attachmentBaseName}-expected.png`;
865
+ const artifactPath = safeArtifactPath(attachmentName);
866
+ const destPath = (0, import_path3.join)(testScreenshotDir, artifactPath);
867
+ try {
868
+ await (0, import_promises2.mkdir)((0, import_path3.dirname)(destPath), { recursive: true });
869
+ await (0, import_promises2.copyFile)(target.snapshotPath, destPath);
870
+ savedAttachments.push({ name: attachmentName, path: `${safeTestId}/${artifactPath}`, contentType: "image/png" });
871
+ console.log(`[CrvyRprtr] Attached baseline: ${target.snapshotPath}`);
872
+ } catch {
873
+ }
623
874
  }
624
875
  async copySnapshotBaselines(test, status, screenshotDeclarations, savedAttachments) {
625
- if (status !== "passed") return;
626
- const namedDeclarations = screenshotDeclarations.filter(
627
- (declaration) => declaration.snapshotBaseName !== void 0
628
- );
629
- if (namedDeclarations.length === 0) return;
630
- const projectName = test.parent.project()?.name;
631
- const snapshotDir = `${test.location.file}-snapshots`;
632
- const testScreenshotDir = (0, import_path2.join)(this.screenshotDir, this.sanitizeId(test.id));
876
+ if (status !== "passed" || screenshotDeclarations.length === 0) return;
877
+ const input = this.baselineInput(test, screenshotDeclarations);
878
+ if (input === null) return;
879
+ const targets = resolveBaselineTargets(input);
880
+ if (targets.length === 0) return;
881
+ const safeTestId = this.sanitizeId(test.id);
882
+ const testScreenshotDir = (0, import_path3.join)(this.screenshotDir, safeTestId);
633
883
  const limit = (0, import_p_limit.default)(MAX_CONCURRENT_FILE_OPS);
634
- const copyPromises = namedDeclarations.map(
635
- ({ visualName, snapshotBaseName }) => limit(async () => {
636
- const destName = `${visualName}-expected.png`;
637
- const destPath = (0, import_path2.join)(testScreenshotDir, destName);
638
- const snapshotPath = projectName === "" ? (0, import_path2.join)(snapshotDir, `${snapshotBaseName}-${process.platform}.png`) : (0, import_path2.join)(snapshotDir, `${snapshotBaseName}-${projectName ?? "chromium"}-${process.platform}.png`);
639
- try {
640
- await (0, import_promises2.mkdir)((0, import_path2.dirname)(destPath), { recursive: true });
641
- await (0, import_promises2.copyFile)(snapshotPath, destPath);
642
- savedAttachments.push({
643
- name: destName,
644
- path: `${this.sanitizeId(test.id)}/${destName}`,
645
- contentType: "image/png"
646
- });
647
- console.log(`[CrvyRprtr] Attached baseline: ${snapshotPath}`);
648
- } catch {
649
- }
650
- })
884
+ await Promise.all(
885
+ targets.map(
886
+ (target) => limit(() => this.copyResolvedBaseline(safeTestId, testScreenshotDir, target, savedAttachments))
887
+ )
651
888
  );
652
- await Promise.all(copyPromises);
653
889
  }
654
890
  async saveAttachments(testId, result) {
655
891
  const savedAttachments = [];
656
- const testScreenshotDir = (0, import_path2.join)(this.screenshotDir, this.sanitizeId(testId));
892
+ const safeTestId = this.sanitizeId(testId);
893
+ const testScreenshotDir = (0, import_path3.join)(this.screenshotDir, safeTestId);
657
894
  const limit = (0, import_p_limit.default)(MAX_CONCURRENT_FILE_OPS);
658
- const attachmentPromises = result.attachments.filter(
659
- (attachment) => attachment.contentType === "image/png" && attachment.path !== void 0
660
- ).map(
661
- (attachment) => limit(async () => {
662
- try {
663
- await (0, import_promises2.mkdir)(testScreenshotDir, { recursive: true });
664
- const fileName = attachment.name;
665
- const destPath = (0, import_path2.join)(testScreenshotDir, fileName);
666
- await (0, import_promises2.copyFile)(attachment.path, destPath);
667
- const attachmentData = {
668
- name: attachment.name,
669
- path: `${this.sanitizeId(testId)}/${fileName}`,
670
- contentType: attachment.contentType
671
- };
672
- savedAttachments.push(attachmentData);
673
- console.log(`[CrvyRprtr] Saved screenshot: ${destPath}`);
674
- } catch (e) {
675
- console.error(`[CrvyRprtr] Failed to save screenshot: ${attachment.path}`, e);
676
- const fallbackData = {
677
- name: attachment.name,
678
- path: attachment.path,
679
- contentType: attachment.contentType
680
- };
681
- savedAttachments.push(fallbackData);
682
- }
683
- })
895
+ await Promise.all(
896
+ result.attachments.filter(
897
+ (attachment) => attachment.contentType === "image/png" && attachment.path !== void 0
898
+ ).map(
899
+ (attachment) => limit(async () => {
900
+ try {
901
+ const artifactPath = safeArtifactPath(attachment.name);
902
+ const destPath = (0, import_path3.join)(testScreenshotDir, artifactPath);
903
+ await (0, import_promises2.mkdir)((0, import_path3.dirname)(destPath), { recursive: true });
904
+ await (0, import_promises2.copyFile)(attachment.path, destPath);
905
+ savedAttachments.push({
906
+ name: attachment.name,
907
+ path: `${safeTestId}/${artifactPath}`,
908
+ contentType: attachment.contentType
909
+ });
910
+ console.log(`[CrvyRprtr] Saved screenshot: ${destPath}`);
911
+ } catch (error) {
912
+ console.error(`[CrvyRprtr] Failed to save screenshot: ${attachment.path}`, error);
913
+ savedAttachments.push({
914
+ name: attachment.name,
915
+ path: attachment.path,
916
+ contentType: attachment.contentType
917
+ });
918
+ }
919
+ })
920
+ )
684
921
  );
685
- await Promise.all(attachmentPromises);
686
922
  return savedAttachments;
687
923
  }
688
924
  sanitizeId(id) {
689
925
  return id.replace(/[^a-zA-Z0-9-_]/g, "_");
690
926
  }
691
927
  async writeOfflineReport() {
692
- if (this.runEvents.length === 0) {
693
- console.log("[CrvyRprtr] No offline events to write");
694
- return;
695
- }
928
+ if (this.runEvents.length === 0) console.log("[CrvyRprtr] No offline events to write");
929
+ if (this.runEvents.length === 0) return;
696
930
  try {
697
931
  const report = {
698
932
  version: 1,
699
933
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
700
934
  workers: this.workerIndex + 1,
701
- events: this.runEvents.map((e) => ({
702
- ...e,
703
- timestamp: Date.now(),
704
- workerIndex: this.workerIndex
705
- }))
935
+ events: this.runEvents.map((event) => ({ ...event, timestamp: Date.now(), workerIndex: this.workerIndex }))
706
936
  };
707
937
  await (0, import_promises2.writeFile)(this.offlineReportPath, JSON.stringify(report, null, 2));
708
938
  console.log(`[CrvyRprtr] Wrote offline report: ${this.offlineReportPath}`);
709
- } catch (e) {
710
- console.error("[CrvyRprtr] Failed to write offline report:", e);
939
+ } catch (error) {
940
+ console.error("[CrvyRprtr] Failed to write offline report:", error);
711
941
  }
712
942
  }
713
943
  async writeStaticArtifact() {
@@ -718,51 +948,35 @@ var CrvyRprtr = class {
718
948
  reportHtmlPath: this.reportHtmlPath
719
949
  });
720
950
  console.log(`[CrvyRprtr] Wrote report artifact: ${this.reportHtmlPath}`);
721
- } catch (e) {
722
- console.error("[CrvyRprtr] Failed to write report artifact:", e);
951
+ } catch (error) {
952
+ console.error("[CrvyRprtr] Failed to write report artifact:", error);
723
953
  }
724
954
  }
725
955
  async onEnd(result) {
726
- this.send({
727
- type: "run-end",
728
- data: {
729
- status: result.status
730
- }
731
- });
956
+ this.send({ type: "run-end", data: { status: result.status } });
732
957
  await this.writeStaticArtifact();
733
- if (this.hadOfflineMode) {
734
- await this.writeOfflineReport();
735
- }
736
- await new Promise((resolve2) => {
737
- if (!this.ws || this.ws.readyState === WebSocket.CLOSED) {
738
- resolve2();
739
- return;
740
- }
741
- this.ws.onclose = () => {
742
- resolve2();
743
- };
744
- setTimeout(() => {
745
- this.ws?.close();
746
- resolve2();
747
- }, 1e3);
748
- this.ws.close();
958
+ if (this.hadOfflineMode) await this.writeOfflineReport();
959
+ await new Promise((resolve3) => {
960
+ if (this.ws && this.ws.readyState !== WebSocket.CLOSED) {
961
+ this.ws.onclose = () => {
962
+ resolve3();
963
+ };
964
+ setTimeout(() => {
965
+ this.ws?.close();
966
+ resolve3();
967
+ }, 1e3);
968
+ this.ws.close();
969
+ } else resolve3();
749
970
  });
750
971
  }
751
- send(msg) {
752
- const msgObj = msg;
753
- if (msgObj.type === "test-begin" || msgObj.type === "test-end" || msgObj.type === "run-end") {
754
- this.runEvents.push({
755
- type: msgObj.type,
756
- data: msgObj.data
757
- });
758
- }
759
- const payload = JSON.stringify(msg);
972
+ send(message) {
973
+ const event = message;
974
+ if (event.type === "test-begin" || event.type === "test-end" || event.type === "run-end")
975
+ this.runEvents.push({ type: event.type, data: event.data });
976
+ const payload = JSON.stringify(message);
760
977
  if (!this.isOfflineMode) {
761
- if (this.ws?.readyState === WebSocket.OPEN) {
762
- this.ws.send(payload);
763
- } else {
764
- this.queue.push(payload);
765
- }
978
+ if (this.ws?.readyState === WebSocket.OPEN) this.ws.send(payload);
979
+ else this.queue.push(payload);
766
980
  }
767
981
  }
768
982
  };