@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/CHANGELOG.md +60 -0
- package/README.md +34 -9
- package/dist/{chunk-MWLM3HWS.js → chunk-JGS2VWQP.js} +189 -2
- package/dist/{chunk-IH44LYNM.js → chunk-OMDYTNWY.js} +131 -53
- package/dist/cli.js +2 -2
- package/dist/index.css +3 -0
- package/dist/index.js +555 -485
- package/dist/report-state.d.ts +6 -1
- package/dist/report-state.d.ts.map +1 -1
- package/dist/report-utils.d.ts +3 -0
- package/dist/report-utils.d.ts.map +1 -1
- package/dist/reporter-utils.d.ts +11 -2
- package/dist/reporter-utils.d.ts.map +1 -1
- package/dist/reporter.cjs +368 -154
- package/dist/reporter.d.ts +13 -1
- package/dist/reporter.d.ts.map +1 -1
- package/dist/reporter.js +182 -153
- package/dist/schemas.d.ts +115 -15
- package/dist/schemas.d.ts.map +1 -1
- package/dist/server/app.d.ts +5 -0
- package/dist/server/app.d.ts.map +1 -1
- package/dist/server/routes.d.ts +7 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server.cjs +329 -64
- package/dist/server.js +2 -2
- package/dist/snapshot-path-resolver.d.ts +30 -0
- package/dist/snapshot-path-resolver.d.ts.map +1 -0
- package/dist/types.d.ts +3 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
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
|
|
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(
|
|
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
|
|
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
|
|
475
|
-
if (
|
|
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:
|
|
480
|
-
|
|
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
|
-
|
|
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
|
-
|
|
506
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
574
|
-
console.error("[CrvyRprtr] Failed to connect:",
|
|
789
|
+
} catch (error) {
|
|
790
|
+
console.error("[CrvyRprtr] Failed to connect:", error);
|
|
575
791
|
this.enableOfflineMode();
|
|
576
792
|
}
|
|
577
793
|
}
|
|
578
794
|
enableOfflineMode() {
|
|
579
|
-
if (
|
|
580
|
-
|
|
581
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
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
|
|
627
|
-
|
|
628
|
-
);
|
|
629
|
-
if (
|
|
630
|
-
const
|
|
631
|
-
const
|
|
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
|
-
|
|
635
|
-
(
|
|
636
|
-
|
|
637
|
-
|
|
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
|
|
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
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
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
|
-
|
|
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((
|
|
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 (
|
|
710
|
-
console.error("[CrvyRprtr] Failed to write offline report:",
|
|
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 (
|
|
722
|
-
console.error("[CrvyRprtr] Failed to write report artifact:",
|
|
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
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
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(
|
|
752
|
-
const
|
|
753
|
-
if (
|
|
754
|
-
this.runEvents.push({
|
|
755
|
-
|
|
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
|
-
|
|
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
|
};
|