@crvy/rprtr 0.0.5 → 0.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +65 -0
- package/README.md +34 -9
- package/dist/{chunk-MWLM3HWS.js → chunk-EJRLZZ22.js} +312 -125
- package/dist/{chunk-IH44LYNM.js → chunk-XU5VQ3FZ.js} +133 -53
- package/dist/cli.js +2 -2
- package/dist/debug-log.d.ts +3 -0
- package/dist/debug-log.d.ts.map +1 -0
- package/dist/index.css +3 -22
- package/dist/index.js +626 -600
- 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-artifact-ops.d.ts +22 -0
- package/dist/reporter-artifact-ops.d.ts.map +1 -0
- package/dist/reporter-utils.d.ts +11 -2
- package/dist/reporter-utils.d.ts.map +1 -1
- package/dist/reporter.cjs +417 -185
- package/dist/reporter.d.ts +12 -5
- package/dist/reporter.d.ts.map +1 -1
- package/dist/reporter.js +228 -181
- 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/handlers.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 +331 -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,23 @@ __export(reporter_exports, {
|
|
|
34
34
|
default: () => reporter_default
|
|
35
35
|
});
|
|
36
36
|
module.exports = __toCommonJS(reporter_exports);
|
|
37
|
+
var import_fs = require("fs");
|
|
38
|
+
var import_promises3 = require("fs/promises");
|
|
39
|
+
var import_path4 = require("path");
|
|
40
|
+
var import_p_limit2 = __toESM(require("p-limit"), 1);
|
|
41
|
+
|
|
42
|
+
// src/debug-log.ts
|
|
43
|
+
var isDebug = () => process.env.DEBUG !== void 0 && process.env.DEBUG !== "";
|
|
44
|
+
var log = (...args) => {
|
|
45
|
+
if (isDebug()) console.log(...args);
|
|
46
|
+
};
|
|
47
|
+
var logError = (...args) => {
|
|
48
|
+
if (isDebug()) console.error(...args);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// src/reporter-artifact-ops.ts
|
|
37
52
|
var import_promises2 = require("fs/promises");
|
|
38
|
-
var
|
|
53
|
+
var import_path3 = require("path");
|
|
39
54
|
var import_p_limit = __toESM(require("p-limit"), 1);
|
|
40
55
|
|
|
41
56
|
// src/report-artifact.ts
|
|
@@ -62,6 +77,12 @@ function withImageSource(image) {
|
|
|
62
77
|
source: classifyImage(image)
|
|
63
78
|
};
|
|
64
79
|
}
|
|
80
|
+
function getDeclaredVisualNames(visualNames, visualDeclarations) {
|
|
81
|
+
return visualDeclarations?.map(({ visualName }) => visualName) ?? visualNames;
|
|
82
|
+
}
|
|
83
|
+
function copyVisualDeclarations(visualDeclarations) {
|
|
84
|
+
return visualDeclarations?.map((visualDeclaration) => ({ ...visualDeclaration }));
|
|
85
|
+
}
|
|
65
86
|
function mergeDeclaredImages(images, visualNames) {
|
|
66
87
|
const names = /* @__PURE__ */ new Set([...Object.keys(images), ...visualNames]);
|
|
67
88
|
return Object.fromEntries(
|
|
@@ -181,10 +202,14 @@ function applyTestEndEvent(state, data, options = {}) {
|
|
|
181
202
|
}
|
|
182
203
|
test.status = mapStatus(data.status);
|
|
183
204
|
const resultStatus = data.status === "passed" ? "success" : data.status === "failed" ? "failed" : "pending";
|
|
205
|
+
const visualDeclarations = copyVisualDeclarations(data.visualDeclarations);
|
|
184
206
|
const images = preservePreviousPassingImages(
|
|
185
207
|
test,
|
|
186
208
|
data.status,
|
|
187
|
-
mergeDeclaredImages(
|
|
209
|
+
mergeDeclaredImages(
|
|
210
|
+
attachmentsToImages(data.attachments, options.screenshotsBaseUrl),
|
|
211
|
+
getDeclaredVisualNames(data.visualNames, visualDeclarations)
|
|
212
|
+
)
|
|
188
213
|
);
|
|
189
214
|
const diffCount = countDiffImages(images);
|
|
190
215
|
const hasDiffs = diffCount > 0;
|
|
@@ -196,6 +221,7 @@ function applyTestEndEvent(state, data, options = {}) {
|
|
|
196
221
|
status: resultStatus,
|
|
197
222
|
retries: 0,
|
|
198
223
|
images,
|
|
224
|
+
visualDeclarations,
|
|
199
225
|
error: data.error,
|
|
200
226
|
duration: data.duration
|
|
201
227
|
}
|
|
@@ -233,6 +259,20 @@ var ImagesSchema = import_zod.z.object({
|
|
|
233
259
|
error: import_zod.z.string().optional(),
|
|
234
260
|
source: VisualSourceSchema.optional()
|
|
235
261
|
});
|
|
262
|
+
var ScreenshotDeclarationSchema = import_zod.z.discriminatedUnion("kind", [
|
|
263
|
+
import_zod.z.object({
|
|
264
|
+
visualName: import_zod.z.string(),
|
|
265
|
+
kind: import_zod.z.literal("named"),
|
|
266
|
+
declaredName: import_zod.z.string(),
|
|
267
|
+
snapshotBaseName: import_zod.z.string(),
|
|
268
|
+
occurrenceIndex: import_zod.z.number()
|
|
269
|
+
}),
|
|
270
|
+
import_zod.z.object({
|
|
271
|
+
visualName: import_zod.z.string(),
|
|
272
|
+
kind: import_zod.z.literal("unnamed"),
|
|
273
|
+
occurrenceIndex: import_zod.z.number()
|
|
274
|
+
})
|
|
275
|
+
]);
|
|
236
276
|
var AttachmentSchema = import_zod.z.object({
|
|
237
277
|
name: import_zod.z.string(),
|
|
238
278
|
path: import_zod.z.string(),
|
|
@@ -244,6 +284,7 @@ var TestResultSchema = import_zod.z.object({
|
|
|
244
284
|
status: TestResultStatusSchema,
|
|
245
285
|
retries: import_zod.z.number(),
|
|
246
286
|
images: import_zod.z.record(import_zod.z.string(), ImagesSchema).optional(),
|
|
287
|
+
visualDeclarations: import_zod.z.array(ScreenshotDeclarationSchema).optional(),
|
|
247
288
|
error: import_zod.z.string().optional(),
|
|
248
289
|
duration: import_zod.z.number().optional()
|
|
249
290
|
});
|
|
@@ -290,6 +331,10 @@ var TestEndDataSchema = import_zod.z.object({
|
|
|
290
331
|
status: import_zod.z.enum(["passed", "failed", "skipped"]),
|
|
291
332
|
attachments: import_zod.z.array(AttachmentSchema),
|
|
292
333
|
visualNames: import_zod.z.array(import_zod.z.string()).default([]),
|
|
334
|
+
visualDeclarations: import_zod.z.preprocess(
|
|
335
|
+
(value) => value === null ? void 0 : value,
|
|
336
|
+
import_zod.z.array(ScreenshotDeclarationSchema).optional()
|
|
337
|
+
),
|
|
293
338
|
error: import_zod.z.string().optional(),
|
|
294
339
|
duration: import_zod.z.number().optional()
|
|
295
340
|
});
|
|
@@ -461,32 +506,285 @@ async function writeReportArtifact(options) {
|
|
|
461
506
|
await (0, import_promises.writeFile)(reportHtmlPath, html);
|
|
462
507
|
}
|
|
463
508
|
|
|
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
|
+
// src/reporter-artifact-ops.ts
|
|
666
|
+
var CURRENT_DIRECTORY_ARTIFACT_SEGMENT = "+dot+";
|
|
667
|
+
var PARENT_DIRECTORY_ARTIFACT_SEGMENT = "+dotdot+";
|
|
668
|
+
var MAX_CONCURRENT_FILE_OPS = 5;
|
|
669
|
+
var SAFE_ARTIFACT_CHARACTER = /^[A-Za-z0-9._-]$/;
|
|
670
|
+
function encodeArtifactPathSegment(segment) {
|
|
671
|
+
if (segment === ".") return CURRENT_DIRECTORY_ARTIFACT_SEGMENT;
|
|
672
|
+
if (segment === "..") return PARENT_DIRECTORY_ARTIFACT_SEGMENT;
|
|
673
|
+
return Array.from(
|
|
674
|
+
segment,
|
|
675
|
+
(character) => SAFE_ARTIFACT_CHARACTER.test(character) ? character : encodeURIComponent(character)
|
|
676
|
+
).join("");
|
|
677
|
+
}
|
|
678
|
+
function safeArtifactPath(name) {
|
|
679
|
+
return name.split("/").map(encodeArtifactPathSegment).join("/");
|
|
680
|
+
}
|
|
681
|
+
function sanitizeId(id) {
|
|
682
|
+
return id.replace(/[^a-zA-Z0-9-_]/g, "_");
|
|
683
|
+
}
|
|
684
|
+
async function copyResolvedBaseline(safeTestId, testScreenshotDir, target, savedAttachments) {
|
|
685
|
+
const attachmentName = `${target.attachmentBaseName}-expected.png`;
|
|
686
|
+
const artifactPath = safeArtifactPath(attachmentName);
|
|
687
|
+
const destPath = (0, import_path3.join)(testScreenshotDir, artifactPath);
|
|
688
|
+
try {
|
|
689
|
+
await (0, import_promises2.mkdir)((0, import_path3.dirname)(destPath), { recursive: true });
|
|
690
|
+
await (0, import_promises2.copyFile)(target.snapshotPath, destPath);
|
|
691
|
+
savedAttachments.push({ name: attachmentName, path: `${safeTestId}/${artifactPath}`, contentType: "image/png" });
|
|
692
|
+
log(`[CrvyRprtr] Attached baseline: ${target.snapshotPath}`);
|
|
693
|
+
} catch {
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
async function saveAttachments(screenshotDir, testId, result) {
|
|
697
|
+
const savedAttachments = [];
|
|
698
|
+
const safeTestId = sanitizeId(testId);
|
|
699
|
+
const testScreenshotDir = (0, import_path3.join)(screenshotDir, safeTestId);
|
|
700
|
+
const limit = (0, import_p_limit.default)(MAX_CONCURRENT_FILE_OPS);
|
|
701
|
+
await Promise.all(
|
|
702
|
+
result.attachments.filter(
|
|
703
|
+
(attachment) => attachment.contentType === "image/png" && attachment.path !== void 0
|
|
704
|
+
).map(
|
|
705
|
+
(attachment) => limit(async () => {
|
|
706
|
+
try {
|
|
707
|
+
const artifactPath = safeArtifactPath(attachment.name);
|
|
708
|
+
const destPath = (0, import_path3.join)(testScreenshotDir, artifactPath);
|
|
709
|
+
await (0, import_promises2.mkdir)((0, import_path3.dirname)(destPath), { recursive: true });
|
|
710
|
+
await (0, import_promises2.copyFile)(attachment.path, destPath);
|
|
711
|
+
savedAttachments.push({
|
|
712
|
+
name: attachment.name,
|
|
713
|
+
path: `${safeTestId}/${artifactPath}`,
|
|
714
|
+
contentType: attachment.contentType ?? "image/png"
|
|
715
|
+
});
|
|
716
|
+
log(`[CrvyRprtr] Saved screenshot: ${destPath}`);
|
|
717
|
+
} catch (error) {
|
|
718
|
+
logError(`[CrvyRprtr] Failed to save screenshot: ${attachment.path}`, error);
|
|
719
|
+
savedAttachments.push({
|
|
720
|
+
name: attachment.name,
|
|
721
|
+
path: attachment.path,
|
|
722
|
+
contentType: attachment.contentType ?? "image/png"
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
})
|
|
726
|
+
)
|
|
727
|
+
);
|
|
728
|
+
return savedAttachments;
|
|
729
|
+
}
|
|
730
|
+
async function writeOfflineReport(runEvents, offlineReportPath, workerIndex) {
|
|
731
|
+
if (runEvents.length === 0) {
|
|
732
|
+
log("[CrvyRprtr] No offline events to write");
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
try {
|
|
736
|
+
const report = {
|
|
737
|
+
version: 1,
|
|
738
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
739
|
+
workers: workerIndex + 1,
|
|
740
|
+
events: runEvents.map((event) => ({ ...event, timestamp: Date.now(), workerIndex }))
|
|
741
|
+
};
|
|
742
|
+
await (0, import_promises2.writeFile)(offlineReportPath, JSON.stringify(report, null, 2));
|
|
743
|
+
log(`[CrvyRprtr] Wrote offline report: ${offlineReportPath}`);
|
|
744
|
+
} catch (error) {
|
|
745
|
+
logError("[CrvyRprtr] Failed to write offline report:", error);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
async function writeStaticArtifact(runEvents, screenshotDir, reportHtmlPath) {
|
|
749
|
+
try {
|
|
750
|
+
await writeReportArtifact({ events: runEvents, screenshotDir, reportHtmlPath });
|
|
751
|
+
log(`[CrvyRprtr] Wrote report artifact: ${reportHtmlPath}`);
|
|
752
|
+
} catch (error) {
|
|
753
|
+
logError("[CrvyRprtr] Failed to write report artifact:", error);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
464
757
|
// src/reporter-utils.ts
|
|
465
758
|
var NAMED_SCREENSHOT_STEP_TITLE = /toHaveScreenshot\((.+?)\)/;
|
|
466
759
|
var UNNAMED_SCREENSHOT_STEP_TITLE = /^Expect "toHaveScreenshot"(?:\s|$)/;
|
|
467
760
|
var SYNTHETIC_SCREENSHOT_PREFIX = "__unnamed-screenshot-";
|
|
468
|
-
function
|
|
761
|
+
function addVisualSuffix(visualName, occurrenceIndex) {
|
|
762
|
+
if (occurrenceIndex === 1) {
|
|
763
|
+
return visualName;
|
|
764
|
+
}
|
|
765
|
+
const slashIndex = visualName.lastIndexOf("/");
|
|
766
|
+
return slashIndex === -1 ? `${visualName}-${occurrenceIndex - 1}` : `${visualName.slice(0, slashIndex + 1)}${visualName.slice(slashIndex + 1)}-${occurrenceIndex - 1}`;
|
|
767
|
+
}
|
|
768
|
+
function normalizeNamedScreenshot(titleMatch, state) {
|
|
469
769
|
const unquotedName = titleMatch.trim().replace(/^['"`]|['"`]$/g, "");
|
|
470
770
|
if (unquotedName === "") {
|
|
471
771
|
return null;
|
|
472
772
|
}
|
|
473
773
|
const normalizedPath = unquotedName.replace(/\\/g, "/");
|
|
474
|
-
const
|
|
475
|
-
if (
|
|
774
|
+
const declaredName = normalizedPath.replace(/\.png$/, "");
|
|
775
|
+
if (declaredName === "") {
|
|
476
776
|
return null;
|
|
477
777
|
}
|
|
778
|
+
const occurrenceIndex = (state.namedOccurrences.get(declaredName) ?? 0) + 1;
|
|
779
|
+
state.namedOccurrences.set(declaredName, occurrenceIndex);
|
|
478
780
|
return {
|
|
479
|
-
visualName:
|
|
480
|
-
|
|
781
|
+
visualName: addVisualSuffix(declaredName, occurrenceIndex),
|
|
782
|
+
kind: "named",
|
|
783
|
+
declaredName,
|
|
784
|
+
snapshotBaseName: declaredName,
|
|
785
|
+
occurrenceIndex
|
|
481
786
|
};
|
|
482
787
|
}
|
|
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
788
|
function visitStep(step, state) {
|
|
491
789
|
const declarationsBeforeChildren = state.declarations.length;
|
|
492
790
|
for (const nestedStep of step.steps) {
|
|
@@ -494,16 +792,19 @@ function visitStep(step, state) {
|
|
|
494
792
|
}
|
|
495
793
|
const namedMatch = step.title.match(NAMED_SCREENSHOT_STEP_TITLE);
|
|
496
794
|
if (namedMatch?.[1] !== void 0) {
|
|
497
|
-
const declaration = normalizeNamedScreenshot(namedMatch[1]);
|
|
795
|
+
const declaration = normalizeNamedScreenshot(namedMatch[1], state);
|
|
498
796
|
if (declaration !== null) {
|
|
499
|
-
|
|
797
|
+
state.declarations.push(declaration);
|
|
500
798
|
return true;
|
|
501
799
|
}
|
|
502
800
|
}
|
|
503
801
|
const hasNestedScreenshotDeclaration = state.declarations.length > declarationsBeforeChildren;
|
|
504
802
|
if (UNNAMED_SCREENSHOT_STEP_TITLE.test(step.title) && !hasNestedScreenshotDeclaration) {
|
|
505
|
-
|
|
506
|
-
|
|
803
|
+
const occurrenceIndex = state.nextUnnamedIndex;
|
|
804
|
+
state.declarations.push({
|
|
805
|
+
visualName: `${SYNTHETIC_SCREENSHOT_PREFIX}${occurrenceIndex}`,
|
|
806
|
+
kind: "unnamed",
|
|
807
|
+
occurrenceIndex
|
|
507
808
|
});
|
|
508
809
|
state.nextUnnamedIndex += 1;
|
|
509
810
|
return true;
|
|
@@ -513,7 +814,7 @@ function visitStep(step, state) {
|
|
|
513
814
|
function extractScreenshotDeclarations(steps) {
|
|
514
815
|
const state = {
|
|
515
816
|
declarations: [],
|
|
516
|
-
|
|
817
|
+
namedOccurrences: /* @__PURE__ */ new Map(),
|
|
517
818
|
nextUnnamedIndex: 1
|
|
518
819
|
};
|
|
519
820
|
for (const step of steps) {
|
|
@@ -523,7 +824,6 @@ function extractScreenshotDeclarations(steps) {
|
|
|
523
824
|
}
|
|
524
825
|
|
|
525
826
|
// src/reporter.ts
|
|
526
|
-
var MAX_CONCURRENT_FILE_OPS = 5;
|
|
527
827
|
var CrvyRprtr = class {
|
|
528
828
|
ws = null;
|
|
529
829
|
serverUrl;
|
|
@@ -532,6 +832,11 @@ var CrvyRprtr = class {
|
|
|
532
832
|
workerIndex;
|
|
533
833
|
offlineReportPath;
|
|
534
834
|
reportHtmlPath;
|
|
835
|
+
configDir = process.cwd();
|
|
836
|
+
testMetadata = /* @__PURE__ */ new Map();
|
|
837
|
+
playwrightSnapshotDir;
|
|
838
|
+
playwrightSnapshotPathTemplate;
|
|
839
|
+
playwrightToHaveScreenshotPathTemplate;
|
|
535
840
|
isOfflineMode = false;
|
|
536
841
|
hadOfflineMode = false;
|
|
537
842
|
runEvents = [];
|
|
@@ -541,228 +846,155 @@ var CrvyRprtr = class {
|
|
|
541
846
|
this.workerIndex = parseInt(process.env.TEST_WORKER_INDEX ?? "0", 10) || 0;
|
|
542
847
|
this.offlineReportPath = options.offlineReportPath ?? `./crvy-rprtr-${this.workerIndex}.json`;
|
|
543
848
|
this.reportHtmlPath = options.reportHtmlPath ?? "./crvy-rprtr.html";
|
|
849
|
+
this.playwrightSnapshotDir = options.playwrightSnapshotDir;
|
|
850
|
+
this.playwrightSnapshotPathTemplate = options.playwrightSnapshotPathTemplate;
|
|
851
|
+
this.playwrightToHaveScreenshotPathTemplate = options.playwrightToHaveScreenshotPathTemplate;
|
|
544
852
|
}
|
|
545
853
|
async onBegin(config, suite) {
|
|
546
|
-
|
|
547
|
-
|
|
854
|
+
this.configDir = config.configFile === void 0 ? config.rootDir : (0, import_path4.dirname)(config.configFile);
|
|
855
|
+
log(`[CrvyRprtr] Starting run with ${suite.allTests().length} tests`);
|
|
856
|
+
await (0, import_promises3.mkdir)(this.screenshotDir, { recursive: true });
|
|
548
857
|
this.connect();
|
|
549
858
|
}
|
|
550
859
|
connect() {
|
|
551
860
|
const WebSocketConstructor = globalThis.WebSocket;
|
|
552
861
|
if (typeof WebSocketConstructor !== "function") {
|
|
553
|
-
|
|
862
|
+
log("[CrvyRprtr] WebSocket unavailable in current runtime; offline mode enabled");
|
|
554
863
|
this.enableOfflineMode();
|
|
555
864
|
return;
|
|
556
865
|
}
|
|
557
866
|
try {
|
|
558
867
|
this.ws = new WebSocketConstructor(this.serverUrl);
|
|
559
868
|
this.ws.onopen = () => {
|
|
560
|
-
|
|
869
|
+
log("[CrvyRprtr] Connected to Crvy Rprtr server");
|
|
561
870
|
this.isOfflineMode = false;
|
|
562
|
-
for (const
|
|
871
|
+
for (const message of this.queue) this.ws.send(message);
|
|
563
872
|
this.queue = [];
|
|
564
873
|
};
|
|
565
874
|
this.ws.onerror = (error) => {
|
|
566
|
-
|
|
875
|
+
logError("[CrvyRprtr] WebSocket error:", error);
|
|
567
876
|
this.enableOfflineMode();
|
|
568
877
|
};
|
|
569
878
|
this.ws.onclose = () => {
|
|
570
|
-
|
|
879
|
+
log("[CrvyRprtr] Disconnected from Crvy Rprtr server");
|
|
571
880
|
this.enableOfflineMode();
|
|
572
881
|
};
|
|
573
|
-
} catch (
|
|
574
|
-
|
|
882
|
+
} catch (error) {
|
|
883
|
+
logError("[CrvyRprtr] Failed to connect:", error);
|
|
575
884
|
this.enableOfflineMode();
|
|
576
885
|
}
|
|
577
886
|
}
|
|
578
887
|
enableOfflineMode() {
|
|
579
|
-
if (
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
console.log("[CrvyRprtr] Offline mode enabled - events will be queued to file");
|
|
583
|
-
}
|
|
888
|
+
if (this.isOfflineMode) return;
|
|
889
|
+
this.isOfflineMode = this.hadOfflineMode = true;
|
|
890
|
+
log("[CrvyRprtr] Offline mode enabled - events will be queued to file");
|
|
584
891
|
}
|
|
585
|
-
|
|
892
|
+
describeTitlePath(test) {
|
|
586
893
|
const titlePath = [];
|
|
587
|
-
let suite = test.parent;
|
|
588
|
-
while (suite && suite.type === "describe") {
|
|
894
|
+
for (let suite = test.parent; suite?.type === "describe"; suite = suite.parent)
|
|
589
895
|
titlePath.unshift(suite.title);
|
|
590
|
-
|
|
591
|
-
|
|
896
|
+
return titlePath;
|
|
897
|
+
}
|
|
898
|
+
reporterTitlePath(test) {
|
|
899
|
+
return typeof test.titlePath === "function" ? test.titlePath() : ["", test.parent.project()?.name ?? "chromium", test.location.file, ...this.describeTitlePath(test), test.title];
|
|
900
|
+
}
|
|
901
|
+
onTestBegin(test) {
|
|
902
|
+
this.testMetadata.set(test.id, { reporterTitlePath: this.reporterTitlePath(test) });
|
|
592
903
|
this.send({
|
|
593
904
|
type: "test-begin",
|
|
594
905
|
data: {
|
|
595
906
|
id: test.id,
|
|
596
907
|
title: test.title,
|
|
597
|
-
titlePath,
|
|
908
|
+
titlePath: this.describeTitlePath(test),
|
|
598
909
|
browser: test.parent.project()?.name ?? "chromium",
|
|
599
|
-
location: {
|
|
600
|
-
file: test.location.file,
|
|
601
|
-
line: test.location.line
|
|
602
|
-
}
|
|
910
|
+
location: { file: test.location.file, line: test.location.line }
|
|
603
911
|
}
|
|
604
912
|
});
|
|
605
913
|
}
|
|
606
914
|
async onTestEnd(test, result) {
|
|
607
915
|
const screenshotDeclarations = extractScreenshotDeclarations(result.steps);
|
|
608
|
-
const
|
|
609
|
-
const savedAttachments = await this.saveAttachments(test.id, result);
|
|
610
|
-
await this.copySnapshotBaselines(test, result.status, screenshotDeclarations, savedAttachments);
|
|
611
|
-
this.send({
|
|
612
|
-
type: "test-end",
|
|
613
|
-
data: {
|
|
614
|
-
id: test.id,
|
|
615
|
-
title: test.title,
|
|
616
|
-
status: result.status,
|
|
617
|
-
attachments: savedAttachments,
|
|
618
|
-
visualNames,
|
|
619
|
-
error: result.errors.length > 0 ? result.errors[0]?.message : void 0,
|
|
620
|
-
duration: result.duration
|
|
621
|
-
}
|
|
622
|
-
});
|
|
623
|
-
}
|
|
624
|
-
async copySnapshotBaselines(test, status, screenshotDeclarations, savedAttachments) {
|
|
625
|
-
if (status !== "passed") return;
|
|
626
|
-
const namedDeclarations = screenshotDeclarations.filter(
|
|
627
|
-
(declaration) => declaration.snapshotBaseName !== void 0
|
|
628
|
-
);
|
|
629
|
-
if (namedDeclarations.length === 0) return;
|
|
630
|
-
const projectName = test.parent.project()?.name;
|
|
631
|
-
const snapshotDir = `${test.location.file}-snapshots`;
|
|
632
|
-
const testScreenshotDir = (0, import_path2.join)(this.screenshotDir, this.sanitizeId(test.id));
|
|
633
|
-
const limit = (0, import_p_limit.default)(MAX_CONCURRENT_FILE_OPS);
|
|
634
|
-
const copyPromises = namedDeclarations.map(
|
|
635
|
-
({ visualName, snapshotBaseName }) => limit(async () => {
|
|
636
|
-
const destName = `${visualName}-expected.png`;
|
|
637
|
-
const destPath = (0, import_path2.join)(testScreenshotDir, destName);
|
|
638
|
-
const snapshotPath = projectName === "" ? (0, import_path2.join)(snapshotDir, `${snapshotBaseName}-${process.platform}.png`) : (0, import_path2.join)(snapshotDir, `${snapshotBaseName}-${projectName ?? "chromium"}-${process.platform}.png`);
|
|
639
|
-
try {
|
|
640
|
-
await (0, import_promises2.mkdir)((0, import_path2.dirname)(destPath), { recursive: true });
|
|
641
|
-
await (0, import_promises2.copyFile)(snapshotPath, destPath);
|
|
642
|
-
savedAttachments.push({
|
|
643
|
-
name: destName,
|
|
644
|
-
path: `${this.sanitizeId(test.id)}/${destName}`,
|
|
645
|
-
contentType: "image/png"
|
|
646
|
-
});
|
|
647
|
-
console.log(`[CrvyRprtr] Attached baseline: ${snapshotPath}`);
|
|
648
|
-
} catch {
|
|
649
|
-
}
|
|
650
|
-
})
|
|
651
|
-
);
|
|
652
|
-
await Promise.all(copyPromises);
|
|
653
|
-
}
|
|
654
|
-
async saveAttachments(testId, result) {
|
|
655
|
-
const savedAttachments = [];
|
|
656
|
-
const testScreenshotDir = (0, import_path2.join)(this.screenshotDir, this.sanitizeId(testId));
|
|
657
|
-
const limit = (0, import_p_limit.default)(MAX_CONCURRENT_FILE_OPS);
|
|
658
|
-
const attachmentPromises = result.attachments.filter(
|
|
659
|
-
(attachment) => attachment.contentType === "image/png" && attachment.path !== void 0
|
|
660
|
-
).map(
|
|
661
|
-
(attachment) => limit(async () => {
|
|
662
|
-
try {
|
|
663
|
-
await (0, import_promises2.mkdir)(testScreenshotDir, { recursive: true });
|
|
664
|
-
const fileName = attachment.name;
|
|
665
|
-
const destPath = (0, import_path2.join)(testScreenshotDir, fileName);
|
|
666
|
-
await (0, import_promises2.copyFile)(attachment.path, destPath);
|
|
667
|
-
const attachmentData = {
|
|
668
|
-
name: attachment.name,
|
|
669
|
-
path: `${this.sanitizeId(testId)}/${fileName}`,
|
|
670
|
-
contentType: attachment.contentType
|
|
671
|
-
};
|
|
672
|
-
savedAttachments.push(attachmentData);
|
|
673
|
-
console.log(`[CrvyRprtr] Saved screenshot: ${destPath}`);
|
|
674
|
-
} catch (e) {
|
|
675
|
-
console.error(`[CrvyRprtr] Failed to save screenshot: ${attachment.path}`, e);
|
|
676
|
-
const fallbackData = {
|
|
677
|
-
name: attachment.name,
|
|
678
|
-
path: attachment.path,
|
|
679
|
-
contentType: attachment.contentType
|
|
680
|
-
};
|
|
681
|
-
savedAttachments.push(fallbackData);
|
|
682
|
-
}
|
|
683
|
-
})
|
|
684
|
-
);
|
|
685
|
-
await Promise.all(attachmentPromises);
|
|
686
|
-
return savedAttachments;
|
|
687
|
-
}
|
|
688
|
-
sanitizeId(id) {
|
|
689
|
-
return id.replace(/[^a-zA-Z0-9-_]/g, "_");
|
|
690
|
-
}
|
|
691
|
-
async writeOfflineReport() {
|
|
692
|
-
if (this.runEvents.length === 0) {
|
|
693
|
-
console.log("[CrvyRprtr] No offline events to write");
|
|
694
|
-
return;
|
|
695
|
-
}
|
|
916
|
+
const savedAttachments = await saveAttachments(this.screenshotDir, test.id, result);
|
|
696
917
|
try {
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
console.error("[CrvyRprtr] Failed to write offline report:", e);
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
async writeStaticArtifact() {
|
|
714
|
-
try {
|
|
715
|
-
await writeReportArtifact({
|
|
716
|
-
events: this.runEvents,
|
|
717
|
-
screenshotDir: this.screenshotDir,
|
|
718
|
-
reportHtmlPath: this.reportHtmlPath
|
|
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,
|
|
924
|
+
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
|
+
}
|
|
719
931
|
});
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
console.error("[CrvyRprtr] Failed to write report artifact:", e);
|
|
932
|
+
} finally {
|
|
933
|
+
this.testMetadata.delete(test.id);
|
|
723
934
|
}
|
|
724
935
|
}
|
|
936
|
+
baselineInput(test, shots) {
|
|
937
|
+
const project = test.parent.project();
|
|
938
|
+
const snapshotDir = this.playwrightSnapshotDir ?? project?.snapshotDir;
|
|
939
|
+
if (project === void 0 || typeof project.testDir !== "string" || typeof snapshotDir !== "string") return null;
|
|
940
|
+
return {
|
|
941
|
+
testFile: test.location.file,
|
|
942
|
+
reporterTitlePath: this.testMetadata.get(test.id)?.reporterTitlePath ?? this.reporterTitlePath(test),
|
|
943
|
+
declarations: shots,
|
|
944
|
+
config: {
|
|
945
|
+
configDir: this.configDir,
|
|
946
|
+
testDir: project.testDir,
|
|
947
|
+
snapshotDir,
|
|
948
|
+
projectName: project.name,
|
|
949
|
+
snapshotSuffix: process.platform,
|
|
950
|
+
snapshotPathTemplate: this.playwrightSnapshotPathTemplate,
|
|
951
|
+
toHaveScreenshotPathTemplate: this.playwrightToHaveScreenshotPathTemplate
|
|
952
|
+
},
|
|
953
|
+
snapshotPathExists: import_fs.existsSync
|
|
954
|
+
};
|
|
955
|
+
}
|
|
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);
|
|
964
|
+
const limit = (0, import_p_limit2.default)(5);
|
|
965
|
+
await Promise.all(
|
|
966
|
+
targets.map(
|
|
967
|
+
(target) => limit(() => copyResolvedBaseline(safeTestId, testScreenshotDir, target, savedAttachments))
|
|
968
|
+
)
|
|
969
|
+
);
|
|
970
|
+
}
|
|
725
971
|
async onEnd(result) {
|
|
726
|
-
this.send({
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
}
|
|
731
|
-
});
|
|
732
|
-
await this.writeStaticArtifact();
|
|
733
|
-
if (this.hadOfflineMode) {
|
|
734
|
-
await this.writeOfflineReport();
|
|
735
|
-
}
|
|
736
|
-
await new Promise((resolve2) => {
|
|
972
|
+
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);
|
|
975
|
+
await new Promise((resolve3) => {
|
|
737
976
|
if (!this.ws || this.ws.readyState === WebSocket.CLOSED) {
|
|
738
|
-
|
|
977
|
+
resolve3();
|
|
739
978
|
return;
|
|
740
979
|
}
|
|
741
980
|
this.ws.onclose = () => {
|
|
742
|
-
|
|
981
|
+
resolve3();
|
|
743
982
|
};
|
|
744
983
|
setTimeout(() => {
|
|
745
984
|
this.ws?.close();
|
|
746
|
-
|
|
985
|
+
resolve3();
|
|
747
986
|
}, 1e3);
|
|
748
987
|
this.ws.close();
|
|
749
988
|
});
|
|
750
989
|
}
|
|
751
|
-
send(
|
|
752
|
-
const
|
|
753
|
-
if (
|
|
754
|
-
this.runEvents.push({
|
|
755
|
-
|
|
756
|
-
data: msgObj.data
|
|
757
|
-
});
|
|
758
|
-
}
|
|
759
|
-
const payload = JSON.stringify(msg);
|
|
990
|
+
send(message) {
|
|
991
|
+
const event = message;
|
|
992
|
+
if (event.type === "test-begin" || event.type === "test-end" || event.type === "run-end")
|
|
993
|
+
this.runEvents.push({ type: event.type, data: event.data });
|
|
994
|
+
const payload = JSON.stringify(message);
|
|
760
995
|
if (!this.isOfflineMode) {
|
|
761
|
-
if (this.ws?.readyState === WebSocket.OPEN)
|
|
762
|
-
|
|
763
|
-
} else {
|
|
764
|
-
this.queue.push(payload);
|
|
765
|
-
}
|
|
996
|
+
if (this.ws?.readyState === WebSocket.OPEN) this.ws.send(payload);
|
|
997
|
+
else this.queue.push(payload);
|
|
766
998
|
}
|
|
767
999
|
}
|
|
768
1000
|
};
|