@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/server.cjs
CHANGED
|
@@ -35,15 +35,16 @@ __export(server_exports, {
|
|
|
35
35
|
module.exports = __toCommonJS(server_exports);
|
|
36
36
|
|
|
37
37
|
// src/server/app.ts
|
|
38
|
-
var
|
|
38
|
+
var import_path8 = require("path");
|
|
39
39
|
var import_url = require("url");
|
|
40
40
|
var import_p_limit = __toESM(require("p-limit"), 1);
|
|
41
41
|
|
|
42
42
|
// src/offline-reports.ts
|
|
43
43
|
var import_promises = require("fs/promises");
|
|
44
|
-
var
|
|
44
|
+
var import_path2 = require("path");
|
|
45
45
|
|
|
46
46
|
// src/report-utils.ts
|
|
47
|
+
var import_path = require("path");
|
|
47
48
|
function normalizeScreenshotsBaseUrl(baseUrl) {
|
|
48
49
|
return baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
49
50
|
}
|
|
@@ -88,7 +89,7 @@ function attachmentsToImages(attachments, screenshotsBaseUrl = "/screenshots/")
|
|
|
88
89
|
const role = match[2];
|
|
89
90
|
if (baseName === null || baseName === void 0 || role === null || role === void 0) continue;
|
|
90
91
|
images[baseName] ??= {};
|
|
91
|
-
const url = `${baseUrl}${attachment.path}`;
|
|
92
|
+
const url = (0, import_path.isAbsolute)(attachment.path) ? `/file/${encodeURIComponent(attachment.path)}` : `${baseUrl}${attachment.path}`;
|
|
92
93
|
const img = images[baseName];
|
|
93
94
|
if (img !== null && img !== void 0) {
|
|
94
95
|
if (role === "actual") img.actual = url;
|
|
@@ -300,10 +301,29 @@ var CrvyRprtrSuiteSchema = import_zod.z.lazy(
|
|
|
300
301
|
children: import_zod.z.record(import_zod.z.string(), import_zod.z.union([CrvyRprtrSuiteSchema, CrvyRprtrTestSchema])).optional()
|
|
301
302
|
})
|
|
302
303
|
);
|
|
303
|
-
var
|
|
304
|
+
var IncomingWebSocketMessageSchema = import_zod.z.object({
|
|
304
305
|
type: import_zod.z.enum(["test-begin", "test-end", "run-end", "approve", "sync"]),
|
|
305
306
|
data: import_zod.z.unknown()
|
|
306
307
|
});
|
|
308
|
+
var WebSocketMessageSchema = import_zod.z.discriminatedUnion("type", [
|
|
309
|
+
import_zod.z.object({ type: import_zod.z.literal("test-begin"), data: TestDataSchema }),
|
|
310
|
+
import_zod.z.object({ type: import_zod.z.literal("test-update"), data: TestDataSchema }),
|
|
311
|
+
import_zod.z.object({
|
|
312
|
+
type: import_zod.z.literal("run-end"),
|
|
313
|
+
data: import_zod.z.object({
|
|
314
|
+
status: import_zod.z.enum(["passed", "failed", "skipped"]),
|
|
315
|
+
removedTestIds: import_zod.z.array(import_zod.z.string())
|
|
316
|
+
})
|
|
317
|
+
}),
|
|
318
|
+
import_zod.z.object({
|
|
319
|
+
type: import_zod.z.literal("sync"),
|
|
320
|
+
data: import_zod.z.object({
|
|
321
|
+
tests: import_zod.z.record(import_zod.z.string(), TestDataSchema),
|
|
322
|
+
isUpdateMode: import_zod.z.boolean().optional()
|
|
323
|
+
})
|
|
324
|
+
}),
|
|
325
|
+
import_zod.z.object({ type: import_zod.z.literal("approve"), data: import_zod.z.unknown() })
|
|
326
|
+
]);
|
|
307
327
|
var TestBeginDataSchema = import_zod.z.object({
|
|
308
328
|
id: import_zod.z.string(),
|
|
309
329
|
title: import_zod.z.string(),
|
|
@@ -323,6 +343,9 @@ var TestEndDataSchema = import_zod.z.object({
|
|
|
323
343
|
error: import_zod.z.string().optional(),
|
|
324
344
|
duration: import_zod.z.number().optional()
|
|
325
345
|
});
|
|
346
|
+
var RunEndDataSchema = import_zod.z.object({
|
|
347
|
+
status: import_zod.z.enum(["passed", "failed", "skipped"])
|
|
348
|
+
});
|
|
326
349
|
var ReportDataSchema = import_zod.z.object({
|
|
327
350
|
isRunning: import_zod.z.boolean(),
|
|
328
351
|
tests: import_zod.z.record(import_zod.z.string(), TestDataSchema),
|
|
@@ -377,7 +400,7 @@ var OFFLINE_REPORT_FILE_PATTERN = /^crvy-rprtr(?:-\d+)?\.json$/;
|
|
|
377
400
|
async function findOfflineReportPaths(searchDir) {
|
|
378
401
|
try {
|
|
379
402
|
const entries = await (0, import_promises.readdir)(searchDir);
|
|
380
|
-
return entries.filter((entry) => OFFLINE_REPORT_FILE_PATTERN.test(entry)).sort((left, right) => left.localeCompare(right)).map((entry) => (0,
|
|
403
|
+
return entries.filter((entry) => OFFLINE_REPORT_FILE_PATTERN.test(entry)).sort((left, right) => left.localeCompare(right)).map((entry) => (0, import_path2.join)(searchDir, entry));
|
|
381
404
|
} catch (error) {
|
|
382
405
|
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
383
406
|
return [];
|
|
@@ -429,7 +452,7 @@ function mergeOfflineReportsIntoTests(existingTests, offlineReports, options = {
|
|
|
429
452
|
|
|
430
453
|
// src/server/file-utils.ts
|
|
431
454
|
var import_promises2 = require("fs/promises");
|
|
432
|
-
var
|
|
455
|
+
var import_path3 = require("path");
|
|
433
456
|
function isFileNotFound(error) {
|
|
434
457
|
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
435
458
|
}
|
|
@@ -470,16 +493,16 @@ async function readJsonFile(filePath) {
|
|
|
470
493
|
}
|
|
471
494
|
}
|
|
472
495
|
async function writeJsonFile(filePath, value) {
|
|
473
|
-
await (0, import_promises2.mkdir)((0,
|
|
496
|
+
await (0, import_promises2.mkdir)((0, import_path3.dirname)(filePath), { recursive: true });
|
|
474
497
|
await (0, import_promises2.writeFile)(filePath, `${JSON.stringify(value, null, 2)}
|
|
475
498
|
`);
|
|
476
499
|
}
|
|
477
500
|
async function copyFilePortable(sourcePath, destinationPath) {
|
|
478
|
-
await (0, import_promises2.mkdir)((0,
|
|
501
|
+
await (0, import_promises2.mkdir)((0, import_path3.dirname)(destinationPath), { recursive: true });
|
|
479
502
|
await (0, import_promises2.copyFile)(sourcePath, destinationPath);
|
|
480
503
|
}
|
|
481
504
|
function inferContentType(filePath) {
|
|
482
|
-
switch ((0,
|
|
505
|
+
switch ((0, import_path3.extname)(filePath).toLowerCase()) {
|
|
483
506
|
case ".css":
|
|
484
507
|
return "text/css";
|
|
485
508
|
case ".gif":
|
|
@@ -509,7 +532,10 @@ async function respondWithFile(filePath, contentType) {
|
|
|
509
532
|
try {
|
|
510
533
|
const file = await (0, import_promises2.readFile)(filePath);
|
|
511
534
|
const resolvedContentType = contentType ?? inferContentType(filePath);
|
|
512
|
-
const headers =
|
|
535
|
+
const headers = { "X-Content-Type-Options": "nosniff" };
|
|
536
|
+
if (resolvedContentType !== void 0) {
|
|
537
|
+
headers["Content-Type"] = resolvedContentType;
|
|
538
|
+
}
|
|
513
539
|
return new Response(file, { headers });
|
|
514
540
|
} catch (error) {
|
|
515
541
|
if (isFileNotFound(error)) {
|
|
@@ -519,56 +545,17 @@ async function respondWithFile(filePath, contentType) {
|
|
|
519
545
|
}
|
|
520
546
|
}
|
|
521
547
|
|
|
522
|
-
// src/server/utils.ts
|
|
523
|
-
function broadcastToBrowsers(wsClients, msg) {
|
|
524
|
-
const payload = JSON.stringify(msg);
|
|
525
|
-
wsClients.forEach((ws) => {
|
|
526
|
-
ws.send(payload);
|
|
527
|
-
});
|
|
528
|
-
}
|
|
529
|
-
|
|
530
548
|
// src/server/handlers.ts
|
|
531
|
-
|
|
532
|
-
const test = applyTestBeginEvent(ctx, data);
|
|
533
|
-
ctx.reportData.isRunning = true;
|
|
534
|
-
console.log(` \u25B6 [${test.browser ?? "?"}] ${test.title}`);
|
|
535
|
-
broadcastToBrowsers(ctx.wsClients, { type: "test-begin", data });
|
|
536
|
-
}
|
|
537
|
-
function handleTestEnd(ctx, data) {
|
|
538
|
-
const result = applyTestEndEvent(ctx, data);
|
|
539
|
-
if (result !== null) {
|
|
540
|
-
const { test, diffCount } = result;
|
|
541
|
-
const icon = data.status === "passed" ? "\u2713" : data.status === "skipped" ? "\u2013" : "\u2717";
|
|
542
|
-
const dur = data.duration === null || data.duration === void 0 ? "" : ` (${data.duration}ms)`;
|
|
543
|
-
const diffNote = diffCount > 0 ? ` [${diffCount} diff(s)]` : "";
|
|
544
|
-
const errNote = data.error !== null && data.error !== void 0 ? `
|
|
545
|
-
Error: ${data.error}` : "";
|
|
546
|
-
console.log(` ${icon} [${test.browser}] ${test.title}${dur}${diffNote}${errNote}`);
|
|
547
|
-
}
|
|
548
|
-
broadcastToBrowsers(ctx.wsClients, { type: "test-update", data });
|
|
549
|
-
}
|
|
550
|
-
async function handleRunEnd(ctx, data) {
|
|
551
|
-
const { passed, failed, pending } = finalizeRunEvent(ctx);
|
|
552
|
-
await ctx.saveReport();
|
|
553
|
-
console.log(`
|
|
554
|
-
Run complete \u2014 ${passed} passed, ${failed} failed, ${pending} skipped`);
|
|
555
|
-
broadcastToBrowsers(ctx.wsClients, { type: "run-end", data });
|
|
556
|
-
}
|
|
557
|
-
function handleApprove() {
|
|
558
|
-
console.log("[Server] Received approve message via WebSocket (handled via HTTP API)");
|
|
559
|
-
}
|
|
560
|
-
function handleSync(ctx) {
|
|
561
|
-
console.log("[Server] Received sync message");
|
|
562
|
-
broadcastToBrowsers(ctx.wsClients, { type: "sync", data: ctx.reportData });
|
|
563
|
-
}
|
|
549
|
+
var import_fs2 = require("fs");
|
|
564
550
|
|
|
565
|
-
// src/server/routes.ts
|
|
551
|
+
// src/server/artifact-routes.ts
|
|
566
552
|
var import_fs = require("fs");
|
|
567
|
-
var
|
|
553
|
+
var import_promises3 = require("fs/promises");
|
|
554
|
+
var import_path6 = require("path");
|
|
568
555
|
|
|
569
556
|
// src/snapshot-path-resolver.ts
|
|
570
557
|
var import_crypto = require("crypto");
|
|
571
|
-
var
|
|
558
|
+
var import_path4 = require("path");
|
|
572
559
|
var DEFAULT_SCREENSHOT_TEMPLATE = "{snapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}{-projectName}{-snapshotSuffix}{ext}";
|
|
573
560
|
var WINDOWS_FILESYSTEM_FRIENDLY_LENGTH = 60;
|
|
574
561
|
function isUnsafeFilePathCharacter(character) {
|
|
@@ -603,16 +590,16 @@ function trimLongString(value, length = WINDOWS_FILESYSTEM_FRIENDLY_LENGTH) {
|
|
|
603
590
|
const end = length - middle.length - start;
|
|
604
591
|
return value.slice(0, start) + middle + value.slice(-end);
|
|
605
592
|
}
|
|
606
|
-
function sanitizeFilePathBeforeExtension(filePath, extension = (0,
|
|
593
|
+
function sanitizeFilePathBeforeExtension(filePath, extension = (0, import_path4.extname)(filePath)) {
|
|
607
594
|
const base = filePath.slice(0, filePath.length - extension.length);
|
|
608
595
|
return sanitizeForFilePath(base) + extension;
|
|
609
596
|
}
|
|
610
597
|
function addSuffixToFilePath(filePath, suffix) {
|
|
611
|
-
const extension = (0,
|
|
598
|
+
const extension = (0, import_path4.extname)(filePath);
|
|
612
599
|
return filePath.slice(0, filePath.length - extension.length) + suffix + extension;
|
|
613
600
|
}
|
|
614
601
|
function normalizedSnapshotDir(config) {
|
|
615
|
-
return (0,
|
|
602
|
+
return (0, import_path4.resolve)(config.configDir, config.snapshotDir);
|
|
616
603
|
}
|
|
617
604
|
function templateValue(template, token, value) {
|
|
618
605
|
return template.replace(
|
|
@@ -622,8 +609,8 @@ function templateValue(template, token, value) {
|
|
|
622
609
|
}
|
|
623
610
|
function applyTemplate(input, nameArgument, extension) {
|
|
624
611
|
const template = input.config.toHaveScreenshotPathTemplate ?? input.config.snapshotPathTemplate ?? DEFAULT_SCREENSHOT_TEMPLATE;
|
|
625
|
-
const relativeTestFilePath = (0,
|
|
626
|
-
const parsed = (0,
|
|
612
|
+
const relativeTestFilePath = (0, import_path4.relative)(input.config.testDir, input.testFile);
|
|
613
|
+
const parsed = (0, import_path4.parse)(relativeTestFilePath);
|
|
627
614
|
const tokens = [
|
|
628
615
|
["testDir", input.config.testDir],
|
|
629
616
|
["snapshotDir", normalizedSnapshotDir(input.config)],
|
|
@@ -641,16 +628,16 @@ function applyTemplate(input, nameArgument, extension) {
|
|
|
641
628
|
(currentTemplate, [token, value]) => templateValue(currentTemplate, token, value),
|
|
642
629
|
template
|
|
643
630
|
);
|
|
644
|
-
return (0,
|
|
631
|
+
return (0, import_path4.resolve)(input.config.configDir, snapshotPath);
|
|
645
632
|
}
|
|
646
|
-
function removeExtension(filePath, extension = (0,
|
|
633
|
+
function removeExtension(filePath, extension = (0, import_path4.extname)(filePath)) {
|
|
647
634
|
return filePath.slice(0, filePath.length - extension.length);
|
|
648
635
|
}
|
|
649
636
|
function snapshotNameParts(declaredName) {
|
|
650
|
-
const extension = (0,
|
|
637
|
+
const extension = (0, import_path4.extname)(declaredName) || ".png";
|
|
651
638
|
return {
|
|
652
639
|
extension,
|
|
653
|
-
filePath: (0,
|
|
640
|
+
filePath: (0, import_path4.extname)(declaredName) === "" ? `${declaredName}${extension}` : declaredName
|
|
654
641
|
};
|
|
655
642
|
}
|
|
656
643
|
function filePathForOccurrence(filePath, occurrenceIndex) {
|
|
@@ -674,7 +661,7 @@ function resolveStringCallTarget(input, declaration) {
|
|
|
674
661
|
function resolveArrayCallTarget(input, declaration) {
|
|
675
662
|
const { extension, filePath } = snapshotNameParts(declaration.declaredName);
|
|
676
663
|
const occurrenceFilePath = filePathForOccurrence(filePath, declaration.occurrenceIndex);
|
|
677
|
-
const nameArgument = (0,
|
|
664
|
+
const nameArgument = (0, import_path4.join)((0, import_path4.dirname)(occurrenceFilePath), (0, import_path4.basename)(occurrenceFilePath, extension));
|
|
678
665
|
return createResolvedBaselineTarget(input, declaration, nameArgument, extension);
|
|
679
666
|
}
|
|
680
667
|
function resolveNamedTarget(input, declaration) {
|
|
@@ -699,9 +686,11 @@ function resolveNamedTarget(input, declaration) {
|
|
|
699
686
|
function reporterTitlesWithoutProjectAndFile(reporterTitlePath2) {
|
|
700
687
|
return reporterTitlePath2.slice(3).filter((part) => part !== "");
|
|
701
688
|
}
|
|
689
|
+
function anonymousNameFromTitles(titles, occurrenceIndex) {
|
|
690
|
+
return sanitizeFilePathBeforeExtension(trimLongString(`${titles.join(" ")} ${occurrenceIndex}.png`), ".png");
|
|
691
|
+
}
|
|
702
692
|
function anonymousName(reporterTitlePath2, occurrenceIndex) {
|
|
703
|
-
|
|
704
|
-
return sanitizeFilePathBeforeExtension(trimLongString(rawAnonymousName), ".png");
|
|
693
|
+
return anonymousNameFromTitles(reporterTitlesWithoutProjectAndFile(reporterTitlePath2), occurrenceIndex);
|
|
705
694
|
}
|
|
706
695
|
function resolveTarget(input, declaration) {
|
|
707
696
|
switch (declaration.kind) {
|
|
@@ -710,7 +699,7 @@ function resolveTarget(input, declaration) {
|
|
|
710
699
|
case "unnamed": {
|
|
711
700
|
const anonymousFileName = anonymousName(input.reporterTitlePath, declaration.occurrenceIndex);
|
|
712
701
|
const extension = ".png";
|
|
713
|
-
const nameArgument = (0,
|
|
702
|
+
const nameArgument = (0, import_path4.join)((0, import_path4.dirname)(anonymousFileName), (0, import_path4.basename)(anonymousFileName, extension));
|
|
714
703
|
return createResolvedBaselineTarget(input, declaration, nameArgument, extension);
|
|
715
704
|
}
|
|
716
705
|
}
|
|
@@ -722,41 +711,67 @@ function resolveBaselineTargets(input) {
|
|
|
722
711
|
});
|
|
723
712
|
}
|
|
724
713
|
|
|
725
|
-
// src/server/
|
|
714
|
+
// src/server/utils.ts
|
|
715
|
+
var import_path5 = require("path");
|
|
726
716
|
var LIVE_UPDATES_WEBSOCKET_PATH = "/";
|
|
717
|
+
function broadcastToBrowsers(wsClients, msg) {
|
|
718
|
+
const payload = JSON.stringify(msg);
|
|
719
|
+
wsClients.forEach((ws) => {
|
|
720
|
+
ws.send(payload);
|
|
721
|
+
});
|
|
722
|
+
}
|
|
727
723
|
function isWebSocketUpgradeRequest(req) {
|
|
728
724
|
return new URL(req.url).pathname === LIVE_UPDATES_WEBSOCKET_PATH && req.headers.get("upgrade")?.toLowerCase() === "websocket";
|
|
729
725
|
}
|
|
730
|
-
|
|
731
|
-
const
|
|
732
|
-
return
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
return css ?? new Response("Not Found", { status: 404 });
|
|
737
|
-
}
|
|
738
|
-
async function handleSrcFiles(req) {
|
|
739
|
-
const path = new URL(req.url).pathname.slice("/src/".length);
|
|
740
|
-
const filePath = `./src/${path}`;
|
|
741
|
-
const contentType = filePath.endsWith(".ts") || filePath.endsWith(".tsx") ? "application/javascript" : filePath.endsWith(".css") ? "text/css" : "text/plain";
|
|
742
|
-
const file = await respondWithFile(filePath, contentType);
|
|
743
|
-
return file ?? new Response("Not Found", { status: 404 });
|
|
726
|
+
function isPathWithinRoots(target, roots) {
|
|
727
|
+
const resolvedTarget = (0, import_path5.resolve)(target);
|
|
728
|
+
return roots.some((root) => {
|
|
729
|
+
const rel = (0, import_path5.relative)((0, import_path5.resolve)(root), resolvedTarget);
|
|
730
|
+
return rel === "" || !rel.startsWith(`..${import_path5.sep}`) && rel !== ".." && !(0, import_path5.isAbsolute)(rel);
|
|
731
|
+
});
|
|
744
732
|
}
|
|
745
|
-
|
|
746
|
-
|
|
733
|
+
|
|
734
|
+
// src/server/artifact-routes.ts
|
|
735
|
+
async function realpathOrNull(path) {
|
|
736
|
+
try {
|
|
737
|
+
return await (0, import_promises3.realpath)(path);
|
|
738
|
+
} catch {
|
|
739
|
+
return null;
|
|
740
|
+
}
|
|
747
741
|
}
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
742
|
+
async function handleFile(ctx, req) {
|
|
743
|
+
const notFound = () => new Response("Not Found", { status: 404 });
|
|
744
|
+
let decodedPath;
|
|
745
|
+
try {
|
|
746
|
+
decodedPath = (0, import_path6.resolve)(decodeURIComponent(new URL(req.url).pathname.slice("/file/".length)));
|
|
747
|
+
} catch {
|
|
748
|
+
return notFound();
|
|
749
|
+
}
|
|
750
|
+
const realTarget = await realpathOrNull(decodedPath);
|
|
751
|
+
if (realTarget === null) {
|
|
752
|
+
return notFound();
|
|
753
|
+
}
|
|
754
|
+
const realRoots = (await Promise.all((ctx.artifactRoots ?? []).map((root) => realpathOrNull((0, import_path6.resolve)(root))))).filter(
|
|
755
|
+
(root) => root !== null
|
|
756
|
+
);
|
|
757
|
+
if (!isPathWithinRoots(realTarget, realRoots)) {
|
|
758
|
+
return notFound();
|
|
759
|
+
}
|
|
760
|
+
try {
|
|
761
|
+
const file = await respondWithFile(realTarget);
|
|
762
|
+
return file ?? notFound();
|
|
763
|
+
} catch {
|
|
764
|
+
return notFound();
|
|
765
|
+
}
|
|
751
766
|
}
|
|
752
767
|
function reporterTitlePath(test) {
|
|
753
768
|
const testFile = test.location?.file;
|
|
754
769
|
return ["", test.browser, testFile ?? "", ...test.titlePath, test.title];
|
|
755
770
|
}
|
|
756
|
-
function
|
|
771
|
+
function resolveBaselineSnapshotPath(routing, test, retry, imageName) {
|
|
757
772
|
const testFile = test.location?.file;
|
|
758
773
|
const declaration = test.results?.[retry]?.visualDeclarations?.find((candidate) => candidate.visualName === imageName);
|
|
759
|
-
if (
|
|
774
|
+
if (routing === void 0 || testFile === void 0 || declaration === void 0) {
|
|
760
775
|
return null;
|
|
761
776
|
}
|
|
762
777
|
const targets = resolveBaselineTargets({
|
|
@@ -764,18 +779,162 @@ function resolveApprovalTarget(ctx, test, retry, imageName) {
|
|
|
764
779
|
reporterTitlePath: reporterTitlePath(test),
|
|
765
780
|
declarations: [declaration],
|
|
766
781
|
config: {
|
|
767
|
-
configDir:
|
|
768
|
-
testDir:
|
|
769
|
-
snapshotDir:
|
|
782
|
+
configDir: routing.configDir,
|
|
783
|
+
testDir: routing.playwrightTestDir ?? (0, import_path6.dirname)(testFile),
|
|
784
|
+
snapshotDir: routing.playwrightSnapshotDir ?? (0, import_path6.dirname)(testFile),
|
|
770
785
|
projectName: test.browser,
|
|
771
786
|
snapshotSuffix: process.platform,
|
|
772
|
-
snapshotPathTemplate:
|
|
773
|
-
toHaveScreenshotPathTemplate:
|
|
787
|
+
snapshotPathTemplate: routing.playwrightSnapshotPathTemplate,
|
|
788
|
+
toHaveScreenshotPathTemplate: routing.playwrightToHaveScreenshotPathTemplate
|
|
774
789
|
},
|
|
775
790
|
snapshotPathExists: import_fs.existsSync
|
|
776
791
|
});
|
|
777
792
|
return targets.length === 1 ? targets[0]?.snapshotPath ?? null : null;
|
|
778
793
|
}
|
|
794
|
+
async function handleBaseline(ctx, req) {
|
|
795
|
+
const notFound = () => new Response("Not Found", { status: 404 });
|
|
796
|
+
let testId;
|
|
797
|
+
let retry;
|
|
798
|
+
let visualName;
|
|
799
|
+
try {
|
|
800
|
+
const segments = new URL(req.url).pathname.slice("/baseline/".length).split("/");
|
|
801
|
+
const [rawTestId, retryRaw, ...visualNameParts] = segments;
|
|
802
|
+
if (rawTestId === void 0 || retryRaw === void 0 || !/^\d+$/.test(retryRaw) || visualNameParts.length === 0) {
|
|
803
|
+
return notFound();
|
|
804
|
+
}
|
|
805
|
+
testId = decodeURIComponent(rawTestId);
|
|
806
|
+
retry = Number(retryRaw);
|
|
807
|
+
visualName = decodeURIComponent(visualNameParts.join("/"));
|
|
808
|
+
} catch {
|
|
809
|
+
return notFound();
|
|
810
|
+
}
|
|
811
|
+
const test = ctx.reportData.tests[testId];
|
|
812
|
+
if (test === void 0) {
|
|
813
|
+
return notFound();
|
|
814
|
+
}
|
|
815
|
+
const snapshotPath = resolveBaselineSnapshotPath(ctx.approvalRouting, test, retry, visualName);
|
|
816
|
+
if (snapshotPath === null) {
|
|
817
|
+
return notFound();
|
|
818
|
+
}
|
|
819
|
+
try {
|
|
820
|
+
const file = await respondWithFile(snapshotPath);
|
|
821
|
+
return file ?? notFound();
|
|
822
|
+
} catch {
|
|
823
|
+
return notFound();
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
function handleArtifactRoute(ctx, req, pathname) {
|
|
827
|
+
if (pathname.startsWith("/file/")) {
|
|
828
|
+
return handleFile(ctx, req);
|
|
829
|
+
}
|
|
830
|
+
if (pathname.startsWith("/baseline/")) {
|
|
831
|
+
return handleBaseline(ctx, req);
|
|
832
|
+
}
|
|
833
|
+
return null;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// src/server/handlers.ts
|
|
837
|
+
function handleTestBegin(ctx, data) {
|
|
838
|
+
const test = applyTestBeginEvent(ctx, data);
|
|
839
|
+
ctx.reportData.isRunning = true;
|
|
840
|
+
console.log(` \u25B6 [${test.browser ?? "?"}] ${test.title}`);
|
|
841
|
+
const message = { type: "test-begin", data: test };
|
|
842
|
+
broadcastToBrowsers(ctx.wsClients, message);
|
|
843
|
+
}
|
|
844
|
+
function enrichDeclaredBaselines(ctx, test) {
|
|
845
|
+
const retry = (test.results?.length ?? 0) - 1;
|
|
846
|
+
const images = test.results?.[retry]?.images;
|
|
847
|
+
if (retry < 0 || images === void 0) {
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
for (const [visualName, image] of Object.entries(images)) {
|
|
851
|
+
if (image === void 0 || image.source !== "declared-only") {
|
|
852
|
+
continue;
|
|
853
|
+
}
|
|
854
|
+
const snapshotPath = resolveBaselineSnapshotPath(ctx.approvalRouting, test, retry, visualName);
|
|
855
|
+
if (snapshotPath === null || !(0, import_fs2.existsSync)(snapshotPath)) {
|
|
856
|
+
continue;
|
|
857
|
+
}
|
|
858
|
+
image.expect = `/baseline/${encodeURIComponent(test.id)}/${retry}/${encodeURIComponent(visualName)}`;
|
|
859
|
+
image.source = "baseline-only";
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
function handleTestEnd(ctx, data) {
|
|
863
|
+
const result = applyTestEndEvent(ctx, data);
|
|
864
|
+
if (result === null) {
|
|
865
|
+
console.error("[Server] test-end for unknown test id:", data.id);
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
const { test, diffCount } = result;
|
|
869
|
+
if (data.status === "passed") {
|
|
870
|
+
enrichDeclaredBaselines(ctx, test);
|
|
871
|
+
}
|
|
872
|
+
const icon = data.status === "passed" ? "\u2713" : data.status === "skipped" ? "\u2013" : "\u2717";
|
|
873
|
+
const dur = data.duration === null || data.duration === void 0 ? "" : ` (${data.duration}ms)`;
|
|
874
|
+
const diffNote = diffCount > 0 ? ` [${diffCount} diff(s)]` : "";
|
|
875
|
+
const errNote = data.error !== null && data.error !== void 0 ? `
|
|
876
|
+
Error: ${data.error}` : "";
|
|
877
|
+
console.log(` ${icon} [${test.browser}] ${test.title}${dur}${diffNote}${errNote}`);
|
|
878
|
+
const message = { type: "test-update", data: test };
|
|
879
|
+
broadcastToBrowsers(ctx.wsClients, message);
|
|
880
|
+
}
|
|
881
|
+
async function handleRunEnd(ctx, data) {
|
|
882
|
+
const removedTestIds = Object.keys(ctx.reportData.tests).filter((id) => !ctx.currentRunIds.has(id));
|
|
883
|
+
const { passed, failed, pending } = finalizeRunEvent(ctx);
|
|
884
|
+
await ctx.saveReport();
|
|
885
|
+
console.log(`
|
|
886
|
+
Run complete \u2014 ${passed} passed, ${failed} failed, ${pending} skipped`);
|
|
887
|
+
const message = {
|
|
888
|
+
type: "run-end",
|
|
889
|
+
data: { status: data.status, removedTestIds }
|
|
890
|
+
};
|
|
891
|
+
broadcastToBrowsers(ctx.wsClients, message);
|
|
892
|
+
}
|
|
893
|
+
function handleApprove() {
|
|
894
|
+
console.log("[Server] Received approve message via WebSocket (handled via HTTP API)");
|
|
895
|
+
}
|
|
896
|
+
function handleSync(ctx) {
|
|
897
|
+
console.log("[Server] Received sync message");
|
|
898
|
+
const message = {
|
|
899
|
+
type: "sync",
|
|
900
|
+
data: {
|
|
901
|
+
tests: ctx.reportData.tests,
|
|
902
|
+
isUpdateMode: ctx.reportData.isUpdateMode
|
|
903
|
+
}
|
|
904
|
+
};
|
|
905
|
+
broadcastToBrowsers(ctx.wsClients, message);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// src/server/routes.ts
|
|
909
|
+
var import_path7 = require("path");
|
|
910
|
+
async function handleRoot(ctx) {
|
|
911
|
+
const html = await respondWithFile((0, import_path7.join)(ctx.staticDir, "index.html"), "text/html");
|
|
912
|
+
return html ?? new Response("Not Found", { status: 404 });
|
|
913
|
+
}
|
|
914
|
+
async function handleAppCss() {
|
|
915
|
+
const css = await respondWithFile("./src/client/app.css", "text/css");
|
|
916
|
+
return css ?? new Response("Not Found", { status: 404 });
|
|
917
|
+
}
|
|
918
|
+
async function handleSrcFiles(req) {
|
|
919
|
+
const path = new URL(req.url).pathname.slice("/src/".length);
|
|
920
|
+
const filePath = `./src/${path}`;
|
|
921
|
+
const contentType = filePath.endsWith(".ts") || filePath.endsWith(".tsx") ? "application/javascript" : filePath.endsWith(".css") ? "text/css" : "text/plain";
|
|
922
|
+
const file = await respondWithFile(filePath, contentType);
|
|
923
|
+
return file ?? new Response("Not Found", { status: 404 });
|
|
924
|
+
}
|
|
925
|
+
function handleApiReport(ctx) {
|
|
926
|
+
return Response.json(ctx.reportData);
|
|
927
|
+
}
|
|
928
|
+
var APPROVAL_TARGET_ERROR = "Could not resolve approval target";
|
|
929
|
+
function actualPathFromUrl(ctx, actualUrl) {
|
|
930
|
+
if (actualUrl.startsWith("/screenshots/")) {
|
|
931
|
+
return (0, import_path7.join)(ctx.reportData.screenshotDir, actualUrl.slice("/screenshots/".length));
|
|
932
|
+
}
|
|
933
|
+
if (actualUrl.startsWith("/file/")) {
|
|
934
|
+
return decodeURIComponent(actualUrl.slice("/file/".length));
|
|
935
|
+
}
|
|
936
|
+
return actualUrl;
|
|
937
|
+
}
|
|
779
938
|
async function handleApiApprove(ctx, req) {
|
|
780
939
|
try {
|
|
781
940
|
const rawBody = await req.json();
|
|
@@ -793,7 +952,7 @@ async function handleApiApprove(ctx, req) {
|
|
|
793
952
|
if (actualUrl === void 0) {
|
|
794
953
|
return Response.json({ success: false, error: "Actual image not found" }, { status: 409 });
|
|
795
954
|
}
|
|
796
|
-
const snapshotPath =
|
|
955
|
+
const snapshotPath = resolveBaselineSnapshotPath(ctx.approvalRouting, test, retry, image);
|
|
797
956
|
if (snapshotPath === null) {
|
|
798
957
|
return Response.json({ success: false, error: APPROVAL_TARGET_ERROR }, { status: 409 });
|
|
799
958
|
}
|
|
@@ -828,7 +987,7 @@ function createBulkApprovalUpdates(ctx) {
|
|
|
828
987
|
if (actualUrl === void 0) {
|
|
829
988
|
return [Promise.resolve({ kind: "unresolved" })];
|
|
830
989
|
}
|
|
831
|
-
const snapshotPath =
|
|
990
|
+
const snapshotPath = resolveBaselineSnapshotPath(ctx.approvalRouting, test, lastRetry, imageName);
|
|
832
991
|
if (snapshotPath === null) {
|
|
833
992
|
return [Promise.resolve({ kind: "unresolved" })];
|
|
834
993
|
}
|
|
@@ -891,7 +1050,7 @@ async function handleScreenshots(ctx, req) {
|
|
|
891
1050
|
}
|
|
892
1051
|
async function handleDist(ctx, req) {
|
|
893
1052
|
const path = new URL(req.url).pathname.slice("/dist/".length);
|
|
894
|
-
const filePath = (0,
|
|
1053
|
+
const filePath = (0, import_path7.join)(ctx.staticDir, path);
|
|
895
1054
|
const contentType = filePath.endsWith(".css") ? "text/css" : filePath.endsWith(".js") ? "application/javascript" : filePath.endsWith(".svelte") ? "text/plain" : "application/octet-stream";
|
|
896
1055
|
const file = await respondWithFile(filePath, contentType);
|
|
897
1056
|
return file ?? new Response("Not Found", { status: 404 });
|
|
@@ -919,6 +1078,10 @@ function handleHttpRequest(ctx, req) {
|
|
|
919
1078
|
if (pathname.startsWith("/api/images/")) {
|
|
920
1079
|
return handleApiImages(req);
|
|
921
1080
|
}
|
|
1081
|
+
const artifactResponse = handleArtifactRoute(ctx, req, pathname);
|
|
1082
|
+
if (artifactResponse !== null) {
|
|
1083
|
+
return artifactResponse;
|
|
1084
|
+
}
|
|
922
1085
|
if (pathname.startsWith("/screenshots/")) {
|
|
923
1086
|
return handleScreenshots(ctx, req);
|
|
924
1087
|
}
|
|
@@ -940,12 +1103,13 @@ function createReportData(options) {
|
|
|
940
1103
|
screenshotDir: options.screenshotDir ?? "./screenshots"
|
|
941
1104
|
};
|
|
942
1105
|
}
|
|
943
|
-
function createHandlerContext(reportData, wsClients, currentRunIds, saveReport) {
|
|
1106
|
+
function createHandlerContext(reportData, wsClients, currentRunIds, saveReport, approvalRouting) {
|
|
944
1107
|
return {
|
|
945
1108
|
reportData,
|
|
946
1109
|
wsClients,
|
|
947
1110
|
currentRunIds,
|
|
948
|
-
saveReport
|
|
1111
|
+
saveReport,
|
|
1112
|
+
approvalRouting
|
|
949
1113
|
};
|
|
950
1114
|
}
|
|
951
1115
|
async function loadReport(reportPath, reportData) {
|
|
@@ -1015,9 +1179,15 @@ async function handleParsedWebSocketMessage(ctx, msg) {
|
|
|
1015
1179
|
handleTestEnd(ctx, parsed);
|
|
1016
1180
|
break;
|
|
1017
1181
|
}
|
|
1018
|
-
case "run-end":
|
|
1019
|
-
|
|
1182
|
+
case "run-end": {
|
|
1183
|
+
const parsed = safeParse(RunEndDataSchema, msg.data);
|
|
1184
|
+
if (parsed === null) {
|
|
1185
|
+
console.error("Invalid run-end message data", msg.data);
|
|
1186
|
+
break;
|
|
1187
|
+
}
|
|
1188
|
+
await handleRunEnd(ctx, parsed);
|
|
1020
1189
|
break;
|
|
1190
|
+
}
|
|
1021
1191
|
case "approve":
|
|
1022
1192
|
handleApprove();
|
|
1023
1193
|
break;
|
|
@@ -1030,7 +1200,7 @@ function createWebSocketMessageHandler(getHandlerContext) {
|
|
|
1030
1200
|
return async function handleWebSocketMessage(message) {
|
|
1031
1201
|
try {
|
|
1032
1202
|
const parsed = JSON.parse(message);
|
|
1033
|
-
const wsMessage = safeParse(
|
|
1203
|
+
const wsMessage = safeParse(IncomingWebSocketMessageSchema, parsed);
|
|
1034
1204
|
if (wsMessage === null) {
|
|
1035
1205
|
console.error("Invalid WebSocket message: missing or invalid type", parsed);
|
|
1036
1206
|
return;
|
|
@@ -1043,19 +1213,19 @@ function createWebSocketMessageHandler(getHandlerContext) {
|
|
|
1043
1213
|
};
|
|
1044
1214
|
}
|
|
1045
1215
|
async function resolveStaticDir(staticDir) {
|
|
1046
|
-
const currentDir = (0,
|
|
1216
|
+
const currentDir = (0, import_path8.dirname)((0, import_url.fileURLToPath)(import_meta.url));
|
|
1047
1217
|
const candidates = staticDir === void 0 ? [
|
|
1048
1218
|
currentDir,
|
|
1049
|
-
(0,
|
|
1050
|
-
(0,
|
|
1051
|
-
(0,
|
|
1052
|
-
(0,
|
|
1053
|
-
(0,
|
|
1054
|
-
] : [staticDir, (0,
|
|
1219
|
+
(0, import_path8.join)(currentDir, "dist"),
|
|
1220
|
+
(0, import_path8.join)(currentDir, "..", "dist"),
|
|
1221
|
+
(0, import_path8.join)(currentDir, "..", "..", "dist"),
|
|
1222
|
+
(0, import_path8.join)(currentDir, ".."),
|
|
1223
|
+
(0, import_path8.join)(currentDir, "..", "..")
|
|
1224
|
+
] : [staticDir, (0, import_path8.join)(staticDir, "dist")];
|
|
1055
1225
|
const resolvedCandidates = await Promise.all(
|
|
1056
1226
|
candidates.map(async (candidate) => ({
|
|
1057
1227
|
candidate,
|
|
1058
|
-
exists: await fileExists((0,
|
|
1228
|
+
exists: await fileExists((0, import_path8.join)(candidate, "index.html"))
|
|
1059
1229
|
}))
|
|
1060
1230
|
);
|
|
1061
1231
|
const resolved = resolvedCandidates.find(({ exists }) => exists);
|
|
@@ -1066,15 +1236,19 @@ async function resolveStaticDir(staticDir) {
|
|
|
1066
1236
|
}
|
|
1067
1237
|
async function resolveReportPath(reportPath) {
|
|
1068
1238
|
if (await isDirectory(reportPath)) {
|
|
1069
|
-
return { reportFile: (0,
|
|
1239
|
+
return { reportFile: (0, import_path8.join)(reportPath, "report.json"), offlineReportDir: reportPath };
|
|
1070
1240
|
}
|
|
1071
|
-
return { reportFile: reportPath, offlineReportDir: (0,
|
|
1241
|
+
return { reportFile: reportPath, offlineReportDir: (0, import_path8.dirname)(reportPath) };
|
|
1072
1242
|
}
|
|
1073
1243
|
function createRoutesContext(reportData, staticDir, saveReport, options) {
|
|
1244
|
+
const artifactRoots = [reportData.screenshotDir, options.outputDir, options.playwrightSnapshotDir].filter(
|
|
1245
|
+
(root) => root !== void 0 && root !== ""
|
|
1246
|
+
);
|
|
1074
1247
|
return {
|
|
1075
1248
|
reportData,
|
|
1076
1249
|
staticDir,
|
|
1077
1250
|
saveReport,
|
|
1251
|
+
artifactRoots,
|
|
1078
1252
|
approvalRouting: {
|
|
1079
1253
|
configDir: options.configDir ?? process.cwd(),
|
|
1080
1254
|
playwrightTestDir: options.playwrightTestDir,
|
|
@@ -1096,7 +1270,7 @@ async function createServerApp(options = {}) {
|
|
|
1096
1270
|
await writeJsonFile(reportFile, reportData);
|
|
1097
1271
|
}
|
|
1098
1272
|
const routesContext = createRoutesContext(reportData, staticDir, saveReport, options);
|
|
1099
|
-
const getHandlerContext = () => createHandlerContext(reportData, wsClients, currentRunIds, saveReport);
|
|
1273
|
+
const getHandlerContext = () => createHandlerContext(reportData, wsClients, currentRunIds, saveReport, routesContext.approvalRouting);
|
|
1100
1274
|
const handleRequest = (req) => handleHttpRequest(routesContext, req);
|
|
1101
1275
|
const handleWebSocketMessage = createWebSocketMessageHandler(getHandlerContext);
|
|
1102
1276
|
await loadReport(reportFile, reportData);
|
|
@@ -1286,7 +1460,7 @@ function attachWebSocketServer(server, app) {
|
|
|
1286
1460
|
});
|
|
1287
1461
|
}
|
|
1288
1462
|
async function listen(server, port) {
|
|
1289
|
-
await new Promise((
|
|
1463
|
+
await new Promise((resolve4, reject) => {
|
|
1290
1464
|
const onError = (error) => {
|
|
1291
1465
|
server.off("error", onError);
|
|
1292
1466
|
reject(error);
|
|
@@ -1294,7 +1468,7 @@ async function listen(server, port) {
|
|
|
1294
1468
|
server.on("error", onError);
|
|
1295
1469
|
server.listen(port, () => {
|
|
1296
1470
|
server.off("error", onError);
|
|
1297
|
-
|
|
1471
|
+
resolve4();
|
|
1298
1472
|
});
|
|
1299
1473
|
});
|
|
1300
1474
|
}
|