@crvy/rprtr 0.1.4 → 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 +61 -0
- package/README.md +8 -7
- package/dist/{chunk-WRMHUY5A.js → chunk-HAFWYUNO.js} +181 -130
- package/dist/{chunk-KLGJSEKV.js → chunk-RFZGJSL3.js} +439 -127
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +6 -4
- package/dist/index.css +26 -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/report-state.d.ts +3 -1
- package/dist/report-state.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 +186 -134
- 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/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 +633 -273
- 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
|
@@ -35,13 +35,13 @@ __export(server_exports, {
|
|
|
35
35
|
module.exports = __toCommonJS(server_exports);
|
|
36
36
|
|
|
37
37
|
// src/server/app.ts
|
|
38
|
-
var
|
|
38
|
+
var import_path8 = require("path");
|
|
39
39
|
var import_url = require("url");
|
|
40
|
-
var import_p_limit = __toESM(require("p-limit"), 1);
|
|
41
40
|
|
|
42
41
|
// src/offline-reports.ts
|
|
43
|
-
var
|
|
44
|
-
var
|
|
42
|
+
var import_promises2 = require("fs/promises");
|
|
43
|
+
var import_path2 = require("path");
|
|
44
|
+
var import_p_limit = __toESM(require("p-limit"), 1);
|
|
45
45
|
|
|
46
46
|
// src/path-utils.ts
|
|
47
47
|
var posixAbsolutePattern = /^\//u;
|
|
@@ -186,7 +186,12 @@ function createMutableReportState(screenshotDir = "./screenshots") {
|
|
|
186
186
|
function applyTestBeginEvent(state, data) {
|
|
187
187
|
const { id, title, titlePath, browser, projectName, location } = data;
|
|
188
188
|
state.currentRunIds.add(id);
|
|
189
|
-
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 = {
|
|
190
195
|
id,
|
|
191
196
|
titlePath: titlePath ?? [],
|
|
192
197
|
browser: browser ?? "",
|
|
@@ -198,7 +203,8 @@ function applyTestBeginEvent(state, data) {
|
|
|
198
203
|
location,
|
|
199
204
|
status: "running"
|
|
200
205
|
};
|
|
201
|
-
|
|
206
|
+
state.reportData.tests[id] = created;
|
|
207
|
+
return created;
|
|
202
208
|
}
|
|
203
209
|
function applyTestEndEvent(state, data, options = {}) {
|
|
204
210
|
const test = state.reportData.tests[data.id];
|
|
@@ -236,11 +242,13 @@ function applyTestEndEvent(state, data, options = {}) {
|
|
|
236
242
|
diffCount
|
|
237
243
|
};
|
|
238
244
|
}
|
|
239
|
-
function finalizeRunEvent(state) {
|
|
245
|
+
function finalizeRunEvent(state, options = {}) {
|
|
240
246
|
state.reportData.isRunning = false;
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
+
}
|
|
244
252
|
state.currentRunIds.clear();
|
|
245
253
|
const runTests = Object.values(state.reportData.tests).filter((test) => test !== void 0);
|
|
246
254
|
return {
|
|
@@ -251,169 +259,211 @@ function finalizeRunEvent(state) {
|
|
|
251
259
|
}
|
|
252
260
|
|
|
253
261
|
// src/schemas.ts
|
|
262
|
+
var import_zod2 = require("zod");
|
|
263
|
+
|
|
264
|
+
// src/schemas/http.ts
|
|
254
265
|
var import_zod = require("zod");
|
|
255
|
-
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({
|
|
256
272
|
file: import_zod.z.string(),
|
|
257
|
-
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())
|
|
258
277
|
});
|
|
259
|
-
var
|
|
260
|
-
|
|
261
|
-
actual: import_zod.z.string().optional(),
|
|
262
|
-
expect: import_zod.z.string().optional(),
|
|
263
|
-
diff: import_zod.z.string().optional(),
|
|
264
|
-
error: import_zod.z.string().optional(),
|
|
265
|
-
source: VisualSourceSchema.optional()
|
|
278
|
+
var RunRequestBodySchema = import_zod.z.object({
|
|
279
|
+
tests: import_zod.z.array(RunTestDescriptorSchema).optional()
|
|
266
280
|
});
|
|
267
|
-
var
|
|
281
|
+
var RunResponseSchema = import_zod.z.discriminatedUnion("ok", [
|
|
282
|
+
import_zod.z.object({ ok: import_zod.z.literal(true) }),
|
|
268
283
|
import_zod.z.object({
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
}),
|
|
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) }),
|
|
275
290
|
import_zod.z.object({
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
occurrenceIndex: import_zod.z.number()
|
|
291
|
+
ok: import_zod.z.literal(false),
|
|
292
|
+
reason: import_zod.z.literal("not-running")
|
|
279
293
|
})
|
|
280
294
|
]);
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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()
|
|
285
309
|
});
|
|
286
|
-
var
|
|
287
|
-
|
|
288
|
-
|
|
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()
|
|
322
|
+
})
|
|
323
|
+
]);
|
|
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()
|
|
328
|
+
});
|
|
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({
|
|
289
332
|
status: TestResultStatusSchema,
|
|
290
|
-
retries:
|
|
291
|
-
images:
|
|
292
|
-
visualDeclarations:
|
|
293
|
-
error:
|
|
294
|
-
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()
|
|
295
338
|
});
|
|
296
|
-
var TestDataSchema =
|
|
297
|
-
id:
|
|
298
|
-
titlePath:
|
|
299
|
-
browser:
|
|
300
|
-
projectName:
|
|
301
|
-
title:
|
|
302
|
-
skip:
|
|
303
|
-
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(),
|
|
304
347
|
status: TestStatusSchema.optional(),
|
|
305
|
-
results:
|
|
306
|
-
approved:
|
|
307
|
-
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(),
|
|
308
351
|
location: LocationSchema.optional()
|
|
309
352
|
});
|
|
310
353
|
var CrvyRprtrTestSchema = TestDataSchema.extend({
|
|
311
|
-
checked:
|
|
354
|
+
checked: import_zod2.z.boolean()
|
|
312
355
|
});
|
|
313
|
-
var CrvyRprtrSuiteSchema =
|
|
314
|
-
() =>
|
|
315
|
-
path:
|
|
316
|
-
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(),
|
|
317
360
|
status: TestStatusSchema.optional(),
|
|
318
|
-
opened:
|
|
319
|
-
checked:
|
|
320
|
-
indeterminate:
|
|
321
|
-
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()
|
|
322
365
|
})
|
|
323
366
|
);
|
|
324
|
-
var IncomingWebSocketMessageSchema =
|
|
325
|
-
type:
|
|
326
|
-
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()
|
|
327
370
|
});
|
|
328
|
-
var WebSocketMessageSchema =
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
type:
|
|
333
|
-
data:
|
|
334
|
-
status:
|
|
335
|
-
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())
|
|
336
379
|
})
|
|
337
380
|
}),
|
|
338
|
-
|
|
339
|
-
type:
|
|
340
|
-
data:
|
|
341
|
-
tests:
|
|
342
|
-
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()
|
|
343
386
|
})
|
|
344
387
|
}),
|
|
345
|
-
|
|
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
|
+
})
|
|
346
395
|
]);
|
|
347
|
-
var TestBeginDataSchema =
|
|
348
|
-
id:
|
|
349
|
-
title:
|
|
350
|
-
titlePath:
|
|
351
|
-
browser:
|
|
352
|
-
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(),
|
|
353
402
|
location: LocationSchema
|
|
354
403
|
});
|
|
355
|
-
var TestEndDataSchema =
|
|
356
|
-
id:
|
|
357
|
-
status:
|
|
358
|
-
attachments:
|
|
359
|
-
visualNames:
|
|
360
|
-
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(
|
|
361
410
|
(value) => value === null ? void 0 : value,
|
|
362
|
-
|
|
411
|
+
import_zod2.z.array(ScreenshotDeclarationSchema).optional()
|
|
363
412
|
),
|
|
364
|
-
error:
|
|
365
|
-
duration:
|
|
413
|
+
error: import_zod2.z.string().optional(),
|
|
414
|
+
duration: import_zod2.z.number().optional()
|
|
366
415
|
});
|
|
367
|
-
var RunEndDataSchema =
|
|
368
|
-
status:
|
|
416
|
+
var RunEndDataSchema = import_zod2.z.object({
|
|
417
|
+
status: import_zod2.z.enum(["passed", "failed", "skipped"])
|
|
369
418
|
});
|
|
370
|
-
var RegisterDataSchema =
|
|
371
|
-
playwrightSnapshotDir:
|
|
372
|
-
playwrightTestDir:
|
|
373
|
-
playwrightSnapshotPathTemplate:
|
|
374
|
-
playwrightToHaveScreenshotPathTemplate:
|
|
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()
|
|
375
426
|
});
|
|
376
|
-
var ReportDataSchema =
|
|
377
|
-
isRunning:
|
|
378
|
-
tests:
|
|
379
|
-
browsers:
|
|
380
|
-
isUpdateMode:
|
|
381
|
-
screenshotDir:
|
|
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()
|
|
382
433
|
});
|
|
383
|
-
var LoadedReportDataSchema =
|
|
384
|
-
tests:
|
|
385
|
-
isUpdateMode:
|
|
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()
|
|
386
437
|
});
|
|
387
|
-
var OfflineEventSchema =
|
|
388
|
-
type:
|
|
389
|
-
data:
|
|
390
|
-
timestamp:
|
|
391
|
-
workerIndex:
|
|
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()
|
|
392
443
|
});
|
|
393
|
-
var OfflineReportSchema =
|
|
394
|
-
version:
|
|
395
|
-
generatedAt:
|
|
396
|
-
workers:
|
|
397
|
-
events:
|
|
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)
|
|
398
449
|
});
|
|
399
|
-
var
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
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()
|
|
403
455
|
});
|
|
404
|
-
var
|
|
405
|
-
tests: import_zod.z.record(import_zod.z.string(), TestDataSchema),
|
|
406
|
-
isUpdateMode: import_zod.z.boolean().optional()
|
|
407
|
-
});
|
|
408
|
-
var ClientBootstrapDataSchema = import_zod.z.object({
|
|
456
|
+
var ClientBootstrapDataSchema = import_zod2.z.object({
|
|
409
457
|
report: ReportApiResponseSchema.extend({
|
|
410
|
-
isUpdateMode:
|
|
458
|
+
isUpdateMode: import_zod2.z.boolean()
|
|
411
459
|
}),
|
|
412
|
-
liveUpdates:
|
|
413
|
-
approvalEnabled:
|
|
414
|
-
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()
|
|
415
465
|
});
|
|
416
|
-
var ImagesViewModeSchema =
|
|
466
|
+
var ImagesViewModeSchema = import_zod2.z.enum(["side-by-side", "swap", "slide", "blend"]);
|
|
417
467
|
function safeParse(schema, data) {
|
|
418
468
|
const result = schema.safeParse(data);
|
|
419
469
|
if (result.success) {
|
|
@@ -422,70 +472,15 @@ function safeParse(schema, data) {
|
|
|
422
472
|
return null;
|
|
423
473
|
}
|
|
424
474
|
|
|
425
|
-
// src/offline-reports.ts
|
|
426
|
-
var OFFLINE_REPORT_FILE_PATTERN = /^crvy-rprtr(?:-\d+)?\.json$/;
|
|
427
|
-
async function findOfflineReportPaths(searchDir) {
|
|
428
|
-
try {
|
|
429
|
-
const entries = await (0, import_promises.readdir)(searchDir);
|
|
430
|
-
return entries.filter((entry) => OFFLINE_REPORT_FILE_PATTERN.test(entry)).sort((left, right) => left.localeCompare(right)).map((entry) => (0, import_path.join)(searchDir, entry));
|
|
431
|
-
} catch (error) {
|
|
432
|
-
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
433
|
-
return [];
|
|
434
|
-
}
|
|
435
|
-
throw error;
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
function parseOfflineReport(value) {
|
|
439
|
-
const parsed = safeParse(OfflineReportSchema, value);
|
|
440
|
-
if (parsed === null || parsed.version !== 1 || !Array.isArray(parsed.events)) {
|
|
441
|
-
return null;
|
|
442
|
-
}
|
|
443
|
-
return parsed;
|
|
444
|
-
}
|
|
445
|
-
function mergeOfflineReportsIntoTests(existingTests, offlineReports, options = {}) {
|
|
446
|
-
const state = createMutableReportState(options.screenshotDir);
|
|
447
|
-
let shouldFinalize = false;
|
|
448
|
-
for (const report of offlineReports) {
|
|
449
|
-
for (const event of report.events) {
|
|
450
|
-
switch (event.type) {
|
|
451
|
-
case "test-begin": {
|
|
452
|
-
const parsed = safeParse(TestBeginDataSchema, event.data);
|
|
453
|
-
if (parsed !== null) {
|
|
454
|
-
applyTestBeginEvent(state, parsed);
|
|
455
|
-
}
|
|
456
|
-
break;
|
|
457
|
-
}
|
|
458
|
-
case "test-end": {
|
|
459
|
-
const parsed = safeParse(TestEndDataSchema, event.data);
|
|
460
|
-
if (parsed !== null) {
|
|
461
|
-
applyTestEndEvent(state, parsed, { screenshotsBaseUrl: options.screenshotsBaseUrl });
|
|
462
|
-
}
|
|
463
|
-
break;
|
|
464
|
-
}
|
|
465
|
-
case "run-end":
|
|
466
|
-
shouldFinalize = true;
|
|
467
|
-
break;
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
if (shouldFinalize) {
|
|
472
|
-
finalizeRunEvent(state);
|
|
473
|
-
}
|
|
474
|
-
return {
|
|
475
|
-
...existingTests,
|
|
476
|
-
...state.reportData.tests
|
|
477
|
-
};
|
|
478
|
-
}
|
|
479
|
-
|
|
480
475
|
// src/server/file-utils.ts
|
|
481
|
-
var
|
|
482
|
-
var
|
|
476
|
+
var import_promises = require("fs/promises");
|
|
477
|
+
var import_path = require("path");
|
|
483
478
|
function isFileNotFound(error) {
|
|
484
479
|
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
485
480
|
}
|
|
486
481
|
async function fileExists(filePath) {
|
|
487
482
|
try {
|
|
488
|
-
await (0,
|
|
483
|
+
await (0, import_promises.access)(filePath);
|
|
489
484
|
return true;
|
|
490
485
|
} catch (error) {
|
|
491
486
|
if (isFileNotFound(error)) {
|
|
@@ -496,7 +491,7 @@ async function fileExists(filePath) {
|
|
|
496
491
|
}
|
|
497
492
|
async function isDirectory(filePath) {
|
|
498
493
|
try {
|
|
499
|
-
const stats = await (0,
|
|
494
|
+
const stats = await (0, import_promises.stat)(filePath);
|
|
500
495
|
return stats.isDirectory();
|
|
501
496
|
} catch (error) {
|
|
502
497
|
if (isFileNotFound(error)) {
|
|
@@ -507,7 +502,7 @@ async function isDirectory(filePath) {
|
|
|
507
502
|
}
|
|
508
503
|
async function readJsonFile(filePath) {
|
|
509
504
|
try {
|
|
510
|
-
const raw = await (0,
|
|
505
|
+
const raw = await (0, import_promises.readFile)(filePath, "utf8");
|
|
511
506
|
if (raw.trim() === "") {
|
|
512
507
|
return null;
|
|
513
508
|
}
|
|
@@ -520,16 +515,16 @@ async function readJsonFile(filePath) {
|
|
|
520
515
|
}
|
|
521
516
|
}
|
|
522
517
|
async function writeJsonFile(filePath, value) {
|
|
523
|
-
await (0,
|
|
524
|
-
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)}
|
|
525
520
|
`);
|
|
526
521
|
}
|
|
527
522
|
async function copyFilePortable(sourcePath, destinationPath) {
|
|
528
|
-
await (0,
|
|
529
|
-
await (0,
|
|
523
|
+
await (0, import_promises.mkdir)((0, import_path.dirname)(destinationPath), { recursive: true });
|
|
524
|
+
await (0, import_promises.copyFile)(sourcePath, destinationPath);
|
|
530
525
|
}
|
|
531
526
|
function inferContentType(filePath) {
|
|
532
|
-
switch ((0,
|
|
527
|
+
switch ((0, import_path.extname)(filePath).toLowerCase()) {
|
|
533
528
|
case ".css":
|
|
534
529
|
return "text/css";
|
|
535
530
|
case ".gif":
|
|
@@ -557,7 +552,7 @@ function inferContentType(filePath) {
|
|
|
557
552
|
}
|
|
558
553
|
async function respondWithFile(filePath, contentType) {
|
|
559
554
|
try {
|
|
560
|
-
const file = await (0,
|
|
555
|
+
const file = await (0, import_promises.readFile)(filePath);
|
|
561
556
|
const resolvedContentType = contentType ?? inferContentType(filePath);
|
|
562
557
|
const headers = { "X-Content-Type-Options": "nosniff" };
|
|
563
558
|
if (resolvedContentType !== void 0) {
|
|
@@ -572,6 +567,93 @@ async function respondWithFile(filePath, contentType) {
|
|
|
572
567
|
}
|
|
573
568
|
}
|
|
574
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
|
+
|
|
575
657
|
// src/server/handlers.ts
|
|
576
658
|
var import_fs2 = require("fs");
|
|
577
659
|
|
|
@@ -914,8 +996,8 @@ function handleTestEnd(ctx, data) {
|
|
|
914
996
|
broadcastToBrowsers(ctx.wsClients, message);
|
|
915
997
|
}
|
|
916
998
|
async function handleRunEnd(ctx, data) {
|
|
917
|
-
const removedTestIds = Object.keys(ctx.reportData.tests).filter((id) => !ctx.currentRunIds.has(id));
|
|
918
|
-
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 });
|
|
919
1001
|
await ctx.saveReport();
|
|
920
1002
|
console.log(`
|
|
921
1003
|
Run complete \u2014 ${passed} passed, ${failed} failed, ${pending} skipped`);
|
|
@@ -968,16 +1050,93 @@ function handleRegister(ctx, data) {
|
|
|
968
1050
|
ctx.routesContext.approvalRouting.playwrightToHaveScreenshotPathTemplate = data.playwrightToHaveScreenshotPathTemplate;
|
|
969
1051
|
}
|
|
970
1052
|
}
|
|
1053
|
+
if (data.configFile !== void 0 && data.cwd !== void 0) {
|
|
1054
|
+
ctx.routesContext.runContext = { configFile: data.configFile, cwd: data.cwd };
|
|
1055
|
+
}
|
|
971
1056
|
console.log("[Server] Reporter registered with config:", {
|
|
972
1057
|
playwrightSnapshotDir: data.playwrightSnapshotDir,
|
|
973
1058
|
playwrightTestDir: data.playwrightTestDir
|
|
974
1059
|
});
|
|
975
1060
|
}
|
|
976
1061
|
|
|
977
|
-
// src/server/
|
|
1062
|
+
// src/server/playwright-config.ts
|
|
978
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
|
+
|
|
1103
|
+
// src/server/routes.ts
|
|
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
|
|
979
1138
|
async function handleRoot(ctx) {
|
|
980
|
-
const html = await respondWithFile((0,
|
|
1139
|
+
const html = await respondWithFile((0, import_path7.join)(ctx.staticDir, "index.html"), "text/html");
|
|
981
1140
|
return html ?? new Response("Not Found", { status: 404 });
|
|
982
1141
|
}
|
|
983
1142
|
async function handleAppCss() {
|
|
@@ -992,12 +1151,15 @@ async function handleSrcFiles(req) {
|
|
|
992
1151
|
return file ?? new Response("Not Found", { status: 404 });
|
|
993
1152
|
}
|
|
994
1153
|
function handleApiReport(ctx) {
|
|
995
|
-
return Response.json(
|
|
1154
|
+
return Response.json({
|
|
1155
|
+
...ctx.reportData,
|
|
1156
|
+
runEnabled: ctx.runContext !== void 0
|
|
1157
|
+
});
|
|
996
1158
|
}
|
|
997
1159
|
var APPROVAL_TARGET_ERROR = "Could not resolve approval target";
|
|
998
1160
|
function actualPathFromUrl(ctx, actualUrl) {
|
|
999
1161
|
if (actualUrl.startsWith("/screenshots/")) {
|
|
1000
|
-
return (0,
|
|
1162
|
+
return (0, import_path7.join)(ctx.reportData.screenshotDir, actualUrl.slice("/screenshots/".length));
|
|
1001
1163
|
}
|
|
1002
1164
|
if (actualUrl.startsWith("/file/")) {
|
|
1003
1165
|
return decodeURIComponent(actualUrl.slice("/file/".length));
|
|
@@ -1119,12 +1281,12 @@ async function handleScreenshots(ctx, req) {
|
|
|
1119
1281
|
}
|
|
1120
1282
|
async function handleDist(ctx, req) {
|
|
1121
1283
|
const path = new URL(req.url).pathname.slice("/dist/".length);
|
|
1122
|
-
const filePath = (0,
|
|
1284
|
+
const filePath = (0, import_path7.join)(ctx.staticDir, path);
|
|
1123
1285
|
const contentType = filePath.endsWith(".css") ? "text/css" : filePath.endsWith(".js") ? "application/javascript" : filePath.endsWith(".svelte") ? "text/plain" : "application/octet-stream";
|
|
1124
1286
|
const file = await respondWithFile(filePath, contentType);
|
|
1125
1287
|
return file ?? new Response("Not Found", { status: 404 });
|
|
1126
1288
|
}
|
|
1127
|
-
function handleHttpRequest(ctx, req) {
|
|
1289
|
+
function handleHttpRequest(ctx, req, runController) {
|
|
1128
1290
|
const pathname = new URL(req.url).pathname;
|
|
1129
1291
|
if (pathname === "/") {
|
|
1130
1292
|
return handleRoot(ctx);
|
|
@@ -1144,6 +1306,10 @@ function handleHttpRequest(ctx, req) {
|
|
|
1144
1306
|
if (pathname === "/api/approve-all" && req.method === "POST") {
|
|
1145
1307
|
return handleApiApproveAll(ctx);
|
|
1146
1308
|
}
|
|
1309
|
+
const runResponse = handleRunRoutes(pathname, req.method, runController, req);
|
|
1310
|
+
if (runResponse !== null) {
|
|
1311
|
+
return runResponse;
|
|
1312
|
+
}
|
|
1147
1313
|
if (pathname.startsWith("/api/images/")) {
|
|
1148
1314
|
return handleApiImages(req);
|
|
1149
1315
|
}
|
|
@@ -1160,9 +1326,222 @@ function handleHttpRequest(ctx, req) {
|
|
|
1160
1326
|
return Promise.resolve(new Response("Not Found", { status: 404 }));
|
|
1161
1327
|
}
|
|
1162
1328
|
|
|
1163
|
-
// 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");
|
|
1164
1337
|
var import_meta = { url: require("url").pathToFileURL(__filename).href };
|
|
1165
|
-
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 = {};
|
|
1166
1545
|
function createReportData(options) {
|
|
1167
1546
|
return {
|
|
1168
1547
|
isRunning: false,
|
|
@@ -1188,37 +1567,6 @@ async function loadReport(reportPath, reportData) {
|
|
|
1188
1567
|
console.log("No report.json found, using empty state");
|
|
1189
1568
|
}
|
|
1190
1569
|
}
|
|
1191
|
-
async function readOfflineReport(filePath) {
|
|
1192
|
-
try {
|
|
1193
|
-
const raw = await readJsonFile(filePath);
|
|
1194
|
-
if (raw === null) {
|
|
1195
|
-
return null;
|
|
1196
|
-
}
|
|
1197
|
-
const parsed = parseOfflineReport(raw);
|
|
1198
|
-
if (parsed !== null) {
|
|
1199
|
-
console.log(`[Server] Loading offline report: ${filePath}`);
|
|
1200
|
-
return parsed;
|
|
1201
|
-
}
|
|
1202
|
-
} catch {
|
|
1203
|
-
}
|
|
1204
|
-
return null;
|
|
1205
|
-
}
|
|
1206
|
-
async function loadOfflineReports(offlineReportDir, reportData) {
|
|
1207
|
-
const offlineReportPaths = await findOfflineReportPaths(offlineReportDir);
|
|
1208
|
-
if (offlineReportPaths.length === 0) {
|
|
1209
|
-
return;
|
|
1210
|
-
}
|
|
1211
|
-
const limit = (0, import_p_limit.default)(MAX_CONCURRENT_FILE_OPS);
|
|
1212
|
-
const reports = await Promise.all(offlineReportPaths.map((filePath) => limit(() => readOfflineReport(filePath))));
|
|
1213
|
-
const validReports = reports.filter((report) => report !== null);
|
|
1214
|
-
if (validReports.length === 0) {
|
|
1215
|
-
return;
|
|
1216
|
-
}
|
|
1217
|
-
reportData.tests = mergeOfflineReportsIntoTests(reportData.tests, validReports, {
|
|
1218
|
-
screenshotDir: reportData.screenshotDir,
|
|
1219
|
-
screenshotsBaseUrl: "/screenshots/"
|
|
1220
|
-
});
|
|
1221
|
-
}
|
|
1222
1570
|
async function handleParsedWebSocketMessage(ctx, msg) {
|
|
1223
1571
|
switch (msg.type) {
|
|
1224
1572
|
case "test-begin": {
|
|
@@ -1282,19 +1630,19 @@ function createWebSocketMessageHandler(getHandlerContext) {
|
|
|
1282
1630
|
};
|
|
1283
1631
|
}
|
|
1284
1632
|
async function resolveStaticDir(staticDir) {
|
|
1285
|
-
const currentDir = (0,
|
|
1633
|
+
const currentDir = (0, import_path8.dirname)((0, import_url.fileURLToPath)(import_meta2.url));
|
|
1286
1634
|
const candidates = staticDir === void 0 ? [
|
|
1287
1635
|
currentDir,
|
|
1288
|
-
(0,
|
|
1289
|
-
(0,
|
|
1290
|
-
(0,
|
|
1291
|
-
(0,
|
|
1292
|
-
(0,
|
|
1293
|
-
] : [staticDir, (0,
|
|
1636
|
+
(0, import_path8.join)(currentDir, "dist"),
|
|
1637
|
+
(0, import_path8.join)(currentDir, "..", "dist"),
|
|
1638
|
+
(0, import_path8.join)(currentDir, "..", "..", "dist"),
|
|
1639
|
+
(0, import_path8.join)(currentDir, ".."),
|
|
1640
|
+
(0, import_path8.join)(currentDir, "..", "..")
|
|
1641
|
+
] : [staticDir, (0, import_path8.join)(staticDir, "dist")];
|
|
1294
1642
|
const resolvedCandidates = await Promise.all(
|
|
1295
1643
|
candidates.map(async (candidate) => ({
|
|
1296
1644
|
candidate,
|
|
1297
|
-
exists: await fileExists((0,
|
|
1645
|
+
exists: await fileExists((0, import_path8.join)(candidate, "index.html"))
|
|
1298
1646
|
}))
|
|
1299
1647
|
);
|
|
1300
1648
|
const resolved = resolvedCandidates.find(({ exists }) => exists);
|
|
@@ -1305,27 +1653,33 @@ async function resolveStaticDir(staticDir) {
|
|
|
1305
1653
|
}
|
|
1306
1654
|
async function resolveReportPath(reportPath) {
|
|
1307
1655
|
if (await isDirectory(reportPath)) {
|
|
1308
|
-
return { reportFile: (0,
|
|
1656
|
+
return { reportFile: (0, import_path8.join)(reportPath, "report.json"), offlineReportDir: reportPath };
|
|
1309
1657
|
}
|
|
1310
|
-
return { reportFile: reportPath, offlineReportDir: (0,
|
|
1658
|
+
return { reportFile: reportPath, offlineReportDir: (0, import_path8.dirname)(reportPath) };
|
|
1311
1659
|
}
|
|
1312
|
-
function
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
}
|
|
1328
|
-
|
|
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
|
+
});
|
|
1329
1683
|
}
|
|
1330
1684
|
async function createServerApp(options = {}) {
|
|
1331
1685
|
const port = options.port ?? 3e3;
|
|
@@ -1335,26 +1689,32 @@ async function createServerApp(options = {}) {
|
|
|
1335
1689
|
const staticDir = await resolveStaticDir(options.staticDir);
|
|
1336
1690
|
const wsClients = /* @__PURE__ */ new Set();
|
|
1337
1691
|
const currentRunIds = /* @__PURE__ */ new Set();
|
|
1338
|
-
|
|
1339
|
-
await writeJsonFile(reportFile, reportData);
|
|
1340
|
-
}
|
|
1692
|
+
const saveReport = () => writeJsonFile(reportFile, reportData);
|
|
1341
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
|
+
});
|
|
1342
1699
|
const getHandlerContext = () => ({
|
|
1343
1700
|
reportData,
|
|
1344
1701
|
wsClients,
|
|
1345
1702
|
currentRunIds,
|
|
1703
|
+
isFilteredRun,
|
|
1346
1704
|
saveReport,
|
|
1347
1705
|
approvalRouting: routesContext.approvalRouting,
|
|
1348
|
-
routesContext
|
|
1706
|
+
routesContext,
|
|
1707
|
+
runController
|
|
1349
1708
|
});
|
|
1350
|
-
const handleRequest = (req) => handleHttpRequest(routesContext, req);
|
|
1709
|
+
const handleRequest = (req) => handleHttpRequest(routesContext, req, runController);
|
|
1351
1710
|
const handleWebSocketMessage = createWebSocketMessageHandler(getHandlerContext);
|
|
1352
1711
|
await loadReport(reportFile, reportData);
|
|
1353
|
-
await loadOfflineReports(
|
|
1712
|
+
await loadOfflineReports(reportData, offlineReportDir);
|
|
1354
1713
|
return {
|
|
1355
1714
|
port,
|
|
1356
1715
|
wsClients,
|
|
1357
1716
|
close: () => {
|
|
1717
|
+
runController.dispose();
|
|
1358
1718
|
},
|
|
1359
1719
|
handleRequest,
|
|
1360
1720
|
handleWebSocketMessage
|