@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/CHANGELOG.md +50 -0
- package/dist/{chunk-TS64TROX.js → chunk-35CZLYSW.js} +210 -58
- package/dist/{chunk-EJRLZZ22.js → chunk-43JLQN36.js} +45 -5
- package/dist/ci.d.ts +2 -0
- package/dist/ci.d.ts.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +7 -4
- package/dist/index.js +338 -46
- package/dist/report-utils.d.ts.map +1 -1
- package/dist/reporter-artifact-ops.d.ts +1 -3
- package/dist/reporter-artifact-ops.d.ts.map +1 -1
- package/dist/reporter.cjs +285 -198
- package/dist/reporter.d.ts +5 -3
- package/dist/reporter.d.ts.map +1 -1
- package/dist/reporter.js +81 -31
- package/dist/schemas.d.ts +210 -1
- package/dist/schemas.d.ts.map +1 -1
- package/dist/server/app.d.ts +6 -0
- package/dist/server/app.d.ts.map +1 -1
- package/dist/server/artifact-routes.d.ts +8 -0
- package/dist/server/artifact-routes.d.ts.map +1 -0
- package/dist/server/file-utils.d.ts.map +1 -1
- package/dist/server/handlers.d.ts +6 -2
- package/dist/server/handlers.d.ts.map +1 -1
- package/dist/server/routes.d.ts +2 -2
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/utils.d.ts +3 -0
- package/dist/server/utils.d.ts.map +1 -1
- package/dist/server.cjs +289 -115
- package/dist/server.js +2 -2
- package/dist/snapshot-path-resolver.d.ts +2 -0
- package/dist/snapshot-path-resolver.d.ts.map +1 -1
- package/dist/types.d.ts +24 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
|
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
|
|
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(
|
|
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,
|
|
432
|
-
const candidates = [currentDir, (0,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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.
|
|
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 =
|
|
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
|
-
|
|
915
|
-
const
|
|
916
|
-
const
|
|
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
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
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
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
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
|
|
957
|
-
if (status !== "passed" ||
|
|
958
|
-
const
|
|
959
|
-
|
|
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
|
-
|
|
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
|
-
|
|
974
|
-
|
|
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();
|
package/dist/reporter.d.ts
CHANGED
|
@@ -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):
|
|
36
|
+
onTestEnd(test: TestCase, result: TestResult): void;
|
|
35
37
|
private baselineInput;
|
|
36
|
-
private
|
|
38
|
+
private copyBaselinesForTargets;
|
|
37
39
|
onEnd(result: FullResult): Promise<void>;
|
|
38
40
|
private send;
|
|
39
41
|
}
|
package/dist/reporter.d.ts.map
CHANGED
|
@@ -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;
|
|
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"}
|