@crvy/rprtr 0.1.3 → 0.2.2
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 +84 -0
- package/README.md +15 -6
- package/dist/{chunk-X2TCDD54.js → chunk-HAFWYUNO.js} +204 -137
- package/dist/{chunk-F7NSIHCV.js → chunk-RFZGJSL3.js} +448 -128
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +6 -4
- package/dist/index.css +29 -0
- package/dist/index.js +297 -76
- package/dist/offline-reports.d.ts +4 -0
- package/dist/offline-reports.d.ts.map +1 -1
- package/dist/path-utils.d.ts +3 -0
- package/dist/path-utils.d.ts.map +1 -0
- package/dist/report-state.d.ts +3 -1
- package/dist/report-state.d.ts.map +1 -1
- package/dist/report-utils.d.ts.map +1 -1
- package/dist/reporter-utils.d.ts +0 -1
- package/dist/reporter-utils.d.ts.map +1 -1
- package/dist/reporter.cjs +232 -170
- package/dist/reporter.d.ts +1 -1
- package/dist/reporter.d.ts.map +1 -1
- package/dist/reporter.js +8 -6
- package/dist/schemas/http.d.ts +44 -0
- package/dist/schemas/http.d.ts.map +1 -0
- package/dist/schemas.d.ts +25 -6
- 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.map +1 -1
- package/dist/server/handlers.d.ts +3 -0
- package/dist/server/handlers.d.ts.map +1 -1
- package/dist/server/playwright-config.d.ts +2 -0
- package/dist/server/playwright-config.d.ts.map +1 -0
- package/dist/server/routes-context.d.ts +13 -0
- package/dist/server/routes-context.d.ts.map +1 -0
- package/dist/server/routes.d.ts +6 -1
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/run-controller.d.ts +85 -0
- package/dist/server/run-controller.d.ts.map +1 -0
- package/dist/server/run-routes.d.ts +3 -0
- package/dist/server/run-routes.d.ts.map +1 -0
- package/dist/server.cjs +668 -286
- package/dist/server.js +2 -2
- package/dist/types.d.ts +6 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +6 -5
package/dist/server.cjs
CHANGED
|
@@ -37,14 +37,32 @@ module.exports = __toCommonJS(server_exports);
|
|
|
37
37
|
// src/server/app.ts
|
|
38
38
|
var import_path8 = require("path");
|
|
39
39
|
var import_url = require("url");
|
|
40
|
-
var import_p_limit = __toESM(require("p-limit"), 1);
|
|
41
40
|
|
|
42
41
|
// src/offline-reports.ts
|
|
43
|
-
var
|
|
42
|
+
var import_promises2 = require("fs/promises");
|
|
44
43
|
var import_path2 = require("path");
|
|
44
|
+
var import_p_limit = __toESM(require("p-limit"), 1);
|
|
45
|
+
|
|
46
|
+
// src/path-utils.ts
|
|
47
|
+
var posixAbsolutePattern = /^\//u;
|
|
48
|
+
var windowsDrivePattern = /^[A-Za-z]:[\\/]/u;
|
|
49
|
+
var windowsUncPattern = /^[\\/][\\/]/u;
|
|
50
|
+
function isPosixAbsolutePath(p) {
|
|
51
|
+
return posixAbsolutePattern.test(p);
|
|
52
|
+
}
|
|
53
|
+
function isWindowsAbsolutePath(p) {
|
|
54
|
+
return windowsDrivePattern.test(p) || windowsUncPattern.test(p);
|
|
55
|
+
}
|
|
56
|
+
function isAnyAbsolutePath(p) {
|
|
57
|
+
return isPosixAbsolutePath(p) || isWindowsAbsolutePath(p);
|
|
58
|
+
}
|
|
59
|
+
function isForeignAbsolutePath(p, hostPlatform) {
|
|
60
|
+
if (!isAnyAbsolutePath(p)) return false;
|
|
61
|
+
if (hostPlatform === "win32") return !isWindowsAbsolutePath(p);
|
|
62
|
+
return !isPosixAbsolutePath(p);
|
|
63
|
+
}
|
|
45
64
|
|
|
46
65
|
// src/report-utils.ts
|
|
47
|
-
var import_path = require("path");
|
|
48
66
|
function normalizeScreenshotsBaseUrl(baseUrl) {
|
|
49
67
|
return baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
50
68
|
}
|
|
@@ -89,7 +107,7 @@ function attachmentsToImages(attachments, screenshotsBaseUrl = "/screenshots/")
|
|
|
89
107
|
const role = match[2];
|
|
90
108
|
if (baseName === null || baseName === void 0 || role === null || role === void 0) continue;
|
|
91
109
|
images[baseName] ??= {};
|
|
92
|
-
const url = (
|
|
110
|
+
const url = isAnyAbsolutePath(attachment.path) ? `/file/${encodeURIComponent(attachment.path)}` : `${baseUrl}${attachment.path}`;
|
|
93
111
|
const img = images[baseName];
|
|
94
112
|
if (img !== null && img !== void 0) {
|
|
95
113
|
if (role === "actual") img.actual = url;
|
|
@@ -124,9 +142,6 @@ function isCurrentArtifact(image) {
|
|
|
124
142
|
}
|
|
125
143
|
function isReusablePassingImage(image) {
|
|
126
144
|
const source = image.source ?? classifyImage(image);
|
|
127
|
-
if (source === "comparison") {
|
|
128
|
-
return image.actual !== void 0 && image.diff === void 0;
|
|
129
|
-
}
|
|
130
145
|
return source === "baseline-only";
|
|
131
146
|
}
|
|
132
147
|
function hasReusablePassingImages(images) {
|
|
@@ -147,8 +162,8 @@ function preservePreviousPassingImages(test, status, images) {
|
|
|
147
162
|
return {
|
|
148
163
|
...currentImages,
|
|
149
164
|
[name]: {
|
|
150
|
-
|
|
151
|
-
source:
|
|
165
|
+
expect: previousImage.expect,
|
|
166
|
+
source: "baseline-only"
|
|
152
167
|
}
|
|
153
168
|
};
|
|
154
169
|
}, images);
|
|
@@ -171,7 +186,12 @@ function createMutableReportState(screenshotDir = "./screenshots") {
|
|
|
171
186
|
function applyTestBeginEvent(state, data) {
|
|
172
187
|
const { id, title, titlePath, browser, projectName, location } = data;
|
|
173
188
|
state.currentRunIds.add(id);
|
|
174
|
-
state.reportData.tests[id]
|
|
189
|
+
const existing = state.reportData.tests[id];
|
|
190
|
+
if (existing !== void 0) {
|
|
191
|
+
existing.status = "running";
|
|
192
|
+
return existing;
|
|
193
|
+
}
|
|
194
|
+
const created = {
|
|
175
195
|
id,
|
|
176
196
|
titlePath: titlePath ?? [],
|
|
177
197
|
browser: browser ?? "",
|
|
@@ -183,7 +203,8 @@ function applyTestBeginEvent(state, data) {
|
|
|
183
203
|
location,
|
|
184
204
|
status: "running"
|
|
185
205
|
};
|
|
186
|
-
|
|
206
|
+
state.reportData.tests[id] = created;
|
|
207
|
+
return created;
|
|
187
208
|
}
|
|
188
209
|
function applyTestEndEvent(state, data, options = {}) {
|
|
189
210
|
const test = state.reportData.tests[data.id];
|
|
@@ -221,11 +242,13 @@ function applyTestEndEvent(state, data, options = {}) {
|
|
|
221
242
|
diffCount
|
|
222
243
|
};
|
|
223
244
|
}
|
|
224
|
-
function finalizeRunEvent(state) {
|
|
245
|
+
function finalizeRunEvent(state, options = {}) {
|
|
225
246
|
state.reportData.isRunning = false;
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
247
|
+
if (options.preserveNonCurrent !== true) {
|
|
248
|
+
state.reportData.tests = Object.fromEntries(
|
|
249
|
+
Object.entries(state.reportData.tests).filter(([id]) => state.currentRunIds.has(id))
|
|
250
|
+
);
|
|
251
|
+
}
|
|
229
252
|
state.currentRunIds.clear();
|
|
230
253
|
const runTests = Object.values(state.reportData.tests).filter((test) => test !== void 0);
|
|
231
254
|
return {
|
|
@@ -236,169 +259,211 @@ function finalizeRunEvent(state) {
|
|
|
236
259
|
}
|
|
237
260
|
|
|
238
261
|
// src/schemas.ts
|
|
262
|
+
var import_zod2 = require("zod");
|
|
263
|
+
|
|
264
|
+
// src/schemas/http.ts
|
|
239
265
|
var import_zod = require("zod");
|
|
240
|
-
var
|
|
266
|
+
var ApproveRequestBodySchema = import_zod.z.object({
|
|
267
|
+
id: import_zod.z.string(),
|
|
268
|
+
retry: import_zod.z.number(),
|
|
269
|
+
image: import_zod.z.string()
|
|
270
|
+
});
|
|
271
|
+
var RunTestDescriptorSchema = import_zod.z.object({
|
|
241
272
|
file: import_zod.z.string(),
|
|
242
|
-
line: import_zod.z.number()
|
|
273
|
+
line: import_zod.z.number(),
|
|
274
|
+
column: import_zod.z.number().optional(),
|
|
275
|
+
projectName: import_zod.z.string().optional(),
|
|
276
|
+
titlePath: import_zod.z.array(import_zod.z.string())
|
|
243
277
|
});
|
|
244
|
-
var
|
|
245
|
-
|
|
246
|
-
actual: import_zod.z.string().optional(),
|
|
247
|
-
expect: import_zod.z.string().optional(),
|
|
248
|
-
diff: import_zod.z.string().optional(),
|
|
249
|
-
error: import_zod.z.string().optional(),
|
|
250
|
-
source: VisualSourceSchema.optional()
|
|
278
|
+
var RunRequestBodySchema = import_zod.z.object({
|
|
279
|
+
tests: import_zod.z.array(RunTestDescriptorSchema).optional()
|
|
251
280
|
});
|
|
252
|
-
var
|
|
281
|
+
var RunResponseSchema = import_zod.z.discriminatedUnion("ok", [
|
|
282
|
+
import_zod.z.object({ ok: import_zod.z.literal(true) }),
|
|
253
283
|
import_zod.z.object({
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
}),
|
|
284
|
+
ok: import_zod.z.literal(false),
|
|
285
|
+
reason: import_zod.z.enum(["no-config", "already-running", "no-tests"])
|
|
286
|
+
})
|
|
287
|
+
]);
|
|
288
|
+
var StopResponseSchema = import_zod.z.discriminatedUnion("ok", [
|
|
289
|
+
import_zod.z.object({ ok: import_zod.z.literal(true) }),
|
|
260
290
|
import_zod.z.object({
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
291
|
+
ok: import_zod.z.literal(false),
|
|
292
|
+
reason: import_zod.z.literal("not-running")
|
|
293
|
+
})
|
|
294
|
+
]);
|
|
295
|
+
|
|
296
|
+
// src/schemas.ts
|
|
297
|
+
var LocationSchema = import_zod2.z.object({
|
|
298
|
+
file: import_zod2.z.string(),
|
|
299
|
+
line: import_zod2.z.number(),
|
|
300
|
+
column: import_zod2.z.number().optional()
|
|
301
|
+
});
|
|
302
|
+
var VisualSourceSchema = import_zod2.z.enum(["comparison", "baseline-only", "declared-only"]);
|
|
303
|
+
var ImagesSchema = import_zod2.z.object({
|
|
304
|
+
actual: import_zod2.z.string().optional(),
|
|
305
|
+
expect: import_zod2.z.string().optional(),
|
|
306
|
+
diff: import_zod2.z.string().optional(),
|
|
307
|
+
error: import_zod2.z.string().optional(),
|
|
308
|
+
source: VisualSourceSchema.optional()
|
|
309
|
+
});
|
|
310
|
+
var ScreenshotDeclarationSchema = import_zod2.z.discriminatedUnion("kind", [
|
|
311
|
+
import_zod2.z.object({
|
|
312
|
+
visualName: import_zod2.z.string(),
|
|
313
|
+
kind: import_zod2.z.literal("named"),
|
|
314
|
+
declaredName: import_zod2.z.string(),
|
|
315
|
+
snapshotBaseName: import_zod2.z.string(),
|
|
316
|
+
occurrenceIndex: import_zod2.z.number()
|
|
317
|
+
}),
|
|
318
|
+
import_zod2.z.object({
|
|
319
|
+
visualName: import_zod2.z.string(),
|
|
320
|
+
kind: import_zod2.z.literal("unnamed"),
|
|
321
|
+
occurrenceIndex: import_zod2.z.number()
|
|
264
322
|
})
|
|
265
323
|
]);
|
|
266
|
-
var AttachmentSchema =
|
|
267
|
-
name:
|
|
268
|
-
path:
|
|
269
|
-
contentType:
|
|
324
|
+
var AttachmentSchema = import_zod2.z.object({
|
|
325
|
+
name: import_zod2.z.string(),
|
|
326
|
+
path: import_zod2.z.string(),
|
|
327
|
+
contentType: import_zod2.z.string()
|
|
270
328
|
});
|
|
271
|
-
var TestStatusSchema =
|
|
272
|
-
var TestResultStatusSchema =
|
|
273
|
-
var TestResultSchema =
|
|
329
|
+
var TestStatusSchema = import_zod2.z.enum(["unknown", "pending", "running", "failed", "approved", "success", "retrying"]);
|
|
330
|
+
var TestResultStatusSchema = import_zod2.z.enum(["failed", "success", "pending"]);
|
|
331
|
+
var TestResultSchema = import_zod2.z.object({
|
|
274
332
|
status: TestResultStatusSchema,
|
|
275
|
-
retries:
|
|
276
|
-
images:
|
|
277
|
-
visualDeclarations:
|
|
278
|
-
error:
|
|
279
|
-
duration:
|
|
333
|
+
retries: import_zod2.z.number(),
|
|
334
|
+
images: import_zod2.z.record(import_zod2.z.string(), ImagesSchema).optional(),
|
|
335
|
+
visualDeclarations: import_zod2.z.array(ScreenshotDeclarationSchema).optional(),
|
|
336
|
+
error: import_zod2.z.string().optional(),
|
|
337
|
+
duration: import_zod2.z.number().optional()
|
|
280
338
|
});
|
|
281
|
-
var TestDataSchema =
|
|
282
|
-
id:
|
|
283
|
-
titlePath:
|
|
284
|
-
browser:
|
|
285
|
-
projectName:
|
|
286
|
-
title:
|
|
287
|
-
skip:
|
|
288
|
-
retries:
|
|
339
|
+
var TestDataSchema = import_zod2.z.object({
|
|
340
|
+
id: import_zod2.z.string(),
|
|
341
|
+
titlePath: import_zod2.z.array(import_zod2.z.string()),
|
|
342
|
+
browser: import_zod2.z.string(),
|
|
343
|
+
projectName: import_zod2.z.string().optional(),
|
|
344
|
+
title: import_zod2.z.string(),
|
|
345
|
+
skip: import_zod2.z.union([import_zod2.z.boolean(), import_zod2.z.string()]).optional(),
|
|
346
|
+
retries: import_zod2.z.number().optional(),
|
|
289
347
|
status: TestStatusSchema.optional(),
|
|
290
|
-
results:
|
|
291
|
-
approved:
|
|
292
|
-
attachments:
|
|
348
|
+
results: import_zod2.z.array(TestResultSchema).optional(),
|
|
349
|
+
approved: import_zod2.z.record(import_zod2.z.string(), import_zod2.z.number()).nullable().optional(),
|
|
350
|
+
attachments: import_zod2.z.array(AttachmentSchema).optional(),
|
|
293
351
|
location: LocationSchema.optional()
|
|
294
352
|
});
|
|
295
353
|
var CrvyRprtrTestSchema = TestDataSchema.extend({
|
|
296
|
-
checked:
|
|
354
|
+
checked: import_zod2.z.boolean()
|
|
297
355
|
});
|
|
298
|
-
var CrvyRprtrSuiteSchema =
|
|
299
|
-
() =>
|
|
300
|
-
path:
|
|
301
|
-
skip:
|
|
356
|
+
var CrvyRprtrSuiteSchema = import_zod2.z.lazy(
|
|
357
|
+
() => import_zod2.z.object({
|
|
358
|
+
path: import_zod2.z.array(import_zod2.z.string()),
|
|
359
|
+
skip: import_zod2.z.boolean(),
|
|
302
360
|
status: TestStatusSchema.optional(),
|
|
303
|
-
opened:
|
|
304
|
-
checked:
|
|
305
|
-
indeterminate:
|
|
306
|
-
children:
|
|
361
|
+
opened: import_zod2.z.boolean(),
|
|
362
|
+
checked: import_zod2.z.boolean(),
|
|
363
|
+
indeterminate: import_zod2.z.boolean(),
|
|
364
|
+
children: import_zod2.z.record(import_zod2.z.string(), import_zod2.z.union([CrvyRprtrSuiteSchema, CrvyRprtrTestSchema])).optional()
|
|
307
365
|
})
|
|
308
366
|
);
|
|
309
|
-
var IncomingWebSocketMessageSchema =
|
|
310
|
-
type:
|
|
311
|
-
data:
|
|
367
|
+
var IncomingWebSocketMessageSchema = import_zod2.z.object({
|
|
368
|
+
type: import_zod2.z.enum(["test-begin", "test-end", "run-end", "approve", "sync", "register"]),
|
|
369
|
+
data: import_zod2.z.unknown()
|
|
312
370
|
});
|
|
313
|
-
var WebSocketMessageSchema =
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
type:
|
|
318
|
-
data:
|
|
319
|
-
status:
|
|
320
|
-
removedTestIds:
|
|
371
|
+
var WebSocketMessageSchema = import_zod2.z.discriminatedUnion("type", [
|
|
372
|
+
import_zod2.z.object({ type: import_zod2.z.literal("test-begin"), data: TestDataSchema }),
|
|
373
|
+
import_zod2.z.object({ type: import_zod2.z.literal("test-update"), data: TestDataSchema }),
|
|
374
|
+
import_zod2.z.object({
|
|
375
|
+
type: import_zod2.z.literal("run-end"),
|
|
376
|
+
data: import_zod2.z.object({
|
|
377
|
+
status: import_zod2.z.enum(["passed", "failed", "skipped"]),
|
|
378
|
+
removedTestIds: import_zod2.z.array(import_zod2.z.string())
|
|
321
379
|
})
|
|
322
380
|
}),
|
|
323
|
-
|
|
324
|
-
type:
|
|
325
|
-
data:
|
|
326
|
-
tests:
|
|
327
|
-
isUpdateMode:
|
|
381
|
+
import_zod2.z.object({
|
|
382
|
+
type: import_zod2.z.literal("sync"),
|
|
383
|
+
data: import_zod2.z.object({
|
|
384
|
+
tests: import_zod2.z.record(import_zod2.z.string(), TestDataSchema),
|
|
385
|
+
isUpdateMode: import_zod2.z.boolean().optional()
|
|
328
386
|
})
|
|
329
387
|
}),
|
|
330
|
-
|
|
388
|
+
import_zod2.z.object({ type: import_zod2.z.literal("approve"), data: import_zod2.z.unknown() }),
|
|
389
|
+
import_zod2.z.object({
|
|
390
|
+
type: import_zod2.z.literal("run-status"),
|
|
391
|
+
data: import_zod2.z.object({
|
|
392
|
+
running: import_zod2.z.boolean()
|
|
393
|
+
})
|
|
394
|
+
})
|
|
331
395
|
]);
|
|
332
|
-
var TestBeginDataSchema =
|
|
333
|
-
id:
|
|
334
|
-
title:
|
|
335
|
-
titlePath:
|
|
336
|
-
browser:
|
|
337
|
-
projectName:
|
|
396
|
+
var TestBeginDataSchema = import_zod2.z.object({
|
|
397
|
+
id: import_zod2.z.string(),
|
|
398
|
+
title: import_zod2.z.string(),
|
|
399
|
+
titlePath: import_zod2.z.array(import_zod2.z.string()),
|
|
400
|
+
browser: import_zod2.z.string(),
|
|
401
|
+
projectName: import_zod2.z.string().optional(),
|
|
338
402
|
location: LocationSchema
|
|
339
403
|
});
|
|
340
|
-
var TestEndDataSchema =
|
|
341
|
-
id:
|
|
342
|
-
status:
|
|
343
|
-
attachments:
|
|
344
|
-
visualNames:
|
|
345
|
-
visualDeclarations:
|
|
404
|
+
var TestEndDataSchema = import_zod2.z.object({
|
|
405
|
+
id: import_zod2.z.string(),
|
|
406
|
+
status: import_zod2.z.enum(["passed", "failed", "skipped"]),
|
|
407
|
+
attachments: import_zod2.z.array(AttachmentSchema),
|
|
408
|
+
visualNames: import_zod2.z.array(import_zod2.z.string()).default([]),
|
|
409
|
+
visualDeclarations: import_zod2.z.preprocess(
|
|
346
410
|
(value) => value === null ? void 0 : value,
|
|
347
|
-
|
|
411
|
+
import_zod2.z.array(ScreenshotDeclarationSchema).optional()
|
|
348
412
|
),
|
|
349
|
-
error:
|
|
350
|
-
duration:
|
|
351
|
-
});
|
|
352
|
-
var RunEndDataSchema = import_zod.z.object({
|
|
353
|
-
status: import_zod.z.enum(["passed", "failed", "skipped"])
|
|
413
|
+
error: import_zod2.z.string().optional(),
|
|
414
|
+
duration: import_zod2.z.number().optional()
|
|
354
415
|
});
|
|
355
|
-
var
|
|
356
|
-
|
|
357
|
-
playwrightTestDir: import_zod.z.string().optional(),
|
|
358
|
-
playwrightSnapshotPathTemplate: import_zod.z.string().optional(),
|
|
359
|
-
playwrightToHaveScreenshotPathTemplate: import_zod.z.string().optional()
|
|
416
|
+
var RunEndDataSchema = import_zod2.z.object({
|
|
417
|
+
status: import_zod2.z.enum(["passed", "failed", "skipped"])
|
|
360
418
|
});
|
|
361
|
-
var
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
419
|
+
var RegisterDataSchema = import_zod2.z.object({
|
|
420
|
+
playwrightSnapshotDir: import_zod2.z.string().optional(),
|
|
421
|
+
playwrightTestDir: import_zod2.z.string().optional(),
|
|
422
|
+
playwrightSnapshotPathTemplate: import_zod2.z.string().optional(),
|
|
423
|
+
playwrightToHaveScreenshotPathTemplate: import_zod2.z.string().optional(),
|
|
424
|
+
configFile: import_zod2.z.string().optional(),
|
|
425
|
+
cwd: import_zod2.z.string().optional()
|
|
367
426
|
});
|
|
368
|
-
var
|
|
369
|
-
|
|
370
|
-
|
|
427
|
+
var ReportDataSchema = import_zod2.z.object({
|
|
428
|
+
isRunning: import_zod2.z.boolean(),
|
|
429
|
+
tests: import_zod2.z.record(import_zod2.z.string(), TestDataSchema),
|
|
430
|
+
browsers: import_zod2.z.array(import_zod2.z.string()),
|
|
431
|
+
isUpdateMode: import_zod2.z.boolean(),
|
|
432
|
+
screenshotDir: import_zod2.z.string()
|
|
371
433
|
});
|
|
372
|
-
var
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
timestamp: import_zod.z.number(),
|
|
376
|
-
workerIndex: import_zod.z.number()
|
|
434
|
+
var LoadedReportDataSchema = import_zod2.z.object({
|
|
435
|
+
tests: import_zod2.z.record(import_zod2.z.string(), TestDataSchema).optional(),
|
|
436
|
+
isUpdateMode: import_zod2.z.boolean().optional()
|
|
377
437
|
});
|
|
378
|
-
var
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
438
|
+
var OfflineEventSchema = import_zod2.z.object({
|
|
439
|
+
type: import_zod2.z.enum(["test-begin", "test-end", "run-end"]),
|
|
440
|
+
data: import_zod2.z.unknown(),
|
|
441
|
+
timestamp: import_zod2.z.number(),
|
|
442
|
+
workerIndex: import_zod2.z.number()
|
|
383
443
|
});
|
|
384
|
-
var
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
444
|
+
var OfflineReportSchema = import_zod2.z.object({
|
|
445
|
+
version: import_zod2.z.number(),
|
|
446
|
+
generatedAt: import_zod2.z.string(),
|
|
447
|
+
workers: import_zod2.z.number(),
|
|
448
|
+
events: import_zod2.z.array(OfflineEventSchema)
|
|
388
449
|
});
|
|
389
|
-
var ReportApiResponseSchema =
|
|
390
|
-
tests:
|
|
391
|
-
isUpdateMode:
|
|
450
|
+
var ReportApiResponseSchema = import_zod2.z.object({
|
|
451
|
+
tests: import_zod2.z.record(import_zod2.z.string(), TestDataSchema),
|
|
452
|
+
isUpdateMode: import_zod2.z.boolean().optional(),
|
|
453
|
+
isRunning: import_zod2.z.boolean().optional(),
|
|
454
|
+
runEnabled: import_zod2.z.boolean().optional()
|
|
392
455
|
});
|
|
393
|
-
var ClientBootstrapDataSchema =
|
|
456
|
+
var ClientBootstrapDataSchema = import_zod2.z.object({
|
|
394
457
|
report: ReportApiResponseSchema.extend({
|
|
395
|
-
isUpdateMode:
|
|
458
|
+
isUpdateMode: import_zod2.z.boolean()
|
|
396
459
|
}),
|
|
397
|
-
liveUpdates:
|
|
398
|
-
approvalEnabled:
|
|
399
|
-
approvalMessage:
|
|
460
|
+
liveUpdates: import_zod2.z.boolean(),
|
|
461
|
+
approvalEnabled: import_zod2.z.boolean(),
|
|
462
|
+
approvalMessage: import_zod2.z.string().optional(),
|
|
463
|
+
runEnabled: import_zod2.z.boolean().optional(),
|
|
464
|
+
isRunning: import_zod2.z.boolean().optional()
|
|
400
465
|
});
|
|
401
|
-
var ImagesViewModeSchema =
|
|
466
|
+
var ImagesViewModeSchema = import_zod2.z.enum(["side-by-side", "swap", "slide", "blend"]);
|
|
402
467
|
function safeParse(schema, data) {
|
|
403
468
|
const result = schema.safeParse(data);
|
|
404
469
|
if (result.success) {
|
|
@@ -407,70 +472,15 @@ function safeParse(schema, data) {
|
|
|
407
472
|
return null;
|
|
408
473
|
}
|
|
409
474
|
|
|
410
|
-
// src/offline-reports.ts
|
|
411
|
-
var OFFLINE_REPORT_FILE_PATTERN = /^crvy-rprtr(?:-\d+)?\.json$/;
|
|
412
|
-
async function findOfflineReportPaths(searchDir) {
|
|
413
|
-
try {
|
|
414
|
-
const entries = await (0, import_promises.readdir)(searchDir);
|
|
415
|
-
return entries.filter((entry) => OFFLINE_REPORT_FILE_PATTERN.test(entry)).sort((left, right) => left.localeCompare(right)).map((entry) => (0, import_path2.join)(searchDir, entry));
|
|
416
|
-
} catch (error) {
|
|
417
|
-
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
418
|
-
return [];
|
|
419
|
-
}
|
|
420
|
-
throw error;
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
function parseOfflineReport(value) {
|
|
424
|
-
const parsed = safeParse(OfflineReportSchema, value);
|
|
425
|
-
if (parsed === null || parsed.version !== 1 || !Array.isArray(parsed.events)) {
|
|
426
|
-
return null;
|
|
427
|
-
}
|
|
428
|
-
return parsed;
|
|
429
|
-
}
|
|
430
|
-
function mergeOfflineReportsIntoTests(existingTests, offlineReports, options = {}) {
|
|
431
|
-
const state = createMutableReportState(options.screenshotDir);
|
|
432
|
-
let shouldFinalize = false;
|
|
433
|
-
for (const report of offlineReports) {
|
|
434
|
-
for (const event of report.events) {
|
|
435
|
-
switch (event.type) {
|
|
436
|
-
case "test-begin": {
|
|
437
|
-
const parsed = safeParse(TestBeginDataSchema, event.data);
|
|
438
|
-
if (parsed !== null) {
|
|
439
|
-
applyTestBeginEvent(state, parsed);
|
|
440
|
-
}
|
|
441
|
-
break;
|
|
442
|
-
}
|
|
443
|
-
case "test-end": {
|
|
444
|
-
const parsed = safeParse(TestEndDataSchema, event.data);
|
|
445
|
-
if (parsed !== null) {
|
|
446
|
-
applyTestEndEvent(state, parsed, { screenshotsBaseUrl: options.screenshotsBaseUrl });
|
|
447
|
-
}
|
|
448
|
-
break;
|
|
449
|
-
}
|
|
450
|
-
case "run-end":
|
|
451
|
-
shouldFinalize = true;
|
|
452
|
-
break;
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
if (shouldFinalize) {
|
|
457
|
-
finalizeRunEvent(state);
|
|
458
|
-
}
|
|
459
|
-
return {
|
|
460
|
-
...existingTests,
|
|
461
|
-
...state.reportData.tests
|
|
462
|
-
};
|
|
463
|
-
}
|
|
464
|
-
|
|
465
475
|
// src/server/file-utils.ts
|
|
466
|
-
var
|
|
467
|
-
var
|
|
476
|
+
var import_promises = require("fs/promises");
|
|
477
|
+
var import_path = require("path");
|
|
468
478
|
function isFileNotFound(error) {
|
|
469
479
|
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
470
480
|
}
|
|
471
481
|
async function fileExists(filePath) {
|
|
472
482
|
try {
|
|
473
|
-
await (0,
|
|
483
|
+
await (0, import_promises.access)(filePath);
|
|
474
484
|
return true;
|
|
475
485
|
} catch (error) {
|
|
476
486
|
if (isFileNotFound(error)) {
|
|
@@ -481,7 +491,7 @@ async function fileExists(filePath) {
|
|
|
481
491
|
}
|
|
482
492
|
async function isDirectory(filePath) {
|
|
483
493
|
try {
|
|
484
|
-
const stats = await (0,
|
|
494
|
+
const stats = await (0, import_promises.stat)(filePath);
|
|
485
495
|
return stats.isDirectory();
|
|
486
496
|
} catch (error) {
|
|
487
497
|
if (isFileNotFound(error)) {
|
|
@@ -492,7 +502,7 @@ async function isDirectory(filePath) {
|
|
|
492
502
|
}
|
|
493
503
|
async function readJsonFile(filePath) {
|
|
494
504
|
try {
|
|
495
|
-
const raw = await (0,
|
|
505
|
+
const raw = await (0, import_promises.readFile)(filePath, "utf8");
|
|
496
506
|
if (raw.trim() === "") {
|
|
497
507
|
return null;
|
|
498
508
|
}
|
|
@@ -505,16 +515,16 @@ async function readJsonFile(filePath) {
|
|
|
505
515
|
}
|
|
506
516
|
}
|
|
507
517
|
async function writeJsonFile(filePath, value) {
|
|
508
|
-
await (0,
|
|
509
|
-
await (0,
|
|
518
|
+
await (0, import_promises.mkdir)((0, import_path.dirname)(filePath), { recursive: true });
|
|
519
|
+
await (0, import_promises.writeFile)(filePath, `${JSON.stringify(value, null, 2)}
|
|
510
520
|
`);
|
|
511
521
|
}
|
|
512
522
|
async function copyFilePortable(sourcePath, destinationPath) {
|
|
513
|
-
await (0,
|
|
514
|
-
await (0,
|
|
523
|
+
await (0, import_promises.mkdir)((0, import_path.dirname)(destinationPath), { recursive: true });
|
|
524
|
+
await (0, import_promises.copyFile)(sourcePath, destinationPath);
|
|
515
525
|
}
|
|
516
526
|
function inferContentType(filePath) {
|
|
517
|
-
switch ((0,
|
|
527
|
+
switch ((0, import_path.extname)(filePath).toLowerCase()) {
|
|
518
528
|
case ".css":
|
|
519
529
|
return "text/css";
|
|
520
530
|
case ".gif":
|
|
@@ -542,7 +552,7 @@ function inferContentType(filePath) {
|
|
|
542
552
|
}
|
|
543
553
|
async function respondWithFile(filePath, contentType) {
|
|
544
554
|
try {
|
|
545
|
-
const file = await (0,
|
|
555
|
+
const file = await (0, import_promises.readFile)(filePath);
|
|
546
556
|
const resolvedContentType = contentType ?? inferContentType(filePath);
|
|
547
557
|
const headers = { "X-Content-Type-Options": "nosniff" };
|
|
548
558
|
if (resolvedContentType !== void 0) {
|
|
@@ -557,17 +567,104 @@ async function respondWithFile(filePath, contentType) {
|
|
|
557
567
|
}
|
|
558
568
|
}
|
|
559
569
|
|
|
570
|
+
// src/offline-reports.ts
|
|
571
|
+
var MAX_CONCURRENT_FILE_OPS = 5;
|
|
572
|
+
var OFFLINE_REPORT_FILE_PATTERN = /^crvy-rprtr(?:-\d+)?\.json$/;
|
|
573
|
+
async function findOfflineReportPaths(searchDir) {
|
|
574
|
+
try {
|
|
575
|
+
const entries = await (0, import_promises2.readdir)(searchDir);
|
|
576
|
+
return entries.filter((entry) => OFFLINE_REPORT_FILE_PATTERN.test(entry)).sort((left, right) => left.localeCompare(right)).map((entry) => (0, import_path2.join)(searchDir, entry));
|
|
577
|
+
} catch (error) {
|
|
578
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
579
|
+
return [];
|
|
580
|
+
}
|
|
581
|
+
throw error;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
function parseOfflineReport(value) {
|
|
585
|
+
const parsed = safeParse(OfflineReportSchema, value);
|
|
586
|
+
if (parsed === null || parsed.version !== 1 || !Array.isArray(parsed.events)) {
|
|
587
|
+
return null;
|
|
588
|
+
}
|
|
589
|
+
return parsed;
|
|
590
|
+
}
|
|
591
|
+
async function readOfflineReport(filePath) {
|
|
592
|
+
try {
|
|
593
|
+
const raw = await readJsonFile(filePath);
|
|
594
|
+
if (raw === null) {
|
|
595
|
+
return null;
|
|
596
|
+
}
|
|
597
|
+
const parsed = parseOfflineReport(raw);
|
|
598
|
+
if (parsed !== null) {
|
|
599
|
+
console.log(`[Server] Loading offline report: ${filePath}`);
|
|
600
|
+
return parsed;
|
|
601
|
+
}
|
|
602
|
+
} catch {
|
|
603
|
+
}
|
|
604
|
+
return null;
|
|
605
|
+
}
|
|
606
|
+
async function loadOfflineReports(reportData, offlineReportDir) {
|
|
607
|
+
const offlineReportPaths = await findOfflineReportPaths(offlineReportDir);
|
|
608
|
+
if (offlineReportPaths.length === 0) {
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
const limit = (0, import_p_limit.default)(MAX_CONCURRENT_FILE_OPS);
|
|
612
|
+
const reports = await Promise.all(offlineReportPaths.map((filePath) => limit(() => readOfflineReport(filePath))));
|
|
613
|
+
const validReports = reports.filter((report) => report !== null);
|
|
614
|
+
if (validReports.length === 0) {
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
reportData.tests = mergeOfflineReportsIntoTests(reportData.tests, validReports, {
|
|
618
|
+
screenshotDir: reportData.screenshotDir,
|
|
619
|
+
screenshotsBaseUrl: "/screenshots/"
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
function mergeOfflineReportsIntoTests(existingTests, offlineReports, options = {}) {
|
|
623
|
+
const state = createMutableReportState(options.screenshotDir);
|
|
624
|
+
let shouldFinalize = false;
|
|
625
|
+
for (const report of offlineReports) {
|
|
626
|
+
for (const event of report.events) {
|
|
627
|
+
switch (event.type) {
|
|
628
|
+
case "test-begin": {
|
|
629
|
+
const parsed = safeParse(TestBeginDataSchema, event.data);
|
|
630
|
+
if (parsed !== null) {
|
|
631
|
+
applyTestBeginEvent(state, parsed);
|
|
632
|
+
}
|
|
633
|
+
break;
|
|
634
|
+
}
|
|
635
|
+
case "test-end": {
|
|
636
|
+
const parsed = safeParse(TestEndDataSchema, event.data);
|
|
637
|
+
if (parsed !== null) {
|
|
638
|
+
applyTestEndEvent(state, parsed, { screenshotsBaseUrl: options.screenshotsBaseUrl });
|
|
639
|
+
}
|
|
640
|
+
break;
|
|
641
|
+
}
|
|
642
|
+
case "run-end":
|
|
643
|
+
shouldFinalize = true;
|
|
644
|
+
break;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
if (shouldFinalize) {
|
|
649
|
+
finalizeRunEvent(state);
|
|
650
|
+
}
|
|
651
|
+
return {
|
|
652
|
+
...existingTests,
|
|
653
|
+
...state.reportData.tests
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
|
|
560
657
|
// src/server/handlers.ts
|
|
561
658
|
var import_fs2 = require("fs");
|
|
562
659
|
|
|
563
660
|
// src/server/artifact-routes.ts
|
|
564
661
|
var import_fs = require("fs");
|
|
565
662
|
var import_promises3 = require("fs/promises");
|
|
566
|
-
var
|
|
663
|
+
var import_path5 = require("path");
|
|
567
664
|
|
|
568
665
|
// src/snapshot-path-resolver.ts
|
|
569
666
|
var import_crypto = require("crypto");
|
|
570
|
-
var
|
|
667
|
+
var import_path3 = require("path");
|
|
571
668
|
var DEFAULT_SCREENSHOT_TEMPLATE = "{snapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}{-projectName}{-snapshotSuffix}{ext}";
|
|
572
669
|
var WINDOWS_FILESYSTEM_FRIENDLY_LENGTH = 60;
|
|
573
670
|
function isUnsafeFilePathCharacter(character) {
|
|
@@ -602,16 +699,16 @@ function trimLongString(value, length = WINDOWS_FILESYSTEM_FRIENDLY_LENGTH) {
|
|
|
602
699
|
const end = length - middle.length - start;
|
|
603
700
|
return value.slice(0, start) + middle + value.slice(-end);
|
|
604
701
|
}
|
|
605
|
-
function sanitizeFilePathBeforeExtension(filePath, extension = (0,
|
|
702
|
+
function sanitizeFilePathBeforeExtension(filePath, extension = (0, import_path3.extname)(filePath)) {
|
|
606
703
|
const base = filePath.slice(0, filePath.length - extension.length);
|
|
607
704
|
return sanitizeForFilePath(base) + extension;
|
|
608
705
|
}
|
|
609
706
|
function addSuffixToFilePath(filePath, suffix) {
|
|
610
|
-
const extension = (0,
|
|
707
|
+
const extension = (0, import_path3.extname)(filePath);
|
|
611
708
|
return filePath.slice(0, filePath.length - extension.length) + suffix + extension;
|
|
612
709
|
}
|
|
613
710
|
function normalizedSnapshotDir(config) {
|
|
614
|
-
return (0,
|
|
711
|
+
return (0, import_path3.resolve)(config.configDir, config.snapshotDir);
|
|
615
712
|
}
|
|
616
713
|
function templateValue(template, token, value) {
|
|
617
714
|
return template.replace(
|
|
@@ -621,8 +718,8 @@ function templateValue(template, token, value) {
|
|
|
621
718
|
}
|
|
622
719
|
function applyTemplate(input, nameArgument, extension) {
|
|
623
720
|
const template = input.config.toHaveScreenshotPathTemplate ?? input.config.snapshotPathTemplate ?? DEFAULT_SCREENSHOT_TEMPLATE;
|
|
624
|
-
const relativeTestFilePath = (0,
|
|
625
|
-
const parsed = (0,
|
|
721
|
+
const relativeTestFilePath = (0, import_path3.relative)(input.config.testDir, input.testFile);
|
|
722
|
+
const parsed = (0, import_path3.parse)(relativeTestFilePath);
|
|
626
723
|
const tokens = [
|
|
627
724
|
["testDir", input.config.testDir],
|
|
628
725
|
["snapshotDir", normalizedSnapshotDir(input.config)],
|
|
@@ -640,16 +737,16 @@ function applyTemplate(input, nameArgument, extension) {
|
|
|
640
737
|
(currentTemplate, [token, value]) => templateValue(currentTemplate, token, value),
|
|
641
738
|
template
|
|
642
739
|
);
|
|
643
|
-
return (0,
|
|
740
|
+
return (0, import_path3.resolve)(input.config.configDir, snapshotPath);
|
|
644
741
|
}
|
|
645
|
-
function removeExtension(filePath, extension = (0,
|
|
742
|
+
function removeExtension(filePath, extension = (0, import_path3.extname)(filePath)) {
|
|
646
743
|
return filePath.slice(0, filePath.length - extension.length);
|
|
647
744
|
}
|
|
648
745
|
function snapshotNameParts(declaredName) {
|
|
649
|
-
const extension = (0,
|
|
746
|
+
const extension = (0, import_path3.extname)(declaredName) || ".png";
|
|
650
747
|
return {
|
|
651
748
|
extension,
|
|
652
|
-
filePath: (0,
|
|
749
|
+
filePath: (0, import_path3.extname)(declaredName) === "" ? `${declaredName}${extension}` : declaredName
|
|
653
750
|
};
|
|
654
751
|
}
|
|
655
752
|
function filePathForOccurrence(filePath, occurrenceIndex) {
|
|
@@ -673,7 +770,7 @@ function resolveStringCallTarget(input, declaration) {
|
|
|
673
770
|
function resolveArrayCallTarget(input, declaration) {
|
|
674
771
|
const { extension, filePath } = snapshotNameParts(declaration.declaredName);
|
|
675
772
|
const occurrenceFilePath = filePathForOccurrence(filePath, declaration.occurrenceIndex);
|
|
676
|
-
const nameArgument = (0,
|
|
773
|
+
const nameArgument = (0, import_path3.join)((0, import_path3.dirname)(occurrenceFilePath), (0, import_path3.basename)(occurrenceFilePath, extension));
|
|
677
774
|
return createResolvedBaselineTarget(input, declaration, nameArgument, extension);
|
|
678
775
|
}
|
|
679
776
|
function resolveNamedTarget(input, declaration) {
|
|
@@ -711,7 +808,7 @@ function resolveTarget(input, declaration) {
|
|
|
711
808
|
case "unnamed": {
|
|
712
809
|
const anonymousFileName = anonymousName(input.reporterTitlePath, declaration.occurrenceIndex);
|
|
713
810
|
const extension = ".png";
|
|
714
|
-
const nameArgument = (0,
|
|
811
|
+
const nameArgument = (0, import_path3.join)((0, import_path3.dirname)(anonymousFileName), (0, import_path3.basename)(anonymousFileName, extension));
|
|
715
812
|
return createResolvedBaselineTarget(input, declaration, nameArgument, extension);
|
|
716
813
|
}
|
|
717
814
|
}
|
|
@@ -724,7 +821,7 @@ function resolveBaselineTargets(input) {
|
|
|
724
821
|
}
|
|
725
822
|
|
|
726
823
|
// src/server/utils.ts
|
|
727
|
-
var
|
|
824
|
+
var import_path4 = require("path");
|
|
728
825
|
var LIVE_UPDATES_WEBSOCKET_PATH = "/";
|
|
729
826
|
function broadcastToBrowsers(wsClients, msg) {
|
|
730
827
|
const payload = JSON.stringify(msg);
|
|
@@ -736,10 +833,10 @@ function isWebSocketUpgradeRequest(req) {
|
|
|
736
833
|
return new URL(req.url).pathname === LIVE_UPDATES_WEBSOCKET_PATH && req.headers.get("upgrade")?.toLowerCase() === "websocket";
|
|
737
834
|
}
|
|
738
835
|
function isPathWithinRoots(target, roots) {
|
|
739
|
-
const resolvedTarget = (0,
|
|
836
|
+
const resolvedTarget = (0, import_path4.resolve)(target);
|
|
740
837
|
return roots.some((root) => {
|
|
741
|
-
const rel = (0,
|
|
742
|
-
return rel === "" || !rel.startsWith(`..${
|
|
838
|
+
const rel = (0, import_path4.relative)((0, import_path4.resolve)(root), resolvedTarget);
|
|
839
|
+
return rel === "" || !rel.startsWith(`..${import_path4.sep}`) && rel !== ".." && !(0, import_path4.isAbsolute)(rel);
|
|
743
840
|
});
|
|
744
841
|
}
|
|
745
842
|
|
|
@@ -753,17 +850,24 @@ async function realpathOrNull(path) {
|
|
|
753
850
|
}
|
|
754
851
|
async function handleFile(ctx, req) {
|
|
755
852
|
const notFound = () => new Response("Not Found", { status: 404 });
|
|
853
|
+
let rawDecoded;
|
|
756
854
|
let decodedPath;
|
|
757
855
|
try {
|
|
758
|
-
|
|
856
|
+
rawDecoded = decodeURIComponent(new URL(req.url).pathname.slice("/file/".length));
|
|
857
|
+
decodedPath = (0, import_path5.resolve)(rawDecoded);
|
|
759
858
|
} catch {
|
|
760
859
|
return notFound();
|
|
761
860
|
}
|
|
762
861
|
const realTarget = await realpathOrNull(decodedPath);
|
|
763
862
|
if (realTarget === null) {
|
|
863
|
+
if (isForeignAbsolutePath(rawDecoded, process.platform)) {
|
|
864
|
+
console.warn(
|
|
865
|
+
`[Crvy Rprtr] /file request resolved to a foreign-OS absolute path that cannot be served on ${process.platform}: ${rawDecoded}`
|
|
866
|
+
);
|
|
867
|
+
}
|
|
764
868
|
return notFound();
|
|
765
869
|
}
|
|
766
|
-
const realRoots = (await Promise.all((ctx.artifactRoots ?? []).map((root) => realpathOrNull((0,
|
|
870
|
+
const realRoots = (await Promise.all((ctx.artifactRoots ?? []).map((root) => realpathOrNull((0, import_path5.resolve)(root))))).filter(
|
|
767
871
|
(root) => root !== null
|
|
768
872
|
);
|
|
769
873
|
if (!isPathWithinRoots(realTarget, realRoots)) {
|
|
@@ -793,8 +897,8 @@ function resolveBaselineSnapshotPath(routing, test, retry, imageName) {
|
|
|
793
897
|
declarations: [declaration],
|
|
794
898
|
config: {
|
|
795
899
|
configDir: routing.configDir,
|
|
796
|
-
testDir: routing.playwrightTestDir ?? (0,
|
|
797
|
-
snapshotDir: routing.playwrightSnapshotDir ?? (0,
|
|
900
|
+
testDir: routing.playwrightTestDir ?? (0, import_path5.dirname)(testFile),
|
|
901
|
+
snapshotDir: routing.playwrightSnapshotDir ?? (0, import_path5.dirname)(testFile),
|
|
798
902
|
projectName: test.projectName ?? test.browser,
|
|
799
903
|
snapshotSuffix: process.platform,
|
|
800
904
|
snapshotPathTemplate: routing.playwrightSnapshotPathTemplate,
|
|
@@ -892,8 +996,8 @@ function handleTestEnd(ctx, data) {
|
|
|
892
996
|
broadcastToBrowsers(ctx.wsClients, message);
|
|
893
997
|
}
|
|
894
998
|
async function handleRunEnd(ctx, data) {
|
|
895
|
-
const removedTestIds = Object.keys(ctx.reportData.tests).filter((id) => !ctx.currentRunIds.has(id));
|
|
896
|
-
const { passed, failed, pending } = finalizeRunEvent(ctx);
|
|
999
|
+
const removedTestIds = ctx.isFilteredRun ? [] : Object.keys(ctx.reportData.tests).filter((id) => !ctx.currentRunIds.has(id));
|
|
1000
|
+
const { passed, failed, pending } = finalizeRunEvent(ctx, { preserveNonCurrent: ctx.isFilteredRun });
|
|
897
1001
|
await ctx.saveReport();
|
|
898
1002
|
console.log(`
|
|
899
1003
|
Run complete \u2014 ${passed} passed, ${failed} failed, ${pending} skipped`);
|
|
@@ -946,14 +1050,91 @@ function handleRegister(ctx, data) {
|
|
|
946
1050
|
ctx.routesContext.approvalRouting.playwrightToHaveScreenshotPathTemplate = data.playwrightToHaveScreenshotPathTemplate;
|
|
947
1051
|
}
|
|
948
1052
|
}
|
|
1053
|
+
if (data.configFile !== void 0 && data.cwd !== void 0) {
|
|
1054
|
+
ctx.routesContext.runContext = { configFile: data.configFile, cwd: data.cwd };
|
|
1055
|
+
}
|
|
949
1056
|
console.log("[Server] Reporter registered with config:", {
|
|
950
1057
|
playwrightSnapshotDir: data.playwrightSnapshotDir,
|
|
951
1058
|
playwrightTestDir: data.playwrightTestDir
|
|
952
1059
|
});
|
|
953
1060
|
}
|
|
954
1061
|
|
|
1062
|
+
// src/server/playwright-config.ts
|
|
1063
|
+
var import_path6 = require("path");
|
|
1064
|
+
var CONFIG_FILES = [
|
|
1065
|
+
"playwright.config.ts",
|
|
1066
|
+
"playwright.config.mts",
|
|
1067
|
+
"playwright.config.cts",
|
|
1068
|
+
"playwright.config.js",
|
|
1069
|
+
"playwright.config.mjs",
|
|
1070
|
+
"playwright.config.cjs"
|
|
1071
|
+
];
|
|
1072
|
+
async function resolvePlaywrightConfig(cwd) {
|
|
1073
|
+
const matches = await Promise.all(
|
|
1074
|
+
CONFIG_FILES.map(async (file) => {
|
|
1075
|
+
const candidate = (0, import_path6.join)(cwd, file);
|
|
1076
|
+
return await fileExists(candidate) ? candidate : null;
|
|
1077
|
+
})
|
|
1078
|
+
);
|
|
1079
|
+
return matches.find((path) => path !== null) ?? null;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
// src/server/routes-context.ts
|
|
1083
|
+
function createRoutesContext(reportData, staticDir, saveReport, options) {
|
|
1084
|
+
const artifactRoots = [reportData.screenshotDir, options.outputDir, options.playwrightSnapshotDir].filter(
|
|
1085
|
+
(root) => root !== void 0 && root !== ""
|
|
1086
|
+
);
|
|
1087
|
+
return {
|
|
1088
|
+
reportData,
|
|
1089
|
+
staticDir,
|
|
1090
|
+
saveReport,
|
|
1091
|
+
artifactRoots,
|
|
1092
|
+
approvalRouting: {
|
|
1093
|
+
configDir: options.configDir ?? process.cwd(),
|
|
1094
|
+
playwrightTestDir: options.playwrightTestDir,
|
|
1095
|
+
playwrightSnapshotDir: options.playwrightSnapshotDir,
|
|
1096
|
+
playwrightSnapshotPathTemplate: options.playwrightSnapshotPathTemplate,
|
|
1097
|
+
playwrightToHaveScreenshotPathTemplate: options.playwrightToHaveScreenshotPathTemplate
|
|
1098
|
+
},
|
|
1099
|
+
runContext: void 0
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
|
|
955
1103
|
// src/server/routes.ts
|
|
956
1104
|
var import_path7 = require("path");
|
|
1105
|
+
|
|
1106
|
+
// src/server/run-routes.ts
|
|
1107
|
+
function handleRunRoutes(pathname, method, runController, req) {
|
|
1108
|
+
if (pathname === "/api/run" && method === "POST") {
|
|
1109
|
+
return handleApiRun(runController, req);
|
|
1110
|
+
}
|
|
1111
|
+
if (pathname === "/api/stop" && method === "POST") {
|
|
1112
|
+
return Promise.resolve(handleApiStop(runController));
|
|
1113
|
+
}
|
|
1114
|
+
return null;
|
|
1115
|
+
}
|
|
1116
|
+
async function handleApiRun(runController, req) {
|
|
1117
|
+
let body = {};
|
|
1118
|
+
try {
|
|
1119
|
+
body = await req.json();
|
|
1120
|
+
} catch {
|
|
1121
|
+
}
|
|
1122
|
+
const parsed = safeParse(RunRequestBodySchema, body);
|
|
1123
|
+
if (parsed === null) {
|
|
1124
|
+
return Response.json({ ok: false, error: "Invalid request body" }, { status: 400 });
|
|
1125
|
+
}
|
|
1126
|
+
const result = runController.start(parsed);
|
|
1127
|
+
if (result.ok) return Response.json(result);
|
|
1128
|
+
const status = result.reason === "no-tests" ? 400 : 409;
|
|
1129
|
+
return Response.json(result, { status });
|
|
1130
|
+
}
|
|
1131
|
+
function handleApiStop(runController) {
|
|
1132
|
+
const result = runController.stop();
|
|
1133
|
+
if (result.ok) return Response.json(result);
|
|
1134
|
+
return Response.json(result, { status: 409 });
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
// src/server/routes.ts
|
|
957
1138
|
async function handleRoot(ctx) {
|
|
958
1139
|
const html = await respondWithFile((0, import_path7.join)(ctx.staticDir, "index.html"), "text/html");
|
|
959
1140
|
return html ?? new Response("Not Found", { status: 404 });
|
|
@@ -970,7 +1151,10 @@ async function handleSrcFiles(req) {
|
|
|
970
1151
|
return file ?? new Response("Not Found", { status: 404 });
|
|
971
1152
|
}
|
|
972
1153
|
function handleApiReport(ctx) {
|
|
973
|
-
return Response.json(
|
|
1154
|
+
return Response.json({
|
|
1155
|
+
...ctx.reportData,
|
|
1156
|
+
runEnabled: ctx.runContext !== void 0
|
|
1157
|
+
});
|
|
974
1158
|
}
|
|
975
1159
|
var APPROVAL_TARGET_ERROR = "Could not resolve approval target";
|
|
976
1160
|
function actualPathFromUrl(ctx, actualUrl) {
|
|
@@ -1102,7 +1286,7 @@ async function handleDist(ctx, req) {
|
|
|
1102
1286
|
const file = await respondWithFile(filePath, contentType);
|
|
1103
1287
|
return file ?? new Response("Not Found", { status: 404 });
|
|
1104
1288
|
}
|
|
1105
|
-
function handleHttpRequest(ctx, req) {
|
|
1289
|
+
function handleHttpRequest(ctx, req, runController) {
|
|
1106
1290
|
const pathname = new URL(req.url).pathname;
|
|
1107
1291
|
if (pathname === "/") {
|
|
1108
1292
|
return handleRoot(ctx);
|
|
@@ -1122,6 +1306,10 @@ function handleHttpRequest(ctx, req) {
|
|
|
1122
1306
|
if (pathname === "/api/approve-all" && req.method === "POST") {
|
|
1123
1307
|
return handleApiApproveAll(ctx);
|
|
1124
1308
|
}
|
|
1309
|
+
const runResponse = handleRunRoutes(pathname, req.method, runController, req);
|
|
1310
|
+
if (runResponse !== null) {
|
|
1311
|
+
return runResponse;
|
|
1312
|
+
}
|
|
1125
1313
|
if (pathname.startsWith("/api/images/")) {
|
|
1126
1314
|
return handleApiImages(req);
|
|
1127
1315
|
}
|
|
@@ -1138,9 +1326,222 @@ function handleHttpRequest(ctx, req) {
|
|
|
1138
1326
|
return Promise.resolve(new Response("Not Found", { status: 404 }));
|
|
1139
1327
|
}
|
|
1140
1328
|
|
|
1141
|
-
// src/server/
|
|
1329
|
+
// src/server/run-controller.ts
|
|
1330
|
+
var import_child_process = require("child_process");
|
|
1331
|
+
var import_node_fs = require("node:fs");
|
|
1332
|
+
var import_node_module = require("node:module");
|
|
1333
|
+
var import_node_os = require("node:os");
|
|
1334
|
+
var import_node_path = require("node:path");
|
|
1335
|
+
var import_commands = require("package-manager-detector/commands");
|
|
1336
|
+
var import_detect = require("package-manager-detector/detect");
|
|
1142
1337
|
var import_meta = { url: require("url").pathToFileURL(__filename).href };
|
|
1143
|
-
var
|
|
1338
|
+
var STOP_GRACE_MS = 5e3;
|
|
1339
|
+
var KNOWN_SIGNALS = {
|
|
1340
|
+
SIGTERM: "SIGTERM",
|
|
1341
|
+
SIGKILL: "SIGKILL"
|
|
1342
|
+
};
|
|
1343
|
+
function resolvePlaywrightLaunch(cwd, playwrightArgs) {
|
|
1344
|
+
const agent = (0, import_detect.getUserAgent)();
|
|
1345
|
+
const resolved = agent === null ? null : (0, import_commands.resolveCommand)(agent, "execute-local", ["playwright", ...playwrightArgs]);
|
|
1346
|
+
if (resolved !== null) return { cmd: resolved.command, args: resolved.args };
|
|
1347
|
+
return { cmd: "npx", args: ["playwright", ...playwrightArgs] };
|
|
1348
|
+
}
|
|
1349
|
+
function descriptorLocation(d) {
|
|
1350
|
+
return d.column === void 0 ? `${d.file}:${d.line}` : `${d.file}:${d.line}:${d.column}`;
|
|
1351
|
+
}
|
|
1352
|
+
function sharedProject(tests) {
|
|
1353
|
+
const names = new Set(tests.map((t) => t.projectName ?? ""));
|
|
1354
|
+
if (names.size === 1) {
|
|
1355
|
+
const name = [...names][0];
|
|
1356
|
+
return name === "" ? void 0 : name;
|
|
1357
|
+
}
|
|
1358
|
+
return void 0;
|
|
1359
|
+
}
|
|
1360
|
+
function gteMinor(version, major, minor) {
|
|
1361
|
+
const match = /^(\d+)\.(\d+)/.exec(version.trim());
|
|
1362
|
+
if (match === null) return false;
|
|
1363
|
+
const maj = parseInt(match[1], 10);
|
|
1364
|
+
const min = parseInt(match[2], 10);
|
|
1365
|
+
if (maj !== major) return maj > major;
|
|
1366
|
+
return min >= minor;
|
|
1367
|
+
}
|
|
1368
|
+
function resolvePlaywrightVersion(cwd) {
|
|
1369
|
+
try {
|
|
1370
|
+
const req = (0, import_node_module.createRequire)((0, import_node_path.join)(cwd, "package.json"));
|
|
1371
|
+
const pkgPath = req.resolve("@playwright/test/package.json");
|
|
1372
|
+
const pkg = JSON.parse((0, import_node_fs.readFileSync)(pkgPath, "utf8"));
|
|
1373
|
+
return typeof pkg === "object" && pkg !== null && "version" in pkg && typeof pkg.version === "string" ? pkg.version : null;
|
|
1374
|
+
} catch {
|
|
1375
|
+
return null;
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
function buildTestListEntries(tests) {
|
|
1379
|
+
return tests.map((d) => {
|
|
1380
|
+
const loc = descriptorLocation(d);
|
|
1381
|
+
const title = d.titlePath.join(" \u203A ");
|
|
1382
|
+
const prefix = d.projectName !== void 0 && d.projectName !== "" ? `[${d.projectName}] \u203A ` : "";
|
|
1383
|
+
return `${prefix}${loc} \u203A ${title}`;
|
|
1384
|
+
});
|
|
1385
|
+
}
|
|
1386
|
+
function defaultWriteTempFile(content) {
|
|
1387
|
+
const path = (0, import_node_path.join)((0, import_node_os.tmpdir)(), `crvy-rprtr-test-list-${process.pid}-${Date.now()}.txt`);
|
|
1388
|
+
(0, import_node_fs.writeFileSync)(path, content, "utf8");
|
|
1389
|
+
return path;
|
|
1390
|
+
}
|
|
1391
|
+
function defaultDeleteTempFile(path) {
|
|
1392
|
+
try {
|
|
1393
|
+
(0, import_node_fs.unlinkSync)(path);
|
|
1394
|
+
} catch {
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
function resolveReporterDefault(cwd) {
|
|
1398
|
+
try {
|
|
1399
|
+
return (0, import_node_module.createRequire)((0, import_node_path.join)(cwd, "package.json")).resolve("@crvy/rprtr");
|
|
1400
|
+
} catch {
|
|
1401
|
+
}
|
|
1402
|
+
try {
|
|
1403
|
+
return (0, import_node_module.createRequire)(import_meta.url).resolve("@crvy/rprtr");
|
|
1404
|
+
} catch {
|
|
1405
|
+
return null;
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
function buildSpawnEnv(port) {
|
|
1409
|
+
const env = {};
|
|
1410
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
1411
|
+
if (key === "CI") continue;
|
|
1412
|
+
env[key] = value;
|
|
1413
|
+
}
|
|
1414
|
+
env.CRVY_RPRTR_SERVER_URL = `ws://localhost:${port}`;
|
|
1415
|
+
env.PLAYWRIGHT_HTML_OPEN = "never";
|
|
1416
|
+
return env;
|
|
1417
|
+
}
|
|
1418
|
+
var RunController = class {
|
|
1419
|
+
constructor(deps) {
|
|
1420
|
+
this.deps = deps;
|
|
1421
|
+
}
|
|
1422
|
+
child = null;
|
|
1423
|
+
sigkillTimer = null;
|
|
1424
|
+
testListPath = null;
|
|
1425
|
+
get isRunning() {
|
|
1426
|
+
return this.child !== null;
|
|
1427
|
+
}
|
|
1428
|
+
supportsTestList(cwd) {
|
|
1429
|
+
const getVersion = this.deps.getPlaywrightVersion ?? resolvePlaywrightVersion;
|
|
1430
|
+
const version = getVersion(cwd);
|
|
1431
|
+
return version !== null && gteMinor(version, 1, 56);
|
|
1432
|
+
}
|
|
1433
|
+
cleanupTempFile() {
|
|
1434
|
+
if (this.testListPath !== null) {
|
|
1435
|
+
const del = this.deps.deleteTempFile ?? defaultDeleteTempFile;
|
|
1436
|
+
del(this.testListPath);
|
|
1437
|
+
this.testListPath = null;
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
start(filters) {
|
|
1441
|
+
const ctx = this.deps.getRunContext();
|
|
1442
|
+
if (ctx === null) return { ok: false, reason: "no-config" };
|
|
1443
|
+
if (this.child !== null) return { ok: false, reason: "already-running" };
|
|
1444
|
+
if (filters.tests !== void 0 && filters.tests.length === 0) {
|
|
1445
|
+
return { ok: false, reason: "no-tests" };
|
|
1446
|
+
}
|
|
1447
|
+
const resolveReporter = this.deps.resolveReporter ?? resolveReporterDefault;
|
|
1448
|
+
const reporterModule = resolveReporter(ctx.cwd);
|
|
1449
|
+
const tests = filters.tests;
|
|
1450
|
+
const useTestList = tests !== void 0 && tests.length > 1 && this.supportsTestList(ctx.cwd);
|
|
1451
|
+
const args = ["test", "--config", ctx.configFile];
|
|
1452
|
+
if (reporterModule !== null) args.push("--reporter", reporterModule);
|
|
1453
|
+
if (useTestList && tests !== void 0) {
|
|
1454
|
+
const content = buildTestListEntries(tests).join("\n");
|
|
1455
|
+
const writeTemp = this.deps.writeTempFile ?? defaultWriteTempFile;
|
|
1456
|
+
this.testListPath = writeTemp(content);
|
|
1457
|
+
args.push("--test-list", this.testListPath);
|
|
1458
|
+
} else if (tests !== void 0 && tests.length > 0) {
|
|
1459
|
+
const project = sharedProject(tests);
|
|
1460
|
+
if (project !== void 0) args.push("--project", project);
|
|
1461
|
+
for (const d of tests) args.push(descriptorLocation(d));
|
|
1462
|
+
}
|
|
1463
|
+
const resolveLaunch = this.deps.resolveLaunch ?? resolvePlaywrightLaunch;
|
|
1464
|
+
const { cmd, args: launchArgs } = resolveLaunch(ctx.cwd, args);
|
|
1465
|
+
let child;
|
|
1466
|
+
try {
|
|
1467
|
+
child = this.deps.spawn(cmd, launchArgs, { cwd: ctx.cwd, env: buildSpawnEnv(this.deps.port), stdio: "inherit" });
|
|
1468
|
+
} catch (err) {
|
|
1469
|
+
this.cleanupTempFile();
|
|
1470
|
+
throw err;
|
|
1471
|
+
}
|
|
1472
|
+
this.child = child;
|
|
1473
|
+
child.on("exit", (code) => {
|
|
1474
|
+
this.handleChildExit(code);
|
|
1475
|
+
});
|
|
1476
|
+
child.on("error", () => {
|
|
1477
|
+
this.handleChildExit(null);
|
|
1478
|
+
});
|
|
1479
|
+
this.deps.setReportRunning(true);
|
|
1480
|
+
this.deps.setRunFiltered?.(filters.tests !== void 0);
|
|
1481
|
+
this.deps.broadcast({ type: "run-status", data: { running: true } });
|
|
1482
|
+
return { ok: true };
|
|
1483
|
+
}
|
|
1484
|
+
stop() {
|
|
1485
|
+
if (this.child === null) return { ok: false, reason: "not-running" };
|
|
1486
|
+
if (this.sigkillTimer !== null) this.deps.timers.clearTimeout(this.sigkillTimer);
|
|
1487
|
+
this.child.kill("SIGTERM");
|
|
1488
|
+
this.sigkillTimer = this.deps.timers.setTimeout(() => {
|
|
1489
|
+
if (this.child !== null) this.child.kill("SIGKILL");
|
|
1490
|
+
}, STOP_GRACE_MS);
|
|
1491
|
+
return { ok: true };
|
|
1492
|
+
}
|
|
1493
|
+
dispose() {
|
|
1494
|
+
if (this.child === null) return;
|
|
1495
|
+
if (this.sigkillTimer !== null) this.deps.timers.clearTimeout(this.sigkillTimer);
|
|
1496
|
+
this.sigkillTimer = null;
|
|
1497
|
+
this.child.kill("SIGKILL");
|
|
1498
|
+
this.cleanupTempFile();
|
|
1499
|
+
}
|
|
1500
|
+
handleChildExit(code) {
|
|
1501
|
+
if (this.child === null) return;
|
|
1502
|
+
if (this.sigkillTimer !== null) {
|
|
1503
|
+
this.deps.timers.clearTimeout(this.sigkillTimer);
|
|
1504
|
+
this.sigkillTimer = null;
|
|
1505
|
+
}
|
|
1506
|
+
this.child = null;
|
|
1507
|
+
this.cleanupTempFile();
|
|
1508
|
+
if (code !== null && code !== 0) {
|
|
1509
|
+
console.warn(`[RunController] playwright test exited with code ${code}`);
|
|
1510
|
+
}
|
|
1511
|
+
this.deps.setReportRunning(false);
|
|
1512
|
+
this.deps.broadcast({ type: "run-status", data: { running: false } });
|
|
1513
|
+
}
|
|
1514
|
+
};
|
|
1515
|
+
function createRealSpawn() {
|
|
1516
|
+
return (cmd, args, opts) => {
|
|
1517
|
+
const cp = (0, import_child_process.spawn)(cmd, args, opts);
|
|
1518
|
+
return {
|
|
1519
|
+
on: (event, cb) => {
|
|
1520
|
+
cp.on(event, cb);
|
|
1521
|
+
},
|
|
1522
|
+
kill: (signal) => {
|
|
1523
|
+
const sig = KNOWN_SIGNALS[signal];
|
|
1524
|
+
if (sig !== void 0) cp.kill(sig);
|
|
1525
|
+
}
|
|
1526
|
+
};
|
|
1527
|
+
};
|
|
1528
|
+
}
|
|
1529
|
+
function createRealTimers() {
|
|
1530
|
+
const pending = [];
|
|
1531
|
+
return {
|
|
1532
|
+
setTimeout: (fn, ms) => {
|
|
1533
|
+
const id = setTimeout(fn, ms);
|
|
1534
|
+
pending.push(id);
|
|
1535
|
+
return id;
|
|
1536
|
+
},
|
|
1537
|
+
clearTimeout: () => {
|
|
1538
|
+
for (const h of pending.splice(0)) clearTimeout(h);
|
|
1539
|
+
}
|
|
1540
|
+
};
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
// src/server/app.ts
|
|
1544
|
+
var import_meta2 = {};
|
|
1144
1545
|
function createReportData(options) {
|
|
1145
1546
|
return {
|
|
1146
1547
|
isRunning: false,
|
|
@@ -1166,37 +1567,6 @@ async function loadReport(reportPath, reportData) {
|
|
|
1166
1567
|
console.log("No report.json found, using empty state");
|
|
1167
1568
|
}
|
|
1168
1569
|
}
|
|
1169
|
-
async function readOfflineReport(filePath) {
|
|
1170
|
-
try {
|
|
1171
|
-
const raw = await readJsonFile(filePath);
|
|
1172
|
-
if (raw === null) {
|
|
1173
|
-
return null;
|
|
1174
|
-
}
|
|
1175
|
-
const parsed = parseOfflineReport(raw);
|
|
1176
|
-
if (parsed !== null) {
|
|
1177
|
-
console.log(`[Server] Loading offline report: ${filePath}`);
|
|
1178
|
-
return parsed;
|
|
1179
|
-
}
|
|
1180
|
-
} catch {
|
|
1181
|
-
}
|
|
1182
|
-
return null;
|
|
1183
|
-
}
|
|
1184
|
-
async function loadOfflineReports(offlineReportDir, reportData) {
|
|
1185
|
-
const offlineReportPaths = await findOfflineReportPaths(offlineReportDir);
|
|
1186
|
-
if (offlineReportPaths.length === 0) {
|
|
1187
|
-
return;
|
|
1188
|
-
}
|
|
1189
|
-
const limit = (0, import_p_limit.default)(MAX_CONCURRENT_FILE_OPS);
|
|
1190
|
-
const reports = await Promise.all(offlineReportPaths.map((filePath) => limit(() => readOfflineReport(filePath))));
|
|
1191
|
-
const validReports = reports.filter((report) => report !== null);
|
|
1192
|
-
if (validReports.length === 0) {
|
|
1193
|
-
return;
|
|
1194
|
-
}
|
|
1195
|
-
reportData.tests = mergeOfflineReportsIntoTests(reportData.tests, validReports, {
|
|
1196
|
-
screenshotDir: reportData.screenshotDir,
|
|
1197
|
-
screenshotsBaseUrl: "/screenshots/"
|
|
1198
|
-
});
|
|
1199
|
-
}
|
|
1200
1570
|
async function handleParsedWebSocketMessage(ctx, msg) {
|
|
1201
1571
|
switch (msg.type) {
|
|
1202
1572
|
case "test-begin": {
|
|
@@ -1260,7 +1630,7 @@ function createWebSocketMessageHandler(getHandlerContext) {
|
|
|
1260
1630
|
};
|
|
1261
1631
|
}
|
|
1262
1632
|
async function resolveStaticDir(staticDir) {
|
|
1263
|
-
const currentDir = (0, import_path8.dirname)((0, import_url.fileURLToPath)(
|
|
1633
|
+
const currentDir = (0, import_path8.dirname)((0, import_url.fileURLToPath)(import_meta2.url));
|
|
1264
1634
|
const candidates = staticDir === void 0 ? [
|
|
1265
1635
|
currentDir,
|
|
1266
1636
|
(0, import_path8.join)(currentDir, "dist"),
|
|
@@ -1287,23 +1657,29 @@ async function resolveReportPath(reportPath) {
|
|
|
1287
1657
|
}
|
|
1288
1658
|
return { reportFile: reportPath, offlineReportDir: (0, import_path8.dirname)(reportPath) };
|
|
1289
1659
|
}
|
|
1290
|
-
function
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
}
|
|
1306
|
-
|
|
1660
|
+
async function seedRunContext(routesContext, options) {
|
|
1661
|
+
if (routesContext.runContext !== void 0) {
|
|
1662
|
+
return;
|
|
1663
|
+
}
|
|
1664
|
+
const configFile = options.playwrightConfig ?? await resolvePlaywrightConfig(process.cwd());
|
|
1665
|
+
if (configFile !== null) {
|
|
1666
|
+
routesContext.runContext = { configFile, cwd: process.cwd() };
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
function createServerRunController(routesContext, wsClients, reportData, port, setRunFiltered) {
|
|
1670
|
+
return new RunController({
|
|
1671
|
+
getRunContext: () => routesContext.runContext ?? null,
|
|
1672
|
+
port,
|
|
1673
|
+
broadcast: (message) => {
|
|
1674
|
+
broadcastToBrowsers(wsClients, message);
|
|
1675
|
+
},
|
|
1676
|
+
setReportRunning: (running) => {
|
|
1677
|
+
reportData.isRunning = running;
|
|
1678
|
+
},
|
|
1679
|
+
setRunFiltered,
|
|
1680
|
+
spawn: createRealSpawn(),
|
|
1681
|
+
timers: createRealTimers()
|
|
1682
|
+
});
|
|
1307
1683
|
}
|
|
1308
1684
|
async function createServerApp(options = {}) {
|
|
1309
1685
|
const port = options.port ?? 3e3;
|
|
@@ -1313,26 +1689,32 @@ async function createServerApp(options = {}) {
|
|
|
1313
1689
|
const staticDir = await resolveStaticDir(options.staticDir);
|
|
1314
1690
|
const wsClients = /* @__PURE__ */ new Set();
|
|
1315
1691
|
const currentRunIds = /* @__PURE__ */ new Set();
|
|
1316
|
-
|
|
1317
|
-
await writeJsonFile(reportFile, reportData);
|
|
1318
|
-
}
|
|
1692
|
+
const saveReport = () => writeJsonFile(reportFile, reportData);
|
|
1319
1693
|
const routesContext = createRoutesContext(reportData, staticDir, saveReport, options);
|
|
1694
|
+
await seedRunContext(routesContext, options);
|
|
1695
|
+
let isFilteredRun = false;
|
|
1696
|
+
const runController = createServerRunController(routesContext, wsClients, reportData, port, (filtered) => {
|
|
1697
|
+
isFilteredRun = filtered;
|
|
1698
|
+
});
|
|
1320
1699
|
const getHandlerContext = () => ({
|
|
1321
1700
|
reportData,
|
|
1322
1701
|
wsClients,
|
|
1323
1702
|
currentRunIds,
|
|
1703
|
+
isFilteredRun,
|
|
1324
1704
|
saveReport,
|
|
1325
1705
|
approvalRouting: routesContext.approvalRouting,
|
|
1326
|
-
routesContext
|
|
1706
|
+
routesContext,
|
|
1707
|
+
runController
|
|
1327
1708
|
});
|
|
1328
|
-
const handleRequest = (req) => handleHttpRequest(routesContext, req);
|
|
1709
|
+
const handleRequest = (req) => handleHttpRequest(routesContext, req, runController);
|
|
1329
1710
|
const handleWebSocketMessage = createWebSocketMessageHandler(getHandlerContext);
|
|
1330
1711
|
await loadReport(reportFile, reportData);
|
|
1331
|
-
await loadOfflineReports(
|
|
1712
|
+
await loadOfflineReports(reportData, offlineReportDir);
|
|
1332
1713
|
return {
|
|
1333
1714
|
port,
|
|
1334
1715
|
wsClients,
|
|
1335
1716
|
close: () => {
|
|
1717
|
+
runController.dispose();
|
|
1336
1718
|
},
|
|
1337
1719
|
handleRequest,
|
|
1338
1720
|
handleWebSocketMessage
|