@crvy/rprtr 0.0.9 → 0.1.0

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
@@ -36,9 +36,15 @@ __export(reporter_exports, {
36
36
  module.exports = __toCommonJS(reporter_exports);
37
37
  var import_fs = require("fs");
38
38
  var import_promises3 = require("fs/promises");
39
- var import_path4 = require("path");
39
+ var import_path5 = require("path");
40
40
  var import_p_limit2 = __toESM(require("p-limit"), 1);
41
41
 
42
+ // src/ci.ts
43
+ function isCI(env = process.env) {
44
+ const ci = env.CI;
45
+ return ci !== void 0 && ci !== "" && ci !== "false" && ci !== "0";
46
+ }
47
+
42
48
  // src/debug-log.ts
43
49
  var isDebug = () => process.env.DEBUG !== void 0 && process.env.DEBUG !== "";
44
50
  var log = (...args) => {
@@ -55,10 +61,11 @@ var import_p_limit = __toESM(require("p-limit"), 1);
55
61
 
56
62
  // src/report-artifact.ts
57
63
  var import_promises = require("fs/promises");
58
- var import_path = require("path");
64
+ var import_path2 = require("path");
59
65
  var import_url = require("url");
60
66
 
61
67
  // src/report-utils.ts
68
+ var import_path = require("path");
62
69
  function normalizeScreenshotsBaseUrl(baseUrl) {
63
70
  return baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
64
71
  }
@@ -103,7 +110,7 @@ function attachmentsToImages(attachments, screenshotsBaseUrl = "/screenshots/")
103
110
  const role = match[2];
104
111
  if (baseName === null || baseName === void 0 || role === null || role === void 0) continue;
105
112
  images[baseName] ??= {};
106
- const url = `${baseUrl}${attachment.path}`;
113
+ const url = (0, import_path.isAbsolute)(attachment.path) ? `/file/${encodeURIComponent(attachment.path)}` : `${baseUrl}${attachment.path}`;
107
114
  const img = images[baseName];
108
115
  if (img !== null && img !== void 0) {
109
116
  if (role === "actual") img.actual = url;
@@ -315,10 +322,29 @@ var CrvyRprtrSuiteSchema = import_zod.z.lazy(
315
322
  children: import_zod.z.record(import_zod.z.string(), import_zod.z.union([CrvyRprtrSuiteSchema, CrvyRprtrTestSchema])).optional()
316
323
  })
317
324
  );
318
- var WebSocketMessageSchema = import_zod.z.object({
325
+ var IncomingWebSocketMessageSchema = import_zod.z.object({
319
326
  type: import_zod.z.enum(["test-begin", "test-end", "run-end", "approve", "sync"]),
320
327
  data: import_zod.z.unknown()
321
328
  });
329
+ var WebSocketMessageSchema = import_zod.z.discriminatedUnion("type", [
330
+ import_zod.z.object({ type: import_zod.z.literal("test-begin"), data: TestDataSchema }),
331
+ import_zod.z.object({ type: import_zod.z.literal("test-update"), data: TestDataSchema }),
332
+ import_zod.z.object({
333
+ type: import_zod.z.literal("run-end"),
334
+ data: import_zod.z.object({
335
+ status: import_zod.z.enum(["passed", "failed", "skipped"]),
336
+ removedTestIds: import_zod.z.array(import_zod.z.string())
337
+ })
338
+ }),
339
+ import_zod.z.object({
340
+ type: import_zod.z.literal("sync"),
341
+ data: import_zod.z.object({
342
+ tests: import_zod.z.record(import_zod.z.string(), TestDataSchema),
343
+ isUpdateMode: import_zod.z.boolean().optional()
344
+ })
345
+ }),
346
+ import_zod.z.object({ type: import_zod.z.literal("approve"), data: import_zod.z.unknown() })
347
+ ]);
322
348
  var TestBeginDataSchema = import_zod.z.object({
323
349
  id: import_zod.z.string(),
324
350
  title: import_zod.z.string(),
@@ -338,6 +364,9 @@ var TestEndDataSchema = import_zod.z.object({
338
364
  error: import_zod.z.string().optional(),
339
365
  duration: import_zod.z.number().optional()
340
366
  });
367
+ var RunEndDataSchema = import_zod.z.object({
368
+ status: import_zod.z.enum(["passed", "failed", "skipped"])
369
+ });
341
370
  var ReportDataSchema = import_zod.z.object({
342
371
  isRunning: import_zod.z.boolean(),
343
372
  tests: import_zod.z.record(import_zod.z.string(), TestDataSchema),
@@ -392,7 +421,7 @@ var import_meta = { url: require("url").pathToFileURL(__filename).href };
392
421
  var DEFAULT_REPORT_HTML_PATH = "./crvy-rprtr.html";
393
422
  var STATIC_ARTIFACT_APPROVAL_MESSAGE = "This artifact is read-only. Open it with the Crvy Rprtr server to approve screenshots.";
394
423
  function toBrowserPath(pathValue) {
395
- const normalized = pathValue.split(import_path.sep).join("/");
424
+ const normalized = pathValue.split(import_path2.sep).join("/");
396
425
  if (normalized === "") return ".";
397
426
  if (normalized.startsWith(".")) return normalized;
398
427
  return `./${normalized}`;
@@ -428,11 +457,11 @@ function renderArtifactHtml(template, bootstrapData, stylesheet, script) {
428
457
  );
429
458
  }
430
459
  async function getPackagedAssetPath(fileName) {
431
- const currentDir = (0, import_path.dirname)((0, import_url.fileURLToPath)(import_meta.url));
432
- const candidates = [currentDir, (0, import_path.resolve)(currentDir, "../dist")];
460
+ const currentDir = (0, import_path2.dirname)((0, import_url.fileURLToPath)(import_meta.url));
461
+ const candidates = [currentDir, (0, import_path2.resolve)(currentDir, "../dist")];
433
462
  const resolvedCandidates = await Promise.all(
434
463
  candidates.map(async (candidate) => {
435
- const assetPath = (0, import_path.resolve)(candidate, fileName);
464
+ const assetPath = (0, import_path2.resolve)(candidate, fileName);
436
465
  try {
437
466
  await (0, import_promises.access)(assetPath);
438
467
  return assetPath;
@@ -450,7 +479,7 @@ async function getPackagedAssetPath(fileName) {
450
479
  );
451
480
  }
452
481
  function buildStaticBootstrapData(events, screenshotDir, htmlPath) {
453
- const screenshotBaseUrl = toBrowserDirPath((0, import_path.relative)((0, import_path.dirname)(htmlPath), (0, import_path.resolve)(screenshotDir)));
482
+ const screenshotBaseUrl = toBrowserDirPath((0, import_path2.relative)((0, import_path2.dirname)(htmlPath), (0, import_path2.resolve)(screenshotDir)));
454
483
  const state = createMutableReportState(screenshotDir);
455
484
  for (const event of events) {
456
485
  switch (event.type) {
@@ -489,13 +518,13 @@ function buildStaticBootstrapData(events, screenshotDir, htmlPath) {
489
518
  return parsedBootstrapData;
490
519
  }
491
520
  async function writeReportArtifact(options) {
492
- const reportHtmlPath = (0, import_path.resolve)(options.reportHtmlPath ?? DEFAULT_REPORT_HTML_PATH);
521
+ const reportHtmlPath = (0, import_path2.resolve)(options.reportHtmlPath ?? DEFAULT_REPORT_HTML_PATH);
493
522
  const [indexHtmlPath, indexJsPath, indexCssPath] = await Promise.all([
494
523
  getPackagedAssetPath("index.html"),
495
524
  getPackagedAssetPath("index.js"),
496
525
  getPackagedAssetPath("index.css")
497
526
  ]);
498
- await (0, import_promises.mkdir)((0, import_path.dirname)(reportHtmlPath), { recursive: true });
527
+ await (0, import_promises.mkdir)((0, import_path2.dirname)(reportHtmlPath), { recursive: true });
499
528
  const [template, script, stylesheet] = await Promise.all([
500
529
  (0, import_promises.readFile)(indexHtmlPath, "utf8"),
501
530
  (0, import_promises.readFile)(indexJsPath, "utf8"),
@@ -506,162 +535,6 @@ async function writeReportArtifact(options) {
506
535
  await (0, import_promises.writeFile)(reportHtmlPath, html);
507
536
  }
508
537
 
509
- // src/snapshot-path-resolver.ts
510
- var import_crypto = require("crypto");
511
- var import_path2 = require("path");
512
- var DEFAULT_SCREENSHOT_TEMPLATE = "{snapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}{-projectName}{-snapshotSuffix}{ext}";
513
- var WINDOWS_FILESYSTEM_FRIENDLY_LENGTH = 60;
514
- function isUnsafeFilePathCharacter(character) {
515
- const codePoint = character.codePointAt(0);
516
- return codePoint !== void 0 && (codePoint <= 44 || codePoint >= 46 && codePoint <= 47 || codePoint >= 58 && codePoint <= 64 || codePoint >= 91 && codePoint <= 96 || codePoint >= 123 && codePoint <= 127);
517
- }
518
- function sanitizeForFilePath(value) {
519
- return Array.from(value).reduce(
520
- (state, character) => {
521
- const unsafeCharacter = isUnsafeFilePathCharacter(character);
522
- return unsafeCharacter ? state.previousCharacterWasUnsafe ? state : {
523
- value: `${state.value}-`,
524
- previousCharacterWasUnsafe: true
525
- } : {
526
- value: `${state.value}${character}`,
527
- previousCharacterWasUnsafe: false
528
- };
529
- },
530
- {
531
- value: "",
532
- previousCharacterWasUnsafe: false
533
- }
534
- ).value;
535
- }
536
- function trimLongString(value, length = WINDOWS_FILESYSTEM_FRIENDLY_LENGTH) {
537
- if (value.length <= length) {
538
- return value;
539
- }
540
- const hash = (0, import_crypto.createHash)("sha1").update(value).digest("hex");
541
- const middle = `-${hash.slice(0, 5)}-`;
542
- const start = Math.floor((length - middle.length) / 2);
543
- const end = length - middle.length - start;
544
- return value.slice(0, start) + middle + value.slice(-end);
545
- }
546
- function sanitizeFilePathBeforeExtension(filePath, extension = (0, import_path2.extname)(filePath)) {
547
- const base = filePath.slice(0, filePath.length - extension.length);
548
- return sanitizeForFilePath(base) + extension;
549
- }
550
- function addSuffixToFilePath(filePath, suffix) {
551
- const extension = (0, import_path2.extname)(filePath);
552
- return filePath.slice(0, filePath.length - extension.length) + suffix + extension;
553
- }
554
- function normalizedSnapshotDir(config) {
555
- return (0, import_path2.resolve)(config.configDir, config.snapshotDir);
556
- }
557
- function templateValue(template, token, value) {
558
- return template.replace(
559
- new RegExp(`\\{(.)?${token}\\}`, "g"),
560
- (_, prefix) => value === "" ? "" : `${prefix ?? ""}${value}`
561
- );
562
- }
563
- function applyTemplate(input, nameArgument, extension) {
564
- const template = input.config.toHaveScreenshotPathTemplate ?? input.config.snapshotPathTemplate ?? DEFAULT_SCREENSHOT_TEMPLATE;
565
- const relativeTestFilePath = (0, import_path2.relative)(input.config.testDir, input.testFile);
566
- const parsed = (0, import_path2.parse)(relativeTestFilePath);
567
- const tokens = [
568
- ["testDir", input.config.testDir],
569
- ["snapshotDir", normalizedSnapshotDir(input.config)],
570
- ["snapshotSuffix", input.config.snapshotSuffix],
571
- ["testFileDir", parsed.dir],
572
- ["platform", process.platform],
573
- ["projectName", sanitizeForFilePath(input.config.projectName)],
574
- ["testName", ""],
575
- ["testFileName", parsed.base],
576
- ["testFilePath", relativeTestFilePath],
577
- ["arg", nameArgument],
578
- ["ext", extension]
579
- ];
580
- const snapshotPath = tokens.reduce(
581
- (currentTemplate, [token, value]) => templateValue(currentTemplate, token, value),
582
- template
583
- );
584
- return (0, import_path2.resolve)(input.config.configDir, snapshotPath);
585
- }
586
- function removeExtension(filePath, extension = (0, import_path2.extname)(filePath)) {
587
- return filePath.slice(0, filePath.length - extension.length);
588
- }
589
- function snapshotNameParts(declaredName) {
590
- const extension = (0, import_path2.extname)(declaredName) || ".png";
591
- return {
592
- extension,
593
- filePath: (0, import_path2.extname)(declaredName) === "" ? `${declaredName}${extension}` : declaredName
594
- };
595
- }
596
- function filePathForOccurrence(filePath, occurrenceIndex) {
597
- return occurrenceIndex === 1 ? filePath : addSuffixToFilePath(filePath, `-${occurrenceIndex - 1}`);
598
- }
599
- function createResolvedBaselineTarget(input, declaration, nameArgument, extension) {
600
- return {
601
- visualName: declaration.visualName,
602
- attachmentBaseName: declaration.visualName,
603
- artifactBaseName: sanitizeForFilePath(declaration.visualName),
604
- snapshotPath: applyTemplate(input, nameArgument, extension)
605
- };
606
- }
607
- function resolveStringCallTarget(input, declaration) {
608
- const { extension, filePath } = snapshotNameParts(declaration.declaredName);
609
- const occurrenceFilePath = filePathForOccurrence(filePath, declaration.occurrenceIndex);
610
- const sanitizedNameWithExtension = sanitizeFilePathBeforeExtension(occurrenceFilePath, extension);
611
- const nameArgument = removeExtension(sanitizedNameWithExtension, extension);
612
- return createResolvedBaselineTarget(input, declaration, nameArgument, extension);
613
- }
614
- function resolveArrayCallTarget(input, declaration) {
615
- const { extension, filePath } = snapshotNameParts(declaration.declaredName);
616
- const occurrenceFilePath = filePathForOccurrence(filePath, declaration.occurrenceIndex);
617
- const nameArgument = (0, import_path2.join)((0, import_path2.dirname)(occurrenceFilePath), (0, import_path2.basename)(occurrenceFilePath, extension));
618
- return createResolvedBaselineTarget(input, declaration, nameArgument, extension);
619
- }
620
- function resolveNamedTarget(input, declaration) {
621
- if (!declaration.declaredName.includes("/")) {
622
- return resolveStringCallTarget(input, declaration);
623
- }
624
- const stringCallTarget = resolveStringCallTarget(input, declaration);
625
- const arrayCallTarget = resolveArrayCallTarget(input, declaration);
626
- if (stringCallTarget.snapshotPath === arrayCallTarget.snapshotPath) {
627
- return stringCallTarget;
628
- }
629
- if (input.snapshotPathExists === void 0) {
630
- return void 0;
631
- }
632
- const stringCallTargetExists = input.snapshotPathExists(stringCallTarget.snapshotPath);
633
- const arrayCallTargetExists = input.snapshotPathExists(arrayCallTarget.snapshotPath);
634
- if (stringCallTargetExists === arrayCallTargetExists) {
635
- return void 0;
636
- }
637
- return stringCallTargetExists ? stringCallTarget : arrayCallTarget;
638
- }
639
- function reporterTitlesWithoutProjectAndFile(reporterTitlePath) {
640
- return reporterTitlePath.slice(3).filter((part) => part !== "");
641
- }
642
- function anonymousName(reporterTitlePath, occurrenceIndex) {
643
- const rawAnonymousName = `${reporterTitlesWithoutProjectAndFile(reporterTitlePath).join(" ")} ${occurrenceIndex}.png`;
644
- return sanitizeFilePathBeforeExtension(trimLongString(rawAnonymousName), ".png");
645
- }
646
- function resolveTarget(input, declaration) {
647
- switch (declaration.kind) {
648
- case "named":
649
- return typeof declaration.declaredName === "string" && declaration.declaredName !== "" ? resolveNamedTarget(input, declaration) : void 0;
650
- case "unnamed": {
651
- const anonymousFileName = anonymousName(input.reporterTitlePath, declaration.occurrenceIndex);
652
- const extension = ".png";
653
- const nameArgument = (0, import_path2.join)((0, import_path2.dirname)(anonymousFileName), (0, import_path2.basename)(anonymousFileName, extension));
654
- return createResolvedBaselineTarget(input, declaration, nameArgument, extension);
655
- }
656
- }
657
- }
658
- function resolveBaselineTargets(input) {
659
- return input.declarations.flatMap((declaration) => {
660
- const resolvedTarget = resolveTarget(input, declaration);
661
- return resolvedTarget === void 0 ? [] : [resolvedTarget];
662
- });
663
- }
664
-
665
538
  // src/reporter-artifact-ops.ts
666
539
  var CURRENT_DIRECTORY_ARTIFACT_SEGMENT = "+dot+";
667
540
  var PARENT_DIRECTORY_ARTIFACT_SEGMENT = "+dotdot+";
@@ -823,7 +696,187 @@ function extractScreenshotDeclarations(steps) {
823
696
  return state.declarations;
824
697
  }
825
698
 
699
+ // src/snapshot-path-resolver.ts
700
+ var import_crypto = require("crypto");
701
+ var import_path4 = require("path");
702
+ var DEFAULT_SCREENSHOT_TEMPLATE = "{snapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}{-projectName}{-snapshotSuffix}{ext}";
703
+ var WINDOWS_FILESYSTEM_FRIENDLY_LENGTH = 60;
704
+ function isUnsafeFilePathCharacter(character) {
705
+ const codePoint = character.codePointAt(0);
706
+ return codePoint !== void 0 && (codePoint <= 44 || codePoint >= 46 && codePoint <= 47 || codePoint >= 58 && codePoint <= 64 || codePoint >= 91 && codePoint <= 96 || codePoint >= 123 && codePoint <= 127);
707
+ }
708
+ function sanitizeForFilePath(value) {
709
+ return Array.from(value).reduce(
710
+ (state, character) => {
711
+ const unsafeCharacter = isUnsafeFilePathCharacter(character);
712
+ return unsafeCharacter ? state.previousCharacterWasUnsafe ? state : {
713
+ value: `${state.value}-`,
714
+ previousCharacterWasUnsafe: true
715
+ } : {
716
+ value: `${state.value}${character}`,
717
+ previousCharacterWasUnsafe: false
718
+ };
719
+ },
720
+ {
721
+ value: "",
722
+ previousCharacterWasUnsafe: false
723
+ }
724
+ ).value;
725
+ }
726
+ function trimLongString(value, length = WINDOWS_FILESYSTEM_FRIENDLY_LENGTH) {
727
+ if (value.length <= length) {
728
+ return value;
729
+ }
730
+ const hash = (0, import_crypto.createHash)("sha1").update(value).digest("hex");
731
+ const middle = `-${hash.slice(0, 5)}-`;
732
+ const start = Math.floor((length - middle.length) / 2);
733
+ const end = length - middle.length - start;
734
+ return value.slice(0, start) + middle + value.slice(-end);
735
+ }
736
+ function sanitizeFilePathBeforeExtension(filePath, extension = (0, import_path4.extname)(filePath)) {
737
+ const base = filePath.slice(0, filePath.length - extension.length);
738
+ return sanitizeForFilePath(base) + extension;
739
+ }
740
+ function addSuffixToFilePath(filePath, suffix) {
741
+ const extension = (0, import_path4.extname)(filePath);
742
+ return filePath.slice(0, filePath.length - extension.length) + suffix + extension;
743
+ }
744
+ function normalizedSnapshotDir(config) {
745
+ return (0, import_path4.resolve)(config.configDir, config.snapshotDir);
746
+ }
747
+ function templateValue(template, token, value) {
748
+ return template.replace(
749
+ new RegExp(`\\{(.)?${token}\\}`, "g"),
750
+ (_, prefix) => value === "" ? "" : `${prefix ?? ""}${value}`
751
+ );
752
+ }
753
+ function applyTemplate(input, nameArgument, extension) {
754
+ const template = input.config.toHaveScreenshotPathTemplate ?? input.config.snapshotPathTemplate ?? DEFAULT_SCREENSHOT_TEMPLATE;
755
+ const relativeTestFilePath = (0, import_path4.relative)(input.config.testDir, input.testFile);
756
+ const parsed = (0, import_path4.parse)(relativeTestFilePath);
757
+ const tokens = [
758
+ ["testDir", input.config.testDir],
759
+ ["snapshotDir", normalizedSnapshotDir(input.config)],
760
+ ["snapshotSuffix", input.config.snapshotSuffix],
761
+ ["testFileDir", parsed.dir],
762
+ ["platform", process.platform],
763
+ ["projectName", sanitizeForFilePath(input.config.projectName)],
764
+ ["testName", ""],
765
+ ["testFileName", parsed.base],
766
+ ["testFilePath", relativeTestFilePath],
767
+ ["arg", nameArgument],
768
+ ["ext", extension]
769
+ ];
770
+ const snapshotPath = tokens.reduce(
771
+ (currentTemplate, [token, value]) => templateValue(currentTemplate, token, value),
772
+ template
773
+ );
774
+ return (0, import_path4.resolve)(input.config.configDir, snapshotPath);
775
+ }
776
+ function removeExtension(filePath, extension = (0, import_path4.extname)(filePath)) {
777
+ return filePath.slice(0, filePath.length - extension.length);
778
+ }
779
+ function snapshotNameParts(declaredName) {
780
+ const extension = (0, import_path4.extname)(declaredName) || ".png";
781
+ return {
782
+ extension,
783
+ filePath: (0, import_path4.extname)(declaredName) === "" ? `${declaredName}${extension}` : declaredName
784
+ };
785
+ }
786
+ function filePathForOccurrence(filePath, occurrenceIndex) {
787
+ return occurrenceIndex === 1 ? filePath : addSuffixToFilePath(filePath, `-${occurrenceIndex - 1}`);
788
+ }
789
+ function createResolvedBaselineTarget(input, declaration, nameArgument, extension) {
790
+ return {
791
+ visualName: declaration.visualName,
792
+ attachmentBaseName: declaration.visualName,
793
+ artifactBaseName: sanitizeForFilePath(declaration.visualName),
794
+ snapshotPath: applyTemplate(input, nameArgument, extension)
795
+ };
796
+ }
797
+ function resolveStringCallTarget(input, declaration) {
798
+ const { extension, filePath } = snapshotNameParts(declaration.declaredName);
799
+ const occurrenceFilePath = filePathForOccurrence(filePath, declaration.occurrenceIndex);
800
+ const sanitizedNameWithExtension = sanitizeFilePathBeforeExtension(occurrenceFilePath, extension);
801
+ const nameArgument = removeExtension(sanitizedNameWithExtension, extension);
802
+ return createResolvedBaselineTarget(input, declaration, nameArgument, extension);
803
+ }
804
+ function resolveArrayCallTarget(input, declaration) {
805
+ const { extension, filePath } = snapshotNameParts(declaration.declaredName);
806
+ const occurrenceFilePath = filePathForOccurrence(filePath, declaration.occurrenceIndex);
807
+ const nameArgument = (0, import_path4.join)((0, import_path4.dirname)(occurrenceFilePath), (0, import_path4.basename)(occurrenceFilePath, extension));
808
+ return createResolvedBaselineTarget(input, declaration, nameArgument, extension);
809
+ }
810
+ function resolveNamedTarget(input, declaration) {
811
+ if (!declaration.declaredName.includes("/")) {
812
+ return resolveStringCallTarget(input, declaration);
813
+ }
814
+ const stringCallTarget = resolveStringCallTarget(input, declaration);
815
+ const arrayCallTarget = resolveArrayCallTarget(input, declaration);
816
+ if (stringCallTarget.snapshotPath === arrayCallTarget.snapshotPath) {
817
+ return stringCallTarget;
818
+ }
819
+ if (input.snapshotPathExists === void 0) {
820
+ return void 0;
821
+ }
822
+ const stringCallTargetExists = input.snapshotPathExists(stringCallTarget.snapshotPath);
823
+ const arrayCallTargetExists = input.snapshotPathExists(arrayCallTarget.snapshotPath);
824
+ if (stringCallTargetExists === arrayCallTargetExists) {
825
+ return void 0;
826
+ }
827
+ return stringCallTargetExists ? stringCallTarget : arrayCallTarget;
828
+ }
829
+ function reporterTitlesWithoutProjectAndFile(reporterTitlePath) {
830
+ return reporterTitlePath.slice(3).filter((part) => part !== "");
831
+ }
832
+ function anonymousNameFromTitles(titles, occurrenceIndex) {
833
+ return sanitizeFilePathBeforeExtension(trimLongString(`${titles.join(" ")} ${occurrenceIndex}.png`), ".png");
834
+ }
835
+ function anonymousName(reporterTitlePath, occurrenceIndex) {
836
+ return anonymousNameFromTitles(reporterTitlesWithoutProjectAndFile(reporterTitlePath), occurrenceIndex);
837
+ }
838
+ function playwrightAnonymousVisualName(reporterTitlePath, occurrenceIndex) {
839
+ const titles = reporterTitlesWithoutProjectAndFile(reporterTitlePath);
840
+ return titles.length === 0 ? null : removeExtension(anonymousNameFromTitles(titles, occurrenceIndex), ".png");
841
+ }
842
+ function resolveTarget(input, declaration) {
843
+ switch (declaration.kind) {
844
+ case "named":
845
+ return typeof declaration.declaredName === "string" && declaration.declaredName !== "" ? resolveNamedTarget(input, declaration) : void 0;
846
+ case "unnamed": {
847
+ const anonymousFileName = anonymousName(input.reporterTitlePath, declaration.occurrenceIndex);
848
+ const extension = ".png";
849
+ const nameArgument = (0, import_path4.join)((0, import_path4.dirname)(anonymousFileName), (0, import_path4.basename)(anonymousFileName, extension));
850
+ return createResolvedBaselineTarget(input, declaration, nameArgument, extension);
851
+ }
852
+ }
853
+ }
854
+ function withResolvedVisualNames(declarations, reporterTitlePath) {
855
+ return declarations.map((declaration) => {
856
+ if (declaration.kind !== "unnamed") {
857
+ return declaration;
858
+ }
859
+ const resolvedName = playwrightAnonymousVisualName(reporterTitlePath, declaration.occurrenceIndex);
860
+ return resolvedName === null ? declaration : { ...declaration, visualName: resolvedName };
861
+ });
862
+ }
863
+ function resolveBaselineTargets(input) {
864
+ return input.declarations.flatMap((declaration) => {
865
+ const resolvedTarget = resolveTarget(input, declaration);
866
+ return resolvedTarget === void 0 ? [] : [resolvedTarget];
867
+ });
868
+ }
869
+
826
870
  // src/reporter.ts
871
+ function collectNativeImageAttachments(result) {
872
+ return result.attachments.filter(
873
+ (attachment) => attachment.contentType === "image/png" && attachment.path !== void 0
874
+ ).map((attachment) => ({
875
+ name: attachment.name,
876
+ path: attachment.path,
877
+ contentType: attachment.contentType
878
+ }));
879
+ }
827
880
  var CrvyRprtr = class {
828
881
  ws = null;
829
882
  serverUrl;
@@ -838,8 +891,9 @@ var CrvyRprtr = class {
838
891
  playwrightSnapshotPathTemplate;
839
892
  playwrightToHaveScreenshotPathTemplate;
840
893
  isOfflineMode = false;
841
- hadOfflineMode = false;
842
894
  runEvents = [];
895
+ ci;
896
+ pendingArtifacts = [];
843
897
  constructor(options = {}) {
844
898
  this.serverUrl = options.serverUrl ?? "ws://localhost:3000";
845
899
  this.screenshotDir = options.screenshotDir ?? "./screenshots";
@@ -849,12 +903,18 @@ var CrvyRprtr = class {
849
903
  this.playwrightSnapshotDir = options.playwrightSnapshotDir;
850
904
  this.playwrightSnapshotPathTemplate = options.playwrightSnapshotPathTemplate;
851
905
  this.playwrightToHaveScreenshotPathTemplate = options.playwrightToHaveScreenshotPathTemplate;
906
+ this.ci = options.ci ?? isCI();
907
+ if (this.ci) {
908
+ this.isOfflineMode = true;
909
+ }
852
910
  }
853
911
  async onBegin(config, suite) {
854
- this.configDir = config.configFile === void 0 ? config.rootDir : (0, import_path4.dirname)(config.configFile);
912
+ this.configDir = config.configFile === void 0 ? config.rootDir : (0, import_path5.dirname)(config.configFile);
855
913
  log(`[CrvyRprtr] Starting run with ${suite.allTests().length} tests`);
856
914
  await (0, import_promises3.mkdir)(this.screenshotDir, { recursive: true });
857
- this.connect();
915
+ if (!this.ci) {
916
+ this.connect();
917
+ }
858
918
  }
859
919
  connect() {
860
920
  const WebSocketConstructor = globalThis.WebSocket;
@@ -886,7 +946,7 @@ var CrvyRprtr = class {
886
946
  }
887
947
  enableOfflineMode() {
888
948
  if (this.isOfflineMode) return;
889
- this.isOfflineMode = this.hadOfflineMode = true;
949
+ this.isOfflineMode = true;
890
950
  log("[CrvyRprtr] Offline mode enabled - events will be queued to file");
891
951
  }
892
952
  describeTitlePath(test) {
@@ -911,24 +971,36 @@ var CrvyRprtr = class {
911
971
  }
912
972
  });
913
973
  }
914
- async onTestEnd(test, result) {
915
- const screenshotDeclarations = extractScreenshotDeclarations(result.steps);
916
- const savedAttachments = await saveAttachments(this.screenshotDir, test.id, result);
974
+ onTestEnd(test, result) {
975
+ const reporterTitlePath = this.testMetadata.get(test.id)?.reporterTitlePath ?? this.reporterTitlePath(test);
976
+ const screenshotDeclarations = withResolvedVisualNames(
977
+ extractScreenshotDeclarations(result.steps),
978
+ reporterTitlePath
979
+ );
980
+ const nativeAttachments = collectNativeImageAttachments(result);
981
+ const data = {
982
+ id: test.id,
983
+ title: test.title,
984
+ status: result.status,
985
+ attachments: nativeAttachments,
986
+ visualNames: screenshotDeclarations.map(({ visualName }) => visualName),
987
+ visualDeclarations: screenshotDeclarations,
988
+ error: result.errors.length > 0 ? result.errors[0]?.message : void 0,
989
+ duration: result.duration
990
+ };
917
991
  try {
918
- await this.copySnapshotBaselines(test, result.status, screenshotDeclarations, savedAttachments);
919
- this.send({
920
- type: "test-end",
921
- data: {
922
- id: test.id,
923
- title: test.title,
992
+ if (this.ci) {
993
+ const baselineInput = result.status === "passed" ? this.baselineInput(test, screenshotDeclarations) : null;
994
+ const resolvedTargets = baselineInput === null ? [] : resolveBaselineTargets(baselineInput);
995
+ this.pendingArtifacts.push({
996
+ testId: test.id,
924
997
  status: result.status,
925
- attachments: savedAttachments,
926
- visualNames: screenshotDeclarations.map(({ visualName }) => visualName),
927
- visualDeclarations: screenshotDeclarations,
928
- error: result.errors.length > 0 ? result.errors[0]?.message : void 0,
929
- duration: result.duration
930
- }
931
- });
998
+ resolvedTargets,
999
+ nativeAttachments,
1000
+ eventData: data
1001
+ });
1002
+ }
1003
+ this.send({ type: "test-end", data });
932
1004
  } finally {
933
1005
  this.testMetadata.delete(test.id);
934
1006
  }
@@ -953,25 +1025,40 @@ var CrvyRprtr = class {
953
1025
  snapshotPathExists: import_fs.existsSync
954
1026
  };
955
1027
  }
956
- async copySnapshotBaselines(test, status, screenshotDeclarations, savedAttachments) {
957
- if (status !== "passed" || screenshotDeclarations.length === 0) return;
958
- const input = this.baselineInput(test, screenshotDeclarations);
959
- if (input === null) return;
960
- const targets = resolveBaselineTargets(input);
961
- if (targets.length === 0) return;
962
- const safeTestId = sanitizeId(test.id);
963
- const testScreenshotDir = (0, import_path4.join)(this.screenshotDir, safeTestId);
1028
+ async copyBaselinesForTargets(testId, status, resolvedTargets, savedAttachments) {
1029
+ if (status !== "passed" || resolvedTargets.length === 0) return;
1030
+ const safeTestId = sanitizeId(testId);
1031
+ const testScreenshotDir = (0, import_path5.join)(this.screenshotDir, safeTestId);
964
1032
  const limit = (0, import_p_limit2.default)(5);
965
1033
  await Promise.all(
966
- targets.map(
1034
+ resolvedTargets.map(
967
1035
  (target) => limit(() => copyResolvedBaseline(safeTestId, testScreenshotDir, target, savedAttachments))
968
1036
  )
969
1037
  );
970
1038
  }
971
1039
  async onEnd(result) {
972
1040
  this.send({ type: "run-end", data: { status: result.status } });
973
- await writeStaticArtifact(this.runEvents, this.screenshotDir, this.reportHtmlPath);
974
- if (this.hadOfflineMode) await writeOfflineReport(this.runEvents, this.offlineReportPath, this.workerIndex);
1041
+ if (this.ci) {
1042
+ const limit = (0, import_p_limit2.default)(10);
1043
+ await Promise.all(
1044
+ this.pendingArtifacts.map(
1045
+ (pending) => limit(async () => {
1046
+ const savedAttachments = await saveAttachments(this.screenshotDir, pending.testId, {
1047
+ attachments: pending.nativeAttachments
1048
+ });
1049
+ await this.copyBaselinesForTargets(
1050
+ pending.testId,
1051
+ pending.status,
1052
+ pending.resolvedTargets,
1053
+ savedAttachments
1054
+ );
1055
+ pending.eventData.attachments = savedAttachments;
1056
+ })
1057
+ )
1058
+ );
1059
+ await writeStaticArtifact(this.runEvents, this.screenshotDir, this.reportHtmlPath);
1060
+ await writeOfflineReport(this.runEvents, this.offlineReportPath, this.workerIndex);
1061
+ }
975
1062
  await new Promise((resolve3) => {
976
1063
  if (!this.ws || this.ws.readyState === WebSocket.CLOSED) {
977
1064
  resolve3();
@@ -7,6 +7,7 @@ export interface CrvyRprtrOptions {
7
7
  playwrightSnapshotDir?: string;
8
8
  playwrightSnapshotPathTemplate?: string;
9
9
  playwrightToHaveScreenshotPathTemplate?: string;
10
+ ci?: boolean;
10
11
  }
11
12
  export declare class CrvyRprtr implements Reporter {
12
13
  private ws;
@@ -22,8 +23,9 @@ export declare class CrvyRprtr implements Reporter {
22
23
  private playwrightSnapshotPathTemplate?;
23
24
  private playwrightToHaveScreenshotPathTemplate?;
24
25
  private isOfflineMode;
25
- private hadOfflineMode;
26
26
  private runEvents;
27
+ private readonly ci;
28
+ private pendingArtifacts;
27
29
  constructor(options?: CrvyRprtrOptions);
28
30
  onBegin(config: FullConfig, suite: Suite): Promise<void>;
29
31
  private connect;
@@ -31,9 +33,9 @@ export declare class CrvyRprtr implements Reporter {
31
33
  private describeTitlePath;
32
34
  private reporterTitlePath;
33
35
  onTestBegin(test: TestCase): void;
34
- onTestEnd(test: TestCase, result: TestResult): Promise<void>;
36
+ onTestEnd(test: TestCase, result: TestResult): void;
35
37
  private baselineInput;
36
- private copySnapshotBaselines;
38
+ private copyBaselinesForTargets;
37
39
  onEnd(result: FullResult): Promise<void>;
38
40
  private send;
39
41
  }
@@ -1 +1 @@
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"}
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;IAC/C,EAAE,CAAC,EAAE,OAAO,CAAA;CACb;AAuBD,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,SAAS,CAAiB;IAClC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAS;IAC5B,OAAO,CAAC,gBAAgB,CAAgC;gBAE5C,OAAO,GAAE,gBAAqB;IAepC,OAAO,CAAC,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC;IAS9D,OAAO,CAAC,OAAO;IA6Bf,OAAO,CAAC,iBAAiB;IAMzB,OAAO,CAAC,iBAAiB;IAOzB,OAAO,CAAC,iBAAiB;IAMzB,WAAW,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI;IAcjC,SAAS,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,GAAG,IAAI;IAmCnD,OAAO,CAAC,aAAa;YAqBP,uBAAuB;IAiB/B,KAAK,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAyC9C,OAAO,CAAC,IAAI;CAUb;AAED,eAAe,SAAS,CAAA"}