@crvy/rprtr 0.0.4 → 0.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +81 -0
- package/README.md +41 -7
- package/dist/chunk-JGS2VWQP.js +500 -0
- package/dist/{chunk-BRFA7JCQ.js → chunk-OMDYTNWY.js} +266 -65
- package/dist/cli.js +2 -2
- package/dist/index.css +55 -0
- package/dist/index.js +691 -550
- package/dist/report-state.d.ts +6 -1
- package/dist/report-state.d.ts.map +1 -1
- package/dist/report-utils.d.ts +6 -1
- package/dist/report-utils.d.ts.map +1 -1
- package/dist/reporter-utils.d.ts +20 -1
- package/dist/reporter-utils.d.ts.map +1 -1
- package/dist/reporter.cjs +474 -156
- package/dist/reporter.d.ts +13 -1
- package/dist/reporter.d.ts.map +1 -1
- package/dist/reporter.js +224 -141
- package/dist/schemas.d.ts +217 -22
- 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/report-watch.d.ts +9 -0
- package/dist/server/report-watch.d.ts.map +1 -0
- package/dist/server/routes.d.ts +7 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server.cjs +520 -82
- 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 +9 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-IEYNAB6M.js +0 -263
package/dist/server.cjs
CHANGED
|
@@ -35,7 +35,7 @@ __export(server_exports, {
|
|
|
35
35
|
module.exports = __toCommonJS(server_exports);
|
|
36
36
|
|
|
37
37
|
// src/server/app.ts
|
|
38
|
-
var
|
|
38
|
+
var import_path6 = require("path");
|
|
39
39
|
var import_url = require("url");
|
|
40
40
|
var import_p_limit = __toESM(require("p-limit"), 1);
|
|
41
41
|
|
|
@@ -47,6 +47,36 @@ var import_path = require("path");
|
|
|
47
47
|
function normalizeScreenshotsBaseUrl(baseUrl) {
|
|
48
48
|
return baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
49
49
|
}
|
|
50
|
+
function classifyImage(image) {
|
|
51
|
+
if (image.actual !== void 0 || image.diff !== void 0) {
|
|
52
|
+
return "comparison";
|
|
53
|
+
}
|
|
54
|
+
if (image.expect !== void 0) {
|
|
55
|
+
return "baseline-only";
|
|
56
|
+
}
|
|
57
|
+
return "declared-only";
|
|
58
|
+
}
|
|
59
|
+
function withImageSource(image) {
|
|
60
|
+
return {
|
|
61
|
+
...image,
|
|
62
|
+
source: classifyImage(image)
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function getDeclaredVisualNames(visualNames, visualDeclarations) {
|
|
66
|
+
return visualDeclarations?.map(({ visualName }) => visualName) ?? visualNames;
|
|
67
|
+
}
|
|
68
|
+
function copyVisualDeclarations(visualDeclarations) {
|
|
69
|
+
return visualDeclarations?.map((visualDeclaration) => ({ ...visualDeclaration }));
|
|
70
|
+
}
|
|
71
|
+
function mergeDeclaredImages(images, visualNames) {
|
|
72
|
+
const names = /* @__PURE__ */ new Set([...Object.keys(images), ...visualNames]);
|
|
73
|
+
return Object.fromEntries(
|
|
74
|
+
Array.from(names, (name) => {
|
|
75
|
+
const current = images[name] ?? {};
|
|
76
|
+
return [name, withImageSource(current)];
|
|
77
|
+
})
|
|
78
|
+
);
|
|
79
|
+
}
|
|
50
80
|
function attachmentsToImages(attachments, screenshotsBaseUrl = "/screenshots/") {
|
|
51
81
|
const images = {};
|
|
52
82
|
const baseUrl = normalizeScreenshotsBaseUrl(screenshotsBaseUrl);
|
|
@@ -57,7 +87,7 @@ function attachmentsToImages(attachments, screenshotsBaseUrl = "/screenshots/")
|
|
|
57
87
|
const baseName = match[1];
|
|
58
88
|
const role = match[2];
|
|
59
89
|
if (baseName === null || baseName === void 0 || role === null || role === void 0) continue;
|
|
60
|
-
images[baseName] ??= {
|
|
90
|
+
images[baseName] ??= {};
|
|
61
91
|
const url = `${baseUrl}${attachment.path}`;
|
|
62
92
|
const img = images[baseName];
|
|
63
93
|
if (img !== null && img !== void 0) {
|
|
@@ -68,10 +98,11 @@ function attachmentsToImages(attachments, screenshotsBaseUrl = "/screenshots/")
|
|
|
68
98
|
}
|
|
69
99
|
for (const key of Object.keys(images)) {
|
|
70
100
|
const img = images[key];
|
|
71
|
-
if (img?.actual !==
|
|
101
|
+
if (img?.actual !== void 0 && img?.expect !== void 0 && img?.diff === void 0) {
|
|
72
102
|
delete img.expect;
|
|
103
|
+
}
|
|
73
104
|
}
|
|
74
|
-
return images;
|
|
105
|
+
return mergeDeclaredImages(images, []);
|
|
75
106
|
}
|
|
76
107
|
function mapStatus(status) {
|
|
77
108
|
switch (status) {
|
|
@@ -87,17 +118,39 @@ function mapStatus(status) {
|
|
|
87
118
|
}
|
|
88
119
|
|
|
89
120
|
// src/report-state.ts
|
|
90
|
-
function
|
|
91
|
-
return
|
|
92
|
-
|
|
93
|
-
|
|
121
|
+
function isCurrentArtifact(image) {
|
|
122
|
+
return image?.actual !== void 0 || image?.expect !== void 0 || image?.diff !== void 0;
|
|
123
|
+
}
|
|
124
|
+
function isReusablePassingImage(image) {
|
|
125
|
+
const source = image.source ?? classifyImage(image);
|
|
126
|
+
if (source === "comparison") {
|
|
127
|
+
return image.actual !== void 0 && image.diff === void 0;
|
|
128
|
+
}
|
|
129
|
+
return source === "baseline-only";
|
|
130
|
+
}
|
|
131
|
+
function hasReusablePassingImages(images) {
|
|
132
|
+
return Object.values(images).some((img) => img !== null && img !== void 0 && isReusablePassingImage(img));
|
|
94
133
|
}
|
|
95
134
|
function preservePreviousPassingImages(test, status, images) {
|
|
96
|
-
if (status !== "passed"
|
|
135
|
+
if (status !== "passed") {
|
|
97
136
|
return images;
|
|
98
137
|
}
|
|
99
138
|
const previousImages = test.results?.[0]?.images ?? {};
|
|
100
|
-
|
|
139
|
+
if (!hasReusablePassingImages(previousImages)) {
|
|
140
|
+
return images;
|
|
141
|
+
}
|
|
142
|
+
return Object.entries(previousImages).reduce((currentImages, [name, previousImage]) => {
|
|
143
|
+
if (!Object.hasOwn(currentImages, name) || previousImage === void 0 || !isReusablePassingImage(previousImage) || isCurrentArtifact(currentImages[name])) {
|
|
144
|
+
return currentImages;
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
...currentImages,
|
|
148
|
+
[name]: {
|
|
149
|
+
...previousImage,
|
|
150
|
+
source: previousImage.source ?? classifyImage(previousImage)
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
}, images);
|
|
101
154
|
}
|
|
102
155
|
function countDiffImages(images) {
|
|
103
156
|
return Object.values(images).filter((img) => img?.diff !== null && img?.diff !== void 0).length;
|
|
@@ -133,10 +186,15 @@ function applyTestEndEvent(state, data, options = {}) {
|
|
|
133
186
|
return null;
|
|
134
187
|
}
|
|
135
188
|
test.status = mapStatus(data.status);
|
|
189
|
+
const resultStatus = data.status === "passed" ? "success" : data.status === "failed" ? "failed" : "pending";
|
|
190
|
+
const visualDeclarations = copyVisualDeclarations(data.visualDeclarations);
|
|
136
191
|
const images = preservePreviousPassingImages(
|
|
137
192
|
test,
|
|
138
193
|
data.status,
|
|
139
|
-
|
|
194
|
+
mergeDeclaredImages(
|
|
195
|
+
attachmentsToImages(data.attachments, options.screenshotsBaseUrl),
|
|
196
|
+
getDeclaredVisualNames(data.visualNames, visualDeclarations)
|
|
197
|
+
)
|
|
140
198
|
);
|
|
141
199
|
const diffCount = countDiffImages(images);
|
|
142
200
|
const hasDiffs = diffCount > 0;
|
|
@@ -145,9 +203,10 @@ function applyTestEndEvent(state, data, options = {}) {
|
|
|
145
203
|
}
|
|
146
204
|
test.results = [
|
|
147
205
|
{
|
|
148
|
-
status:
|
|
206
|
+
status: resultStatus,
|
|
149
207
|
retries: 0,
|
|
150
208
|
images,
|
|
209
|
+
visualDeclarations,
|
|
151
210
|
error: data.error,
|
|
152
211
|
duration: data.duration
|
|
153
212
|
}
|
|
@@ -177,23 +236,40 @@ var LocationSchema = import_zod.z.object({
|
|
|
177
236
|
file: import_zod.z.string(),
|
|
178
237
|
line: import_zod.z.number()
|
|
179
238
|
});
|
|
239
|
+
var VisualSourceSchema = import_zod.z.enum(["comparison", "baseline-only", "declared-only"]);
|
|
180
240
|
var ImagesSchema = import_zod.z.object({
|
|
181
|
-
actual: import_zod.z.string(),
|
|
241
|
+
actual: import_zod.z.string().optional(),
|
|
182
242
|
expect: import_zod.z.string().optional(),
|
|
183
243
|
diff: import_zod.z.string().optional(),
|
|
184
|
-
error: import_zod.z.string().optional()
|
|
244
|
+
error: import_zod.z.string().optional(),
|
|
245
|
+
source: VisualSourceSchema.optional()
|
|
185
246
|
});
|
|
247
|
+
var ScreenshotDeclarationSchema = import_zod.z.discriminatedUnion("kind", [
|
|
248
|
+
import_zod.z.object({
|
|
249
|
+
visualName: import_zod.z.string(),
|
|
250
|
+
kind: import_zod.z.literal("named"),
|
|
251
|
+
declaredName: import_zod.z.string(),
|
|
252
|
+
snapshotBaseName: import_zod.z.string(),
|
|
253
|
+
occurrenceIndex: import_zod.z.number()
|
|
254
|
+
}),
|
|
255
|
+
import_zod.z.object({
|
|
256
|
+
visualName: import_zod.z.string(),
|
|
257
|
+
kind: import_zod.z.literal("unnamed"),
|
|
258
|
+
occurrenceIndex: import_zod.z.number()
|
|
259
|
+
})
|
|
260
|
+
]);
|
|
186
261
|
var AttachmentSchema = import_zod.z.object({
|
|
187
262
|
name: import_zod.z.string(),
|
|
188
263
|
path: import_zod.z.string(),
|
|
189
264
|
contentType: import_zod.z.string()
|
|
190
265
|
});
|
|
191
266
|
var TestStatusSchema = import_zod.z.enum(["unknown", "pending", "running", "failed", "approved", "success", "retrying"]);
|
|
267
|
+
var TestResultStatusSchema = import_zod.z.enum(["failed", "success", "pending"]);
|
|
192
268
|
var TestResultSchema = import_zod.z.object({
|
|
193
|
-
status:
|
|
269
|
+
status: TestResultStatusSchema,
|
|
194
270
|
retries: import_zod.z.number(),
|
|
195
|
-
// eslint-disable-next-line typescript/no-unsafe-type-assertion
|
|
196
271
|
images: import_zod.z.record(import_zod.z.string(), ImagesSchema).optional(),
|
|
272
|
+
visualDeclarations: import_zod.z.array(ScreenshotDeclarationSchema).optional(),
|
|
197
273
|
error: import_zod.z.string().optional(),
|
|
198
274
|
duration: import_zod.z.number().optional()
|
|
199
275
|
});
|
|
@@ -221,7 +297,6 @@ var CrvyRprtrSuiteSchema = import_zod.z.lazy(
|
|
|
221
297
|
opened: import_zod.z.boolean(),
|
|
222
298
|
checked: import_zod.z.boolean(),
|
|
223
299
|
indeterminate: import_zod.z.boolean(),
|
|
224
|
-
// eslint-disable-next-line typescript/no-unsafe-type-assertion
|
|
225
300
|
children: import_zod.z.record(import_zod.z.string(), import_zod.z.union([CrvyRprtrSuiteSchema, CrvyRprtrTestSchema])).optional()
|
|
226
301
|
})
|
|
227
302
|
);
|
|
@@ -240,6 +315,11 @@ var TestEndDataSchema = import_zod.z.object({
|
|
|
240
315
|
id: import_zod.z.string(),
|
|
241
316
|
status: import_zod.z.enum(["passed", "failed", "skipped"]),
|
|
242
317
|
attachments: import_zod.z.array(AttachmentSchema),
|
|
318
|
+
visualNames: import_zod.z.array(import_zod.z.string()).default([]),
|
|
319
|
+
visualDeclarations: import_zod.z.preprocess(
|
|
320
|
+
(value) => value === null ? void 0 : value,
|
|
321
|
+
import_zod.z.array(ScreenshotDeclarationSchema).optional()
|
|
322
|
+
),
|
|
243
323
|
error: import_zod.z.string().optional(),
|
|
244
324
|
duration: import_zod.z.number().optional()
|
|
245
325
|
});
|
|
@@ -480,14 +560,281 @@ function handleSync(ctx) {
|
|
|
480
560
|
broadcastToBrowsers(ctx.wsClients, { type: "sync", data: ctx.reportData });
|
|
481
561
|
}
|
|
482
562
|
|
|
483
|
-
// src/server/
|
|
563
|
+
// src/server/report-watch.ts
|
|
564
|
+
var import_promises3 = require("fs/promises");
|
|
484
565
|
var import_path3 = require("path");
|
|
566
|
+
var OFFLINE_REPORT_FILE_PATTERN2 = /^crvy-rprtr(?:-\d+)?\.json$/;
|
|
567
|
+
function createDebouncedRefresh(reload, delayMs = 50) {
|
|
568
|
+
let timer = null;
|
|
569
|
+
return () => {
|
|
570
|
+
if (timer !== null) {
|
|
571
|
+
clearTimeout(timer);
|
|
572
|
+
}
|
|
573
|
+
timer = setTimeout(() => {
|
|
574
|
+
timer = null;
|
|
575
|
+
void reload();
|
|
576
|
+
}, delayMs);
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
function isFileNotFound2(error) {
|
|
580
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
581
|
+
}
|
|
582
|
+
async function describeFile(filePath, label) {
|
|
583
|
+
try {
|
|
584
|
+
const fileStats = await (0, import_promises3.stat)(filePath);
|
|
585
|
+
return `${label}:${fileStats.size}:${fileStats.mtimeMs}`;
|
|
586
|
+
} catch (error) {
|
|
587
|
+
if (isFileNotFound2(error)) {
|
|
588
|
+
return null;
|
|
589
|
+
}
|
|
590
|
+
throw error;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
async function createOfflineFingerprint(offlineReportDir) {
|
|
594
|
+
if (!await isDirectory(offlineReportDir)) {
|
|
595
|
+
return "offline:missing";
|
|
596
|
+
}
|
|
597
|
+
try {
|
|
598
|
+
const entries = await (0, import_promises3.readdir)(offlineReportDir, { withFileTypes: true });
|
|
599
|
+
const relevantEntries = entries.filter(
|
|
600
|
+
(entry) => entry.isFile() && (entry.name === "report.json" || OFFLINE_REPORT_FILE_PATTERN2.test(entry.name))
|
|
601
|
+
).sort((left, right) => left.name.localeCompare(right.name));
|
|
602
|
+
const parts = await Promise.all(
|
|
603
|
+
relevantEntries.map((entry) => describeFile((0, import_path3.join)(offlineReportDir, entry.name), entry.name))
|
|
604
|
+
);
|
|
605
|
+
return `offline:${parts.filter((part) => part !== null).join("|")}`;
|
|
606
|
+
} catch (error) {
|
|
607
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
608
|
+
console.warn(`[Server] Unable to scan ${offlineReportDir}: ${errorMsg}`);
|
|
609
|
+
return "offline:error";
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
async function createScreenshotFingerprint(screenshotDir, relativePath = "") {
|
|
613
|
+
const directoryPath = relativePath === "" ? screenshotDir : (0, import_path3.join)(screenshotDir, relativePath);
|
|
614
|
+
if (!await isDirectory(directoryPath)) {
|
|
615
|
+
return relativePath === "" ? ["screenshots:missing"] : [];
|
|
616
|
+
}
|
|
617
|
+
try {
|
|
618
|
+
const entries = await (0, import_promises3.readdir)(directoryPath, { withFileTypes: true });
|
|
619
|
+
const parts = await Promise.all(
|
|
620
|
+
entries.sort((left, right) => left.name.localeCompare(right.name)).map((entry) => {
|
|
621
|
+
const entryRelativePath = relativePath === "" ? entry.name : (0, import_path3.join)(relativePath, entry.name);
|
|
622
|
+
if (entry.isDirectory()) {
|
|
623
|
+
return createScreenshotFingerprint(screenshotDir, entryRelativePath);
|
|
624
|
+
}
|
|
625
|
+
return describeFile((0, import_path3.join)(screenshotDir, entryRelativePath), entryRelativePath);
|
|
626
|
+
})
|
|
627
|
+
);
|
|
628
|
+
return parts.flat().filter((part) => part !== null);
|
|
629
|
+
} catch (error) {
|
|
630
|
+
if (isFileNotFound2(error)) {
|
|
631
|
+
return relativePath === "" ? ["screenshots:missing"] : [];
|
|
632
|
+
}
|
|
633
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
634
|
+
console.warn(`[Server] Unable to scan ${directoryPath}: ${errorMsg}`);
|
|
635
|
+
return relativePath === "" ? ["screenshots:error"] : [];
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
async function createArtifactsFingerprint(options) {
|
|
639
|
+
const [offlineFingerprint, screenshotFingerprint] = await Promise.all([
|
|
640
|
+
createOfflineFingerprint(options.offlineReportDir),
|
|
641
|
+
createScreenshotFingerprint(options.screenshotDir)
|
|
642
|
+
]);
|
|
643
|
+
return `${offlineFingerprint}::${screenshotFingerprint.join("|")}`;
|
|
644
|
+
}
|
|
645
|
+
async function watchReportArtifacts(options) {
|
|
646
|
+
let fingerprint = await createArtifactsFingerprint(options);
|
|
647
|
+
let isPolling = false;
|
|
648
|
+
const interval = setInterval(() => {
|
|
649
|
+
if (isPolling) {
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
isPolling = true;
|
|
653
|
+
void createArtifactsFingerprint(options).then((nextFingerprint) => {
|
|
654
|
+
if (nextFingerprint === fingerprint) {
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
fingerprint = nextFingerprint;
|
|
658
|
+
options.scheduleRefresh();
|
|
659
|
+
}).catch((error) => {
|
|
660
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
661
|
+
console.warn(`[Server] Artifact refresh poll failed: ${errorMsg}`);
|
|
662
|
+
}).finally(() => {
|
|
663
|
+
isPolling = false;
|
|
664
|
+
});
|
|
665
|
+
}, 100);
|
|
666
|
+
return () => {
|
|
667
|
+
clearInterval(interval);
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// src/server/routes.ts
|
|
672
|
+
var import_fs = require("fs");
|
|
673
|
+
var import_path5 = require("path");
|
|
674
|
+
|
|
675
|
+
// src/snapshot-path-resolver.ts
|
|
676
|
+
var import_crypto = require("crypto");
|
|
677
|
+
var import_path4 = require("path");
|
|
678
|
+
var DEFAULT_SCREENSHOT_TEMPLATE = "{snapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}{-projectName}{-snapshotSuffix}{ext}";
|
|
679
|
+
var WINDOWS_FILESYSTEM_FRIENDLY_LENGTH = 60;
|
|
680
|
+
function isUnsafeFilePathCharacter(character) {
|
|
681
|
+
const codePoint = character.codePointAt(0);
|
|
682
|
+
return codePoint !== void 0 && (codePoint <= 44 || codePoint >= 46 && codePoint <= 47 || codePoint >= 58 && codePoint <= 64 || codePoint >= 91 && codePoint <= 96 || codePoint >= 123 && codePoint <= 127);
|
|
683
|
+
}
|
|
684
|
+
function sanitizeForFilePath(value) {
|
|
685
|
+
return Array.from(value).reduce(
|
|
686
|
+
(state, character) => {
|
|
687
|
+
const unsafeCharacter = isUnsafeFilePathCharacter(character);
|
|
688
|
+
return unsafeCharacter ? state.previousCharacterWasUnsafe ? state : {
|
|
689
|
+
value: `${state.value}-`,
|
|
690
|
+
previousCharacterWasUnsafe: true
|
|
691
|
+
} : {
|
|
692
|
+
value: `${state.value}${character}`,
|
|
693
|
+
previousCharacterWasUnsafe: false
|
|
694
|
+
};
|
|
695
|
+
},
|
|
696
|
+
{
|
|
697
|
+
value: "",
|
|
698
|
+
previousCharacterWasUnsafe: false
|
|
699
|
+
}
|
|
700
|
+
).value;
|
|
701
|
+
}
|
|
702
|
+
function trimLongString(value, length = WINDOWS_FILESYSTEM_FRIENDLY_LENGTH) {
|
|
703
|
+
if (value.length <= length) {
|
|
704
|
+
return value;
|
|
705
|
+
}
|
|
706
|
+
const hash = (0, import_crypto.createHash)("sha1").update(value).digest("hex");
|
|
707
|
+
const middle = `-${hash.slice(0, 5)}-`;
|
|
708
|
+
const start = Math.floor((length - middle.length) / 2);
|
|
709
|
+
const end = length - middle.length - start;
|
|
710
|
+
return value.slice(0, start) + middle + value.slice(-end);
|
|
711
|
+
}
|
|
712
|
+
function sanitizeFilePathBeforeExtension(filePath, extension = (0, import_path4.extname)(filePath)) {
|
|
713
|
+
const base = filePath.slice(0, filePath.length - extension.length);
|
|
714
|
+
return sanitizeForFilePath(base) + extension;
|
|
715
|
+
}
|
|
716
|
+
function addSuffixToFilePath(filePath, suffix) {
|
|
717
|
+
const extension = (0, import_path4.extname)(filePath);
|
|
718
|
+
return filePath.slice(0, filePath.length - extension.length) + suffix + extension;
|
|
719
|
+
}
|
|
720
|
+
function normalizedSnapshotDir(config) {
|
|
721
|
+
return (0, import_path4.resolve)(config.configDir, config.snapshotDir);
|
|
722
|
+
}
|
|
723
|
+
function templateValue(template, token, value) {
|
|
724
|
+
return template.replace(
|
|
725
|
+
new RegExp(`\\{(.)?${token}\\}`, "g"),
|
|
726
|
+
(_, prefix) => value === "" ? "" : `${prefix ?? ""}${value}`
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
function applyTemplate(input, nameArgument, extension) {
|
|
730
|
+
const template = input.config.toHaveScreenshotPathTemplate ?? input.config.snapshotPathTemplate ?? DEFAULT_SCREENSHOT_TEMPLATE;
|
|
731
|
+
const relativeTestFilePath = (0, import_path4.relative)(input.config.testDir, input.testFile);
|
|
732
|
+
const parsed = (0, import_path4.parse)(relativeTestFilePath);
|
|
733
|
+
const tokens = [
|
|
734
|
+
["testDir", input.config.testDir],
|
|
735
|
+
["snapshotDir", normalizedSnapshotDir(input.config)],
|
|
736
|
+
["snapshotSuffix", input.config.snapshotSuffix],
|
|
737
|
+
["testFileDir", parsed.dir],
|
|
738
|
+
["platform", process.platform],
|
|
739
|
+
["projectName", sanitizeForFilePath(input.config.projectName)],
|
|
740
|
+
["testName", ""],
|
|
741
|
+
["testFileName", parsed.base],
|
|
742
|
+
["testFilePath", relativeTestFilePath],
|
|
743
|
+
["arg", nameArgument],
|
|
744
|
+
["ext", extension]
|
|
745
|
+
];
|
|
746
|
+
const snapshotPath = tokens.reduce(
|
|
747
|
+
(currentTemplate, [token, value]) => templateValue(currentTemplate, token, value),
|
|
748
|
+
template
|
|
749
|
+
);
|
|
750
|
+
return (0, import_path4.resolve)(input.config.configDir, snapshotPath);
|
|
751
|
+
}
|
|
752
|
+
function removeExtension(filePath, extension = (0, import_path4.extname)(filePath)) {
|
|
753
|
+
return filePath.slice(0, filePath.length - extension.length);
|
|
754
|
+
}
|
|
755
|
+
function snapshotNameParts(declaredName) {
|
|
756
|
+
const extension = (0, import_path4.extname)(declaredName) || ".png";
|
|
757
|
+
return {
|
|
758
|
+
extension,
|
|
759
|
+
filePath: (0, import_path4.extname)(declaredName) === "" ? `${declaredName}${extension}` : declaredName
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
function filePathForOccurrence(filePath, occurrenceIndex) {
|
|
763
|
+
return occurrenceIndex === 1 ? filePath : addSuffixToFilePath(filePath, `-${occurrenceIndex - 1}`);
|
|
764
|
+
}
|
|
765
|
+
function createResolvedBaselineTarget(input, declaration, nameArgument, extension) {
|
|
766
|
+
return {
|
|
767
|
+
visualName: declaration.visualName,
|
|
768
|
+
attachmentBaseName: declaration.visualName,
|
|
769
|
+
artifactBaseName: sanitizeForFilePath(declaration.visualName),
|
|
770
|
+
snapshotPath: applyTemplate(input, nameArgument, extension)
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
function resolveStringCallTarget(input, declaration) {
|
|
774
|
+
const { extension, filePath } = snapshotNameParts(declaration.declaredName);
|
|
775
|
+
const occurrenceFilePath = filePathForOccurrence(filePath, declaration.occurrenceIndex);
|
|
776
|
+
const sanitizedNameWithExtension = sanitizeFilePathBeforeExtension(occurrenceFilePath, extension);
|
|
777
|
+
const nameArgument = removeExtension(sanitizedNameWithExtension, extension);
|
|
778
|
+
return createResolvedBaselineTarget(input, declaration, nameArgument, extension);
|
|
779
|
+
}
|
|
780
|
+
function resolveArrayCallTarget(input, declaration) {
|
|
781
|
+
const { extension, filePath } = snapshotNameParts(declaration.declaredName);
|
|
782
|
+
const occurrenceFilePath = filePathForOccurrence(filePath, declaration.occurrenceIndex);
|
|
783
|
+
const nameArgument = (0, import_path4.join)((0, import_path4.dirname)(occurrenceFilePath), (0, import_path4.basename)(occurrenceFilePath, extension));
|
|
784
|
+
return createResolvedBaselineTarget(input, declaration, nameArgument, extension);
|
|
785
|
+
}
|
|
786
|
+
function resolveNamedTarget(input, declaration) {
|
|
787
|
+
if (!declaration.declaredName.includes("/")) {
|
|
788
|
+
return resolveStringCallTarget(input, declaration);
|
|
789
|
+
}
|
|
790
|
+
const stringCallTarget = resolveStringCallTarget(input, declaration);
|
|
791
|
+
const arrayCallTarget = resolveArrayCallTarget(input, declaration);
|
|
792
|
+
if (stringCallTarget.snapshotPath === arrayCallTarget.snapshotPath) {
|
|
793
|
+
return stringCallTarget;
|
|
794
|
+
}
|
|
795
|
+
if (input.snapshotPathExists === void 0) {
|
|
796
|
+
return void 0;
|
|
797
|
+
}
|
|
798
|
+
const stringCallTargetExists = input.snapshotPathExists(stringCallTarget.snapshotPath);
|
|
799
|
+
const arrayCallTargetExists = input.snapshotPathExists(arrayCallTarget.snapshotPath);
|
|
800
|
+
if (stringCallTargetExists === arrayCallTargetExists) {
|
|
801
|
+
return void 0;
|
|
802
|
+
}
|
|
803
|
+
return stringCallTargetExists ? stringCallTarget : arrayCallTarget;
|
|
804
|
+
}
|
|
805
|
+
function reporterTitlesWithoutProjectAndFile(reporterTitlePath2) {
|
|
806
|
+
return reporterTitlePath2.slice(3).filter((part) => part !== "");
|
|
807
|
+
}
|
|
808
|
+
function anonymousName(reporterTitlePath2, occurrenceIndex) {
|
|
809
|
+
const rawAnonymousName = `${reporterTitlesWithoutProjectAndFile(reporterTitlePath2).join(" ")} ${occurrenceIndex}.png`;
|
|
810
|
+
return sanitizeFilePathBeforeExtension(trimLongString(rawAnonymousName), ".png");
|
|
811
|
+
}
|
|
812
|
+
function resolveTarget(input, declaration) {
|
|
813
|
+
switch (declaration.kind) {
|
|
814
|
+
case "named":
|
|
815
|
+
return typeof declaration.declaredName === "string" && declaration.declaredName !== "" ? resolveNamedTarget(input, declaration) : void 0;
|
|
816
|
+
case "unnamed": {
|
|
817
|
+
const anonymousFileName = anonymousName(input.reporterTitlePath, declaration.occurrenceIndex);
|
|
818
|
+
const extension = ".png";
|
|
819
|
+
const nameArgument = (0, import_path4.join)((0, import_path4.dirname)(anonymousFileName), (0, import_path4.basename)(anonymousFileName, extension));
|
|
820
|
+
return createResolvedBaselineTarget(input, declaration, nameArgument, extension);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
function resolveBaselineTargets(input) {
|
|
825
|
+
return input.declarations.flatMap((declaration) => {
|
|
826
|
+
const resolvedTarget = resolveTarget(input, declaration);
|
|
827
|
+
return resolvedTarget === void 0 ? [] : [resolvedTarget];
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// src/server/routes.ts
|
|
485
832
|
var LIVE_UPDATES_WEBSOCKET_PATH = "/";
|
|
486
833
|
function isWebSocketUpgradeRequest(req) {
|
|
487
834
|
return new URL(req.url).pathname === LIVE_UPDATES_WEBSOCKET_PATH && req.headers.get("upgrade")?.toLowerCase() === "websocket";
|
|
488
835
|
}
|
|
489
836
|
async function handleRoot(ctx) {
|
|
490
|
-
const html = await respondWithFile((0,
|
|
837
|
+
const html = await respondWithFile((0, import_path5.join)(ctx.staticDir, "index.html"), "text/html");
|
|
491
838
|
return html ?? new Response("Not Found", { status: 404 });
|
|
492
839
|
}
|
|
493
840
|
async function handleAppCss() {
|
|
@@ -504,6 +851,37 @@ async function handleSrcFiles(req) {
|
|
|
504
851
|
function handleApiReport(ctx) {
|
|
505
852
|
return Response.json(ctx.reportData);
|
|
506
853
|
}
|
|
854
|
+
var APPROVAL_TARGET_ERROR = "Could not resolve approval target";
|
|
855
|
+
function actualPathFromUrl(ctx, actualUrl) {
|
|
856
|
+
return actualUrl.startsWith("/screenshots/") ? (0, import_path5.join)(ctx.reportData.screenshotDir, actualUrl.slice("/screenshots/".length)) : actualUrl;
|
|
857
|
+
}
|
|
858
|
+
function reporterTitlePath(test) {
|
|
859
|
+
const testFile = test.location?.file;
|
|
860
|
+
return ["", test.browser, testFile ?? "", ...test.titlePath, test.title];
|
|
861
|
+
}
|
|
862
|
+
function resolveApprovalTarget(ctx, test, retry, imageName) {
|
|
863
|
+
const testFile = test.location?.file;
|
|
864
|
+
const declaration = test.results?.[retry]?.visualDeclarations?.find((candidate) => candidate.visualName === imageName);
|
|
865
|
+
if (ctx.approvalRouting === void 0 || testFile === void 0 || declaration === void 0) {
|
|
866
|
+
return null;
|
|
867
|
+
}
|
|
868
|
+
const targets = resolveBaselineTargets({
|
|
869
|
+
testFile,
|
|
870
|
+
reporterTitlePath: reporterTitlePath(test),
|
|
871
|
+
declarations: [declaration],
|
|
872
|
+
config: {
|
|
873
|
+
configDir: ctx.approvalRouting.configDir,
|
|
874
|
+
testDir: ctx.approvalRouting.playwrightTestDir ?? (0, import_path5.dirname)(testFile),
|
|
875
|
+
snapshotDir: ctx.approvalRouting.playwrightSnapshotDir ?? (0, import_path5.dirname)(testFile),
|
|
876
|
+
projectName: test.browser,
|
|
877
|
+
snapshotSuffix: process.platform,
|
|
878
|
+
snapshotPathTemplate: ctx.approvalRouting.playwrightSnapshotPathTemplate,
|
|
879
|
+
toHaveScreenshotPathTemplate: ctx.approvalRouting.playwrightToHaveScreenshotPathTemplate
|
|
880
|
+
},
|
|
881
|
+
snapshotPathExists: import_fs.existsSync
|
|
882
|
+
});
|
|
883
|
+
return targets.length === 1 ? targets[0]?.snapshotPath ?? null : null;
|
|
884
|
+
}
|
|
507
885
|
async function handleApiApprove(ctx, req) {
|
|
508
886
|
try {
|
|
509
887
|
const rawBody = await req.json();
|
|
@@ -514,61 +892,96 @@ async function handleApiApprove(ctx, req) {
|
|
|
514
892
|
}
|
|
515
893
|
const { id, retry, image } = parsed;
|
|
516
894
|
const test = ctx.reportData.tests[id];
|
|
517
|
-
if (test
|
|
518
|
-
|
|
519
|
-
|
|
895
|
+
if (test === void 0) {
|
|
896
|
+
return Response.json({ success: false, error: "Test not found" }, { status: 404 });
|
|
897
|
+
}
|
|
898
|
+
const actualUrl = test.results?.[retry]?.images?.[image]?.actual;
|
|
899
|
+
if (actualUrl === void 0) {
|
|
900
|
+
return Response.json({ success: false, error: "Actual image not found" }, { status: 409 });
|
|
901
|
+
}
|
|
902
|
+
const snapshotPath = resolveApprovalTarget(ctx, test, retry, image);
|
|
903
|
+
if (snapshotPath === null) {
|
|
904
|
+
return Response.json({ success: false, error: APPROVAL_TARGET_ERROR }, { status: 409 });
|
|
905
|
+
}
|
|
906
|
+
try {
|
|
907
|
+
await copyFilePortable(actualPathFromUrl(ctx, actualUrl), snapshotPath);
|
|
908
|
+
test.approved = { ...test.approved ?? {}, [image]: retry };
|
|
520
909
|
await ctx.saveReport();
|
|
521
|
-
|
|
522
|
-
if (actualUrl !== null && actualUrl !== void 0 && test.location?.file !== null && test.location?.file !== void 0) {
|
|
523
|
-
const actualPath = actualUrl.replace("/screenshots/", `${ctx.reportData.screenshotDir}/`);
|
|
524
|
-
const snapshotPath = `${test.location.file}-snapshots/${image}-${test.browser}-${process.platform}.png`;
|
|
525
|
-
try {
|
|
526
|
-
await copyFilePortable(actualPath, snapshotPath);
|
|
527
|
-
console.log(` \u2714 Updated baseline: ${snapshotPath}`);
|
|
528
|
-
} catch (err) {
|
|
529
|
-
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
530
|
-
console.error(` \u2717 Failed to update baseline: ${errorMsg}`);
|
|
531
|
-
}
|
|
532
|
-
}
|
|
910
|
+
console.log(` \u2714 Updated baseline: ${snapshotPath}`);
|
|
533
911
|
console.log(` \u2714 Approved [${test.browser}] ${test.title} \u2014 ${image}`);
|
|
912
|
+
return Response.json({ success: true });
|
|
913
|
+
} catch (err) {
|
|
914
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
915
|
+
console.error(` \u2717 Failed to update baseline: ${errorMsg}`);
|
|
916
|
+
return Response.json({ success: false, error: "Failed to update baseline" }, { status: 500 });
|
|
534
917
|
}
|
|
535
|
-
return Response.json({ success: true });
|
|
536
918
|
} catch {
|
|
537
919
|
return Response.json({ success: false, error: "Invalid request" }, { status: 400 });
|
|
538
920
|
}
|
|
539
921
|
}
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
const approved = {};
|
|
922
|
+
function createBulkApprovalUpdates(ctx) {
|
|
923
|
+
return Object.values(ctx.reportData.tests).flatMap((test) => {
|
|
924
|
+
if (!test.results || test.results.length === 0) {
|
|
925
|
+
return [];
|
|
926
|
+
}
|
|
546
927
|
const lastRetry = test.results.length - 1;
|
|
547
928
|
const lastResult = test.results[lastRetry];
|
|
548
|
-
if (!lastResult?.images)
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
929
|
+
if (!lastResult?.images) {
|
|
930
|
+
return [];
|
|
931
|
+
}
|
|
932
|
+
return Object.keys(lastResult.images).flatMap((imageName) => {
|
|
552
933
|
const actualUrl = lastResult.images?.[imageName]?.actual;
|
|
553
|
-
if (actualUrl
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
}).catch((err) => {
|
|
560
|
-
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
561
|
-
console.error(` \u2717 Failed to update baseline: ${errorMsg}`);
|
|
562
|
-
})
|
|
563
|
-
);
|
|
934
|
+
if (actualUrl === void 0) {
|
|
935
|
+
return [Promise.resolve({ kind: "unresolved" })];
|
|
936
|
+
}
|
|
937
|
+
const snapshotPath = resolveApprovalTarget(ctx, test, lastRetry, imageName);
|
|
938
|
+
if (snapshotPath === null) {
|
|
939
|
+
return [Promise.resolve({ kind: "unresolved" })];
|
|
564
940
|
}
|
|
941
|
+
return [
|
|
942
|
+
copyFilePortable(actualPathFromUrl(ctx, actualUrl), snapshotPath).then(
|
|
943
|
+
() => ({
|
|
944
|
+
kind: "approved",
|
|
945
|
+
imageName,
|
|
946
|
+
retry: lastRetry,
|
|
947
|
+
snapshotPath,
|
|
948
|
+
test
|
|
949
|
+
})
|
|
950
|
+
).catch((err) => {
|
|
951
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
952
|
+
console.error(` \u2717 Failed to update baseline: ${errorMsg}`);
|
|
953
|
+
return { kind: "failed" };
|
|
954
|
+
})
|
|
955
|
+
];
|
|
565
956
|
});
|
|
566
|
-
test.approved = approved;
|
|
567
957
|
});
|
|
958
|
+
}
|
|
959
|
+
function summarizeBulkApprovalOutcomes(outcomes) {
|
|
960
|
+
return outcomes.reduce(
|
|
961
|
+
(summary, outcome) => {
|
|
962
|
+
switch (outcome.kind) {
|
|
963
|
+
case "approved": {
|
|
964
|
+
outcome.test.approved = { ...outcome.test.approved ?? {}, [outcome.imageName]: outcome.retry };
|
|
965
|
+
console.log(` \u2714 Updated baseline: ${outcome.snapshotPath}`);
|
|
966
|
+
return { ...summary, approved: summary.approved + 1 };
|
|
967
|
+
}
|
|
968
|
+
case "unresolved":
|
|
969
|
+
return { ...summary, unresolved: summary.unresolved + 1 };
|
|
970
|
+
case "failed":
|
|
971
|
+
return { ...summary, failed: summary.failed + 1 };
|
|
972
|
+
}
|
|
973
|
+
},
|
|
974
|
+
{ approved: 0, unresolved: 0, failed: 0 }
|
|
975
|
+
);
|
|
976
|
+
}
|
|
977
|
+
async function handleApiApproveAll(ctx) {
|
|
978
|
+
const outcomes = await Promise.all(createBulkApprovalUpdates(ctx));
|
|
979
|
+
const counts = summarizeBulkApprovalOutcomes(outcomes);
|
|
568
980
|
await ctx.saveReport();
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
981
|
+
console.log(
|
|
982
|
+
` \u2714 Approved all \u2014 approved: ${counts.approved}, unresolved: ${counts.unresolved}, failed: ${counts.failed}`
|
|
983
|
+
);
|
|
984
|
+
return Response.json({ success: counts.failed === 0, ...counts });
|
|
572
985
|
}
|
|
573
986
|
async function handleApiImages(req) {
|
|
574
987
|
const path = new URL(req.url).pathname.slice("/api/images/".length);
|
|
@@ -584,7 +997,7 @@ async function handleScreenshots(ctx, req) {
|
|
|
584
997
|
}
|
|
585
998
|
async function handleDist(ctx, req) {
|
|
586
999
|
const path = new URL(req.url).pathname.slice("/dist/".length);
|
|
587
|
-
const filePath = (0,
|
|
1000
|
+
const filePath = (0, import_path5.join)(ctx.staticDir, path);
|
|
588
1001
|
const contentType = filePath.endsWith(".css") ? "text/css" : filePath.endsWith(".js") ? "application/javascript" : filePath.endsWith(".svelte") ? "text/plain" : "application/octet-stream";
|
|
589
1002
|
const file = await respondWithFile(filePath, contentType);
|
|
590
1003
|
return file ?? new Response("Not Found", { status: 404 });
|
|
@@ -603,10 +1016,10 @@ function handleHttpRequest(ctx, req) {
|
|
|
603
1016
|
if (pathname === "/api/report") {
|
|
604
1017
|
return Promise.resolve(handleApiReport(ctx));
|
|
605
1018
|
}
|
|
606
|
-
if (pathname === "/api/approve") {
|
|
1019
|
+
if (pathname === "/api/approve" && req.method === "POST") {
|
|
607
1020
|
return handleApiApprove(ctx, req);
|
|
608
1021
|
}
|
|
609
|
-
if (pathname === "/api/approve-all") {
|
|
1022
|
+
if (pathname === "/api/approve-all" && req.method === "POST") {
|
|
610
1023
|
return handleApiApproveAll(ctx);
|
|
611
1024
|
}
|
|
612
1025
|
if (pathname.startsWith("/api/images/")) {
|
|
@@ -688,6 +1101,10 @@ async function loadOfflineReports(offlineReportDir, reportData) {
|
|
|
688
1101
|
screenshotsBaseUrl: "/screenshots/"
|
|
689
1102
|
});
|
|
690
1103
|
}
|
|
1104
|
+
function resetReloadableReportData(reportData) {
|
|
1105
|
+
reportData.tests = {};
|
|
1106
|
+
reportData.isUpdateMode = false;
|
|
1107
|
+
}
|
|
691
1108
|
async function handleParsedWebSocketMessage(ctx, msg) {
|
|
692
1109
|
switch (msg.type) {
|
|
693
1110
|
case "test-begin": {
|
|
@@ -736,19 +1153,19 @@ function createWebSocketMessageHandler(getHandlerContext) {
|
|
|
736
1153
|
};
|
|
737
1154
|
}
|
|
738
1155
|
async function resolveStaticDir(staticDir) {
|
|
739
|
-
const currentDir = (0,
|
|
1156
|
+
const currentDir = (0, import_path6.dirname)((0, import_url.fileURLToPath)(import_meta.url));
|
|
740
1157
|
const candidates = staticDir === void 0 ? [
|
|
741
1158
|
currentDir,
|
|
742
|
-
(0,
|
|
743
|
-
(0,
|
|
744
|
-
(0,
|
|
745
|
-
(0,
|
|
746
|
-
(0,
|
|
747
|
-
] : [staticDir, (0,
|
|
1159
|
+
(0, import_path6.join)(currentDir, "dist"),
|
|
1160
|
+
(0, import_path6.join)(currentDir, "..", "dist"),
|
|
1161
|
+
(0, import_path6.join)(currentDir, "..", "..", "dist"),
|
|
1162
|
+
(0, import_path6.join)(currentDir, ".."),
|
|
1163
|
+
(0, import_path6.join)(currentDir, "..", "..")
|
|
1164
|
+
] : [staticDir, (0, import_path6.join)(staticDir, "dist")];
|
|
748
1165
|
const resolvedCandidates = await Promise.all(
|
|
749
1166
|
candidates.map(async (candidate) => ({
|
|
750
1167
|
candidate,
|
|
751
|
-
exists: await fileExists((0,
|
|
1168
|
+
exists: await fileExists((0, import_path6.join)(candidate, "index.html"))
|
|
752
1169
|
}))
|
|
753
1170
|
);
|
|
754
1171
|
const resolved = resolvedCandidates.find(({ exists }) => exists);
|
|
@@ -759,9 +1176,23 @@ async function resolveStaticDir(staticDir) {
|
|
|
759
1176
|
}
|
|
760
1177
|
async function resolveReportPath(reportPath) {
|
|
761
1178
|
if (await isDirectory(reportPath)) {
|
|
762
|
-
return { reportFile: (0,
|
|
1179
|
+
return { reportFile: (0, import_path6.join)(reportPath, "report.json"), offlineReportDir: reportPath };
|
|
763
1180
|
}
|
|
764
|
-
return { reportFile: reportPath, offlineReportDir: (0,
|
|
1181
|
+
return { reportFile: reportPath, offlineReportDir: (0, import_path6.dirname)(reportPath) };
|
|
1182
|
+
}
|
|
1183
|
+
function createRoutesContext(reportData, staticDir, saveReport, options) {
|
|
1184
|
+
return {
|
|
1185
|
+
reportData,
|
|
1186
|
+
staticDir,
|
|
1187
|
+
saveReport,
|
|
1188
|
+
approvalRouting: {
|
|
1189
|
+
configDir: options.configDir ?? process.cwd(),
|
|
1190
|
+
playwrightTestDir: options.playwrightTestDir,
|
|
1191
|
+
playwrightSnapshotDir: options.playwrightSnapshotDir,
|
|
1192
|
+
playwrightSnapshotPathTemplate: options.playwrightSnapshotPathTemplate,
|
|
1193
|
+
playwrightToHaveScreenshotPathTemplate: options.playwrightToHaveScreenshotPathTemplate
|
|
1194
|
+
}
|
|
1195
|
+
};
|
|
765
1196
|
}
|
|
766
1197
|
async function createServerApp(options = {}) {
|
|
767
1198
|
const port = options.port ?? 3e3;
|
|
@@ -774,19 +1205,26 @@ async function createServerApp(options = {}) {
|
|
|
774
1205
|
async function saveReport() {
|
|
775
1206
|
await writeJsonFile(reportFile, reportData);
|
|
776
1207
|
}
|
|
777
|
-
const routesContext =
|
|
778
|
-
reportData,
|
|
779
|
-
staticDir,
|
|
780
|
-
saveReport
|
|
781
|
-
};
|
|
1208
|
+
const routesContext = createRoutesContext(reportData, staticDir, saveReport, options);
|
|
782
1209
|
const getHandlerContext = () => createHandlerContext(reportData, wsClients, currentRunIds, saveReport);
|
|
783
1210
|
const handleRequest = (req) => handleHttpRequest(routesContext, req);
|
|
784
1211
|
const handleWebSocketMessage = createWebSocketMessageHandler(getHandlerContext);
|
|
785
|
-
|
|
786
|
-
|
|
1212
|
+
const reloadFromDisk = async () => {
|
|
1213
|
+
resetReloadableReportData(reportData);
|
|
1214
|
+
await loadReport(reportFile, reportData);
|
|
1215
|
+
await loadOfflineReports(offlineReportDir, reportData);
|
|
1216
|
+
broadcastToBrowsers(wsClients, { type: "sync", data: reportData });
|
|
1217
|
+
};
|
|
1218
|
+
await reloadFromDisk();
|
|
1219
|
+
const close = await watchReportArtifacts({
|
|
1220
|
+
offlineReportDir,
|
|
1221
|
+
screenshotDir: reportData.screenshotDir,
|
|
1222
|
+
scheduleRefresh: createDebouncedRefresh(reloadFromDisk)
|
|
1223
|
+
});
|
|
787
1224
|
return {
|
|
788
1225
|
port,
|
|
789
1226
|
wsClients,
|
|
1227
|
+
close,
|
|
790
1228
|
handleRequest,
|
|
791
1229
|
handleWebSocketMessage
|
|
792
1230
|
};
|
|
@@ -967,7 +1405,7 @@ function attachWebSocketServer(server, app) {
|
|
|
967
1405
|
});
|
|
968
1406
|
}
|
|
969
1407
|
async function listen(server, port) {
|
|
970
|
-
await new Promise((
|
|
1408
|
+
await new Promise((resolve2, reject) => {
|
|
971
1409
|
const onError = (error) => {
|
|
972
1410
|
server.off("error", onError);
|
|
973
1411
|
reject(error);
|
|
@@ -975,7 +1413,7 @@ async function listen(server, port) {
|
|
|
975
1413
|
server.on("error", onError);
|
|
976
1414
|
server.listen(port, () => {
|
|
977
1415
|
server.off("error", onError);
|
|
978
|
-
|
|
1416
|
+
resolve2();
|
|
979
1417
|
});
|
|
980
1418
|
});
|
|
981
1419
|
}
|