@crvy/rprtr 0.2.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -0
- package/README.md +33 -0
- package/dist/{chunk-HAFWYUNO.js → chunk-4YJL655E.js} +16 -4
- package/dist/{chunk-473CWZ4V.js → chunk-IFHYWPXG.js} +710 -281
- package/dist/cli.d.ts +7 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +57 -6
- package/dist/index.css +7 -0
- package/dist/index.js +152 -105
- package/dist/reporter-artifact-ops.d.ts +0 -2
- package/dist/reporter-artifact-ops.d.ts.map +1 -1
- package/dist/reporter.cjs +32 -38
- package/dist/reporter.d.ts +2 -1
- package/dist/reporter.d.ts.map +1 -1
- package/dist/reporter.js +27 -43
- package/dist/schemas/http.d.ts +2 -0
- package/dist/schemas/http.d.ts.map +1 -1
- package/dist/schemas.d.ts +14 -0
- 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/docker-launcher.d.ts +27 -0
- package/dist/server/docker-launcher.d.ts.map +1 -0
- package/dist/server/docker-support.d.ts +66 -0
- package/dist/server/docker-support.d.ts.map +1 -0
- package/dist/server/handlers.d.ts +1 -1
- package/dist/server/handlers.d.ts.map +1 -1
- package/dist/server/launcher-resolver.d.ts +23 -0
- package/dist/server/launcher-resolver.d.ts.map +1 -0
- package/dist/server/playwright-config.d.ts +6 -0
- package/dist/server/playwright-config.d.ts.map +1 -1
- package/dist/server/routes-context.d.ts +6 -1
- package/dist/server/routes-context.d.ts.map +1 -1
- package/dist/server/routes.d.ts +7 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/run-controller.d.ts +19 -19
- package/dist/server/run-controller.d.ts.map +1 -1
- package/dist/server/run-launcher.d.ts +45 -0
- package/dist/server/run-launcher.d.ts.map +1 -0
- package/dist/server/run-mode.d.ts +15 -0
- package/dist/server/run-mode.d.ts.map +1 -0
- package/dist/server/server-factories.d.ts +21 -0
- package/dist/server/server-factories.d.ts.map +1 -0
- package/dist/server.cjs +885 -446
- package/dist/server.js +2 -2
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -12,13 +12,14 @@ import {
|
|
|
12
12
|
applyTestEndEvent,
|
|
13
13
|
createMutableReportState,
|
|
14
14
|
finalizeRunEvent,
|
|
15
|
+
isCI,
|
|
15
16
|
isForeignAbsolutePath,
|
|
16
17
|
resolveBaselineTargets,
|
|
17
18
|
safeParse
|
|
18
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-4YJL655E.js";
|
|
19
20
|
|
|
20
21
|
// src/server/app.ts
|
|
21
|
-
import { dirname as
|
|
22
|
+
import { dirname as dirname4, join as join6 } from "path";
|
|
22
23
|
import { fileURLToPath } from "url";
|
|
23
24
|
|
|
24
25
|
// src/offline-reports.ts
|
|
@@ -208,8 +209,544 @@ function mergeOfflineReportsIntoTests(existingTests, offlineReports, options = {
|
|
|
208
209
|
};
|
|
209
210
|
}
|
|
210
211
|
|
|
212
|
+
// src/server/docker-support.ts
|
|
213
|
+
import { spawn } from "child_process";
|
|
214
|
+
import { readFileSync } from "node:fs";
|
|
215
|
+
import { createRequire } from "node:module";
|
|
216
|
+
import { isAbsolute, join as join2, relative, sep } from "node:path";
|
|
217
|
+
import { detect } from "package-manager-detector/detect";
|
|
218
|
+
function createDockerExec() {
|
|
219
|
+
return (args) => new Promise((resolve5, reject) => {
|
|
220
|
+
const child = spawn("docker", args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
221
|
+
const stdout = [];
|
|
222
|
+
const stderr = [];
|
|
223
|
+
child.stdout.on("data", (chunk) => stdout.push(chunk));
|
|
224
|
+
child.stderr.on("data", (chunk) => stderr.push(chunk));
|
|
225
|
+
child.on("error", reject);
|
|
226
|
+
child.on("close", (code) => {
|
|
227
|
+
resolve5({
|
|
228
|
+
exitCode: code ?? 1,
|
|
229
|
+
stdout: Buffer.concat(stdout).toString(),
|
|
230
|
+
stderr: Buffer.concat(stderr).toString()
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
async function probeDockerDaemon(exec) {
|
|
236
|
+
try {
|
|
237
|
+
const result = await exec(["info"]);
|
|
238
|
+
return result.exitCode === 0;
|
|
239
|
+
} catch {
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
async function isDockerImagePresent(exec, image) {
|
|
244
|
+
try {
|
|
245
|
+
const result = await exec(["image", "inspect", image]);
|
|
246
|
+
return result.exitCode === 0;
|
|
247
|
+
} catch {
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
async function pullDockerImage(exec, image) {
|
|
252
|
+
try {
|
|
253
|
+
const result = await exec(["pull", image]);
|
|
254
|
+
return result.exitCode === 0;
|
|
255
|
+
} catch {
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
async function forceRemoveContainer(exec, name) {
|
|
260
|
+
try {
|
|
261
|
+
await exec(["rm", "-f", name]);
|
|
262
|
+
} catch {
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
function resolveDockerImage(options) {
|
|
266
|
+
if (options.image !== void 0 && options.image !== "") return options.image;
|
|
267
|
+
if (options.version === null) return null;
|
|
268
|
+
return `mcr.microsoft.com/playwright:v${options.version}-noble`;
|
|
269
|
+
}
|
|
270
|
+
var CONTAINER_INVOKERS = {
|
|
271
|
+
npm: ["npx"],
|
|
272
|
+
pnpm: ["pnpm", "exec"],
|
|
273
|
+
yarn: ["yarn"],
|
|
274
|
+
bun: ["bunx"]
|
|
275
|
+
};
|
|
276
|
+
var DEFAULT_CONTAINER_COMMAND = ["npx"];
|
|
277
|
+
function resolveContainerCommand(input) {
|
|
278
|
+
if (input.command !== void 0 && input.command.length > 0) return input.command;
|
|
279
|
+
if (!input.hasCustomImage) return [...DEFAULT_CONTAINER_COMMAND];
|
|
280
|
+
if (input.detectedAgentName === void 0 || input.detectedAgentName === null) {
|
|
281
|
+
input.warn?.("Could not detect a package manager for the custom docker image; falling back to npx.");
|
|
282
|
+
return [...DEFAULT_CONTAINER_COMMAND];
|
|
283
|
+
}
|
|
284
|
+
const invoker = CONTAINER_INVOKERS[input.detectedAgentName];
|
|
285
|
+
if (invoker === void 0) {
|
|
286
|
+
input.warn?.(`Package manager "${input.detectedAgentName}" is not supported in docker mode; falling back to npx.`);
|
|
287
|
+
return [...DEFAULT_CONTAINER_COMMAND];
|
|
288
|
+
}
|
|
289
|
+
return [...invoker];
|
|
290
|
+
}
|
|
291
|
+
var detectProjectAgent = async (cwd) => {
|
|
292
|
+
try {
|
|
293
|
+
const result = await detect({ cwd, strategies: ["lockfile", "packageManager-field"] });
|
|
294
|
+
return result === null ? null : { name: result.name, agent: result.agent };
|
|
295
|
+
} catch {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
function rewriteContainerPath(path, mapping) {
|
|
300
|
+
if (path === mapping.from) return mapping.to;
|
|
301
|
+
if (path.startsWith(`${mapping.from}/`)) return mapping.to + path.slice(mapping.from.length);
|
|
302
|
+
return path;
|
|
303
|
+
}
|
|
304
|
+
function resolvePlaywrightVersion(cwd) {
|
|
305
|
+
try {
|
|
306
|
+
const req = createRequire(join2(cwd, "package.json"));
|
|
307
|
+
const pkgPath = req.resolve("@playwright/test/package.json");
|
|
308
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
309
|
+
return typeof pkg === "object" && pkg !== null && "version" in pkg && typeof pkg.version === "string" ? pkg.version : null;
|
|
310
|
+
} catch {
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
function rewriteContainerTestDescriptors(tests, mapping) {
|
|
315
|
+
if (tests === void 0 || mapping === void 0) return tests;
|
|
316
|
+
return tests.map((d) => ({ ...d, file: rewriteContainerPath(d.file, mapping) }));
|
|
317
|
+
}
|
|
318
|
+
function testListEntry(d, file) {
|
|
319
|
+
const loc = d.column === void 0 ? `${file}:${d.line}` : `${file}:${d.line}:${d.column}`;
|
|
320
|
+
const title = d.titlePath.join(" \u203A ");
|
|
321
|
+
const prefix = d.projectName !== void 0 && d.projectName !== "" ? `[${d.projectName}] \u203A ` : "";
|
|
322
|
+
return `${prefix}${loc} \u203A ${title}`;
|
|
323
|
+
}
|
|
324
|
+
function buildTestListEntries(tests, rootDir, cwd) {
|
|
325
|
+
if (rootDir !== void 0) {
|
|
326
|
+
return tests.map((d) => testListEntry(d, isAbsolute(d.file) ? relative(rootDir, d.file) || d.file : d.file));
|
|
327
|
+
}
|
|
328
|
+
return tests.flatMap((d) => {
|
|
329
|
+
if (!isAbsolute(d.file)) return [testListEntry(d, d.file)];
|
|
330
|
+
const rel = relative(cwd ?? process.cwd(), d.file);
|
|
331
|
+
const segments = rel.split(sep);
|
|
332
|
+
return segments.map((_, i) => testListEntry(d, segments.slice(i).join(sep)));
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// src/server/run-controller.ts
|
|
337
|
+
import { spawn as spawn2 } from "child_process";
|
|
338
|
+
import { unlinkSync, writeFileSync } from "node:fs";
|
|
339
|
+
import { createRequire as createRequire2 } from "node:module";
|
|
340
|
+
import { tmpdir } from "node:os";
|
|
341
|
+
import { join as join3 } from "node:path";
|
|
342
|
+
|
|
343
|
+
// src/server/run-launcher.ts
|
|
344
|
+
import { resolveCommand } from "package-manager-detector/commands";
|
|
345
|
+
import { getUserAgent } from "package-manager-detector/detect";
|
|
346
|
+
function resolvePlaywrightLaunch(cwd, playwrightArgs) {
|
|
347
|
+
const agent = getUserAgent();
|
|
348
|
+
const resolved = agent === null ? null : resolveCommand(agent, "execute-local", ["playwright", ...playwrightArgs]);
|
|
349
|
+
if (resolved !== null) return { cmd: resolved.command, args: resolved.args };
|
|
350
|
+
return { cmd: "npx", args: ["playwright", ...playwrightArgs] };
|
|
351
|
+
}
|
|
352
|
+
function buildSpawnEnv(port, baseEnv = process.env) {
|
|
353
|
+
const env = {};
|
|
354
|
+
for (const [key, value] of Object.entries(baseEnv)) {
|
|
355
|
+
if (key === "CI") continue;
|
|
356
|
+
env[key] = value;
|
|
357
|
+
}
|
|
358
|
+
env.CRVY_RPRTR_SERVER_URL = `ws://localhost:${port}`;
|
|
359
|
+
env.PLAYWRIGHT_HTML_OPEN = "never";
|
|
360
|
+
return env;
|
|
361
|
+
}
|
|
362
|
+
function createLocalLauncher(options) {
|
|
363
|
+
return {
|
|
364
|
+
mode: "local",
|
|
365
|
+
launch({ ctx, playwrightArgs }) {
|
|
366
|
+
const resolve5 = options.resolveLaunch ?? resolvePlaywrightLaunch;
|
|
367
|
+
const { cmd, args } = resolve5(ctx.cwd, playwrightArgs);
|
|
368
|
+
return { cmd, args, env: buildSpawnEnv(options.port, options.env) };
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// src/server/run-controller.ts
|
|
374
|
+
var STOP_GRACE_MS = 5e3;
|
|
375
|
+
var KNOWN_SIGNALS = {
|
|
376
|
+
SIGTERM: "SIGTERM",
|
|
377
|
+
SIGKILL: "SIGKILL"
|
|
378
|
+
};
|
|
379
|
+
function sharedProject(tests) {
|
|
380
|
+
const names = new Set(tests.map((t) => t.projectName ?? ""));
|
|
381
|
+
if (names.size === 1) {
|
|
382
|
+
const name = [...names][0];
|
|
383
|
+
return name === "" ? void 0 : name;
|
|
384
|
+
}
|
|
385
|
+
return void 0;
|
|
386
|
+
}
|
|
387
|
+
function gteMinor(version, major, minor) {
|
|
388
|
+
const match = /^(\d+)\.(\d+)/.exec(version.trim());
|
|
389
|
+
if (match === null) return false;
|
|
390
|
+
const maj = parseInt(match[1], 10);
|
|
391
|
+
const min = parseInt(match[2], 10);
|
|
392
|
+
if (maj !== major) return maj > major;
|
|
393
|
+
return min >= minor;
|
|
394
|
+
}
|
|
395
|
+
var defaultWriteTempFile = (content) => {
|
|
396
|
+
const path = join3(tmpdir(), `crvy-rprtr-test-list-${process.pid}-${Date.now()}.txt`);
|
|
397
|
+
writeFileSync(path, content, "utf8");
|
|
398
|
+
return path;
|
|
399
|
+
};
|
|
400
|
+
function defaultDeleteTempFile(path) {
|
|
401
|
+
try {
|
|
402
|
+
unlinkSync(path);
|
|
403
|
+
} catch {
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
function resolveReporterDefault(cwd) {
|
|
407
|
+
try {
|
|
408
|
+
return createRequire2(join3(cwd, "package.json")).resolve("@crvy/rprtr");
|
|
409
|
+
} catch {
|
|
410
|
+
}
|
|
411
|
+
try {
|
|
412
|
+
return createRequire2(import.meta.url).resolve("@crvy/rprtr");
|
|
413
|
+
} catch {
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
var RunController = class {
|
|
418
|
+
constructor(deps) {
|
|
419
|
+
this.deps = deps;
|
|
420
|
+
}
|
|
421
|
+
child = null;
|
|
422
|
+
sigkillTimer = null;
|
|
423
|
+
testListPath = null;
|
|
424
|
+
get isRunning() {
|
|
425
|
+
return this.child !== null;
|
|
426
|
+
}
|
|
427
|
+
supportsTestList(cwd) {
|
|
428
|
+
const getVersion = this.deps.getPlaywrightVersion ?? resolvePlaywrightVersion;
|
|
429
|
+
const version = getVersion(cwd);
|
|
430
|
+
return version !== null && gteMinor(version, 1, 56);
|
|
431
|
+
}
|
|
432
|
+
cleanupTempFile() {
|
|
433
|
+
if (this.testListPath !== null) {
|
|
434
|
+
const del = this.deps.deleteTempFile ?? defaultDeleteTempFile;
|
|
435
|
+
del(this.testListPath);
|
|
436
|
+
this.testListPath = null;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
buildPlaywrightArgs(ctx, filters, tests) {
|
|
440
|
+
const resolveReporter = this.deps.resolveReporter ?? resolveReporterDefault;
|
|
441
|
+
const reporterModule = resolveReporter(ctx.cwd);
|
|
442
|
+
const useTestList = tests !== void 0 && (tests.length > 1 || this.deps.containerPathMapping !== void 0) && this.supportsTestList(ctx.cwd);
|
|
443
|
+
const args = ["test", "--config", ctx.configFile];
|
|
444
|
+
if (reporterModule !== null) args.push("--reporter", reporterModule);
|
|
445
|
+
if (filters.update === true) args.push("--update-snapshots");
|
|
446
|
+
if (useTestList && tests !== void 0) {
|
|
447
|
+
const content = buildTestListEntries(tests, ctx.rootDir, ctx.cwd).join("\n");
|
|
448
|
+
const writeTemp = this.deps.writeTempFile ?? defaultWriteTempFile;
|
|
449
|
+
this.testListPath = writeTemp(content);
|
|
450
|
+
args.push("--test-list", this.testListPath);
|
|
451
|
+
} else if (tests !== void 0 && tests.length > 0) {
|
|
452
|
+
const project = sharedProject(tests);
|
|
453
|
+
if (project !== void 0) args.push(`--project=${project}`);
|
|
454
|
+
for (const d of tests) {
|
|
455
|
+
args.push(d.column === void 0 ? `${d.file}:${d.line}` : `${d.file}:${d.line}:${d.column}`);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return args;
|
|
459
|
+
}
|
|
460
|
+
start(filters) {
|
|
461
|
+
const ctx = this.deps.getRunContext();
|
|
462
|
+
if (ctx === null) return { ok: false, reason: "no-config" };
|
|
463
|
+
if (this.child !== null) return { ok: false, reason: "already-running" };
|
|
464
|
+
if (filters.tests !== void 0 && filters.tests.length === 0) return { ok: false, reason: "no-tests" };
|
|
465
|
+
if (this.deps.launcher.available === false) return { ok: false, reason: "docker-unavailable" };
|
|
466
|
+
const tests = rewriteContainerTestDescriptors(filters.tests, this.deps.containerPathMapping);
|
|
467
|
+
const args = this.buildPlaywrightArgs(ctx, filters, tests);
|
|
468
|
+
const spec = this.deps.launcher.launch({ ctx, playwrightArgs: args });
|
|
469
|
+
let child;
|
|
470
|
+
try {
|
|
471
|
+
child = this.deps.spawn(spec.cmd, spec.args, { cwd: ctx.cwd, env: spec.env, stdio: "inherit" });
|
|
472
|
+
} catch (err) {
|
|
473
|
+
this.cleanupTempFile();
|
|
474
|
+
throw err;
|
|
475
|
+
}
|
|
476
|
+
this.child = child;
|
|
477
|
+
child.on("exit", (code) => {
|
|
478
|
+
this.handleChildExit(code);
|
|
479
|
+
});
|
|
480
|
+
child.on("error", () => {
|
|
481
|
+
this.handleChildExit(null);
|
|
482
|
+
});
|
|
483
|
+
this.deps.setReportRunning(true);
|
|
484
|
+
this.deps.setRunFiltered?.(filters.tests !== void 0);
|
|
485
|
+
this.deps.broadcast({ type: "run-status", data: { running: true, mode: this.deps.launcher.mode } });
|
|
486
|
+
return { ok: true };
|
|
487
|
+
}
|
|
488
|
+
stop() {
|
|
489
|
+
if (this.child === null) return { ok: false, reason: "not-running" };
|
|
490
|
+
if (this.sigkillTimer !== null) this.deps.timers.clearTimeout(this.sigkillTimer);
|
|
491
|
+
this.child.kill("SIGTERM");
|
|
492
|
+
this.sigkillTimer = this.deps.timers.setTimeout(() => {
|
|
493
|
+
if (this.child !== null) {
|
|
494
|
+
this.child.kill("SIGKILL");
|
|
495
|
+
this.deps.launcher.onForceKill?.();
|
|
496
|
+
}
|
|
497
|
+
}, STOP_GRACE_MS);
|
|
498
|
+
return { ok: true };
|
|
499
|
+
}
|
|
500
|
+
async prepareRun() {
|
|
501
|
+
const launcher = this.deps.launcher;
|
|
502
|
+
if (launcher.prepare === void 0) return { ok: true };
|
|
503
|
+
const ctx = this.deps.getRunContext();
|
|
504
|
+
if (ctx === null) return { ok: true };
|
|
505
|
+
try {
|
|
506
|
+
await launcher.prepare({
|
|
507
|
+
ctx,
|
|
508
|
+
onProgress: (phase) => {
|
|
509
|
+
this.deps.broadcast({ type: "run-status", data: { running: true, mode: launcher.mode, phase } });
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
return { ok: true };
|
|
513
|
+
} catch (error) {
|
|
514
|
+
this.deps.broadcast({ type: "run-status", data: { running: false, mode: launcher.mode } });
|
|
515
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
516
|
+
console.warn(`[RunController] run preparation failed: ${message}`);
|
|
517
|
+
return { ok: false, reason: "docker-unavailable" };
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
dispose() {
|
|
521
|
+
if (this.child === null) return;
|
|
522
|
+
if (this.sigkillTimer !== null) this.deps.timers.clearTimeout(this.sigkillTimer);
|
|
523
|
+
this.sigkillTimer = null;
|
|
524
|
+
this.child.kill("SIGKILL");
|
|
525
|
+
this.deps.launcher.onForceKill?.();
|
|
526
|
+
this.cleanupTempFile();
|
|
527
|
+
}
|
|
528
|
+
handleChildExit(code) {
|
|
529
|
+
if (this.child === null) return;
|
|
530
|
+
if (this.sigkillTimer !== null) this.deps.timers.clearTimeout(this.sigkillTimer);
|
|
531
|
+
this.sigkillTimer = null;
|
|
532
|
+
this.child = null;
|
|
533
|
+
this.cleanupTempFile();
|
|
534
|
+
if (code !== null && code !== 0) console.warn(`[RunController] playwright test exited with code ${code}`);
|
|
535
|
+
this.deps.setReportRunning(false);
|
|
536
|
+
this.deps.broadcast({ type: "run-status", data: { running: false, mode: this.deps.launcher.mode } });
|
|
537
|
+
void this.deps.saveReport?.();
|
|
538
|
+
}
|
|
539
|
+
};
|
|
540
|
+
function createRealSpawn() {
|
|
541
|
+
return (cmd, args, opts) => {
|
|
542
|
+
const cp = spawn2(cmd, args, opts);
|
|
543
|
+
return {
|
|
544
|
+
on: (event, cb) => cp.on(event, cb),
|
|
545
|
+
kill: (signal) => {
|
|
546
|
+
const sig = KNOWN_SIGNALS[signal];
|
|
547
|
+
if (sig !== void 0) cp.kill(sig);
|
|
548
|
+
}
|
|
549
|
+
};
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
function createRealTimers() {
|
|
553
|
+
const pending = [];
|
|
554
|
+
return {
|
|
555
|
+
setTimeout: (fn, ms) => {
|
|
556
|
+
const id = setTimeout(fn, ms);
|
|
557
|
+
pending.push(id);
|
|
558
|
+
return id;
|
|
559
|
+
},
|
|
560
|
+
clearTimeout: () => {
|
|
561
|
+
for (const h of pending.splice(0)) clearTimeout(h);
|
|
562
|
+
}
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// src/server/docker-launcher.ts
|
|
567
|
+
var DOCKER_WORK_DIR = "/work";
|
|
568
|
+
var DOCKER_HOST_GATEWAY = "host.docker.internal";
|
|
569
|
+
var ENV_DENYLIST = /* @__PURE__ */ new Set([
|
|
570
|
+
"CI",
|
|
571
|
+
"PLAYWRIGHT_BROWSERS_PATH",
|
|
572
|
+
"CRVY_RPRTR_SERVER_URL",
|
|
573
|
+
"CRVY_RPRTR_PORTABLE_ARTIFACTS",
|
|
574
|
+
"TZ",
|
|
575
|
+
"LANG",
|
|
576
|
+
"LC_ALL",
|
|
577
|
+
"PLAYWRIGHT_HTML_OPEN"
|
|
578
|
+
]);
|
|
579
|
+
var DockerUnavailableError = class extends Error {
|
|
580
|
+
constructor() {
|
|
581
|
+
super("Docker daemon is not available");
|
|
582
|
+
this.name = "DockerUnavailableError";
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
function defaultWarn(message) {
|
|
586
|
+
console.warn(`[crvy-rprtr] ${message}`);
|
|
587
|
+
}
|
|
588
|
+
async function detectAgentName(detect2, cwd) {
|
|
589
|
+
const detected = await (detect2 ?? detectProjectAgent)(cwd);
|
|
590
|
+
return detected?.name ?? null;
|
|
591
|
+
}
|
|
592
|
+
async function prepareDocker(state, exec, ctx, deps, onProgress) {
|
|
593
|
+
if (!await probeDockerDaemon(exec)) {
|
|
594
|
+
state.available = false;
|
|
595
|
+
throw new DockerUnavailableError();
|
|
596
|
+
}
|
|
597
|
+
state.available = true;
|
|
598
|
+
const image = resolveDockerImage({ image: deps.docker?.image, version: deps.getPlaywrightVersion(ctx.cwd) });
|
|
599
|
+
if (image === null) {
|
|
600
|
+
throw new Error("Could not resolve the installed @playwright/test version; set docker.image explicitly.");
|
|
601
|
+
}
|
|
602
|
+
state.image = image;
|
|
603
|
+
state.command = resolveContainerCommand({
|
|
604
|
+
command: deps.docker?.command,
|
|
605
|
+
hasCustomImage: deps.docker?.image !== void 0,
|
|
606
|
+
detectedAgentName: deps.docker?.image === void 0 ? "npm" : await detectAgentName(deps.detectAgent, ctx.cwd),
|
|
607
|
+
warn: deps.warn
|
|
608
|
+
});
|
|
609
|
+
if (!await isDockerImagePresent(exec, image)) {
|
|
610
|
+
onProgress("pulling");
|
|
611
|
+
if (!await pullDockerImage(exec, image)) {
|
|
612
|
+
throw new Error(`Failed to pull docker image: ${image}`);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
var REPORTER_BARE_SPECIFIER = "@crvy/rprtr";
|
|
617
|
+
var PATH_FLAGS = /* @__PURE__ */ new Set(["--config", "--reporter", "--test-list"]);
|
|
618
|
+
function rewritePlaywrightArgs(playwrightArgs, ctx, workDir, warn) {
|
|
619
|
+
const args = [];
|
|
620
|
+
const bindMounts = [];
|
|
621
|
+
for (let i = 0; i < playwrightArgs.length; i++) {
|
|
622
|
+
const flag = playwrightArgs[i];
|
|
623
|
+
if (!PATH_FLAGS.has(flag)) {
|
|
624
|
+
args.push(flag);
|
|
625
|
+
continue;
|
|
626
|
+
}
|
|
627
|
+
const value = playwrightArgs[i + 1];
|
|
628
|
+
if (value === void 0) break;
|
|
629
|
+
i += 1;
|
|
630
|
+
args.push(flag);
|
|
631
|
+
const rewritten = rewriteContainerPath(value, { from: ctx.cwd, to: workDir });
|
|
632
|
+
if (flag === "--reporter" && rewritten === value) {
|
|
633
|
+
args.push(REPORTER_BARE_SPECIFIER);
|
|
634
|
+
} else if (flag === "--test-list" && rewritten === value) {
|
|
635
|
+
args.push(value);
|
|
636
|
+
bindMounts.push(`${value}:${value}:ro`);
|
|
637
|
+
} else {
|
|
638
|
+
if (flag === "--config" && rewritten === value) {
|
|
639
|
+
warn(`--config "${value}" is outside the project directory and will not resolve inside the container.`);
|
|
640
|
+
}
|
|
641
|
+
args.push(rewritten);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
return { args, bindMounts };
|
|
645
|
+
}
|
|
646
|
+
function buildDockerRunArgs(ctx, playwrightArgs, deps) {
|
|
647
|
+
const { args: rewrittenArgs, bindMounts } = rewritePlaywrightArgs(playwrightArgs, ctx, deps.workDir, deps.warn);
|
|
648
|
+
const args = [
|
|
649
|
+
"run",
|
|
650
|
+
"--rm",
|
|
651
|
+
"--init",
|
|
652
|
+
"--name",
|
|
653
|
+
deps.containerName,
|
|
654
|
+
"--add-host",
|
|
655
|
+
`${DOCKER_HOST_GATEWAY}:host-gateway`,
|
|
656
|
+
"--ipc=host"
|
|
657
|
+
];
|
|
658
|
+
if (deps.docker?.platform !== void 0) {
|
|
659
|
+
args.push("--platform", deps.docker.platform);
|
|
660
|
+
}
|
|
661
|
+
args.push("-v", `${ctx.cwd}:${deps.workDir}:rw`, "-w", deps.workDir);
|
|
662
|
+
for (const mount of bindMounts) {
|
|
663
|
+
args.push("-v", mount);
|
|
664
|
+
}
|
|
665
|
+
args.push("-e", `CRVY_RPRTR_SERVER_URL=ws://${DOCKER_HOST_GATEWAY}:${deps.port}`);
|
|
666
|
+
args.push("-e", "CRVY_RPRTR_PORTABLE_ARTIFACTS=1", "-e", "TZ=UTC", "-e", "LANG=C.UTF-8", "-e", "LC_ALL=C.UTF-8");
|
|
667
|
+
args.push("-e", "PLAYWRIGHT_HTML_OPEN=never");
|
|
668
|
+
for (const [key, value] of Object.entries(deps.env)) {
|
|
669
|
+
if (ENV_DENYLIST.has(key) || value === void 0) continue;
|
|
670
|
+
args.push("-e", key);
|
|
671
|
+
}
|
|
672
|
+
if (deps.docker?.extraArgs !== void 0) args.push(...deps.docker.extraArgs);
|
|
673
|
+
args.push(deps.image, ...deps.command, "playwright", ...rewrittenArgs);
|
|
674
|
+
return args;
|
|
675
|
+
}
|
|
676
|
+
function stripCi(env) {
|
|
677
|
+
const out = {};
|
|
678
|
+
for (const [key, value] of Object.entries(env)) {
|
|
679
|
+
if (key === "CI") continue;
|
|
680
|
+
out[key] = value;
|
|
681
|
+
}
|
|
682
|
+
return out;
|
|
683
|
+
}
|
|
684
|
+
function createState(docker) {
|
|
685
|
+
return { available: void 0, prepared: null, image: null, command: docker?.command ?? DEFAULT_CONTAINER_COMMAND };
|
|
686
|
+
}
|
|
687
|
+
function buildLauncher(state, deps) {
|
|
688
|
+
return {
|
|
689
|
+
mode: "docker",
|
|
690
|
+
get available() {
|
|
691
|
+
return state.available;
|
|
692
|
+
},
|
|
693
|
+
prepare({ ctx, onProgress }) {
|
|
694
|
+
state.prepared ??= prepareDocker(
|
|
695
|
+
state,
|
|
696
|
+
deps.exec,
|
|
697
|
+
ctx,
|
|
698
|
+
{
|
|
699
|
+
docker: deps.docker,
|
|
700
|
+
getPlaywrightVersion: deps.getVersion,
|
|
701
|
+
detectAgent: deps.detectAgent,
|
|
702
|
+
warn: deps.warn
|
|
703
|
+
},
|
|
704
|
+
onProgress
|
|
705
|
+
).catch((error) => {
|
|
706
|
+
state.prepared = null;
|
|
707
|
+
throw error;
|
|
708
|
+
});
|
|
709
|
+
return state.prepared;
|
|
710
|
+
},
|
|
711
|
+
launch({ ctx, playwrightArgs }) {
|
|
712
|
+
const image = state.image ?? resolveDockerImage({ image: deps.docker?.image, version: deps.getVersion(ctx.cwd) });
|
|
713
|
+
if (image === null) {
|
|
714
|
+
throw new Error("Could not resolve the docker image; run prepare() first or set docker.image.");
|
|
715
|
+
}
|
|
716
|
+
const args = buildDockerRunArgs(ctx, playwrightArgs, {
|
|
717
|
+
docker: deps.docker,
|
|
718
|
+
workDir: deps.workDir,
|
|
719
|
+
containerName: deps.containerName,
|
|
720
|
+
port: deps.port,
|
|
721
|
+
env: deps.baseEnv,
|
|
722
|
+
image,
|
|
723
|
+
command: state.command,
|
|
724
|
+
warn: deps.warn
|
|
725
|
+
});
|
|
726
|
+
return { cmd: "docker", args, env: stripCi(deps.baseEnv) };
|
|
727
|
+
},
|
|
728
|
+
onForceKill() {
|
|
729
|
+
void forceRemoveContainer(deps.exec, deps.containerName);
|
|
730
|
+
}
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
function createDockerLauncher(options) {
|
|
734
|
+
return buildLauncher(createState(options.docker), {
|
|
735
|
+
exec: options.exec ?? createDockerExec(),
|
|
736
|
+
workDir: options.workDir ?? DOCKER_WORK_DIR,
|
|
737
|
+
containerName: options.containerName ?? `crvy-rprtr-run-${process.pid}`,
|
|
738
|
+
baseEnv: options.env ?? process.env,
|
|
739
|
+
getVersion: options.getPlaywrightVersion ?? resolvePlaywrightVersion,
|
|
740
|
+
detectAgent: options.detectAgent,
|
|
741
|
+
warn: options.warn ?? defaultWarn,
|
|
742
|
+
docker: options.docker,
|
|
743
|
+
port: options.port
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
|
|
211
747
|
// src/server/handlers.ts
|
|
212
748
|
import { existsSync as existsSync2 } from "fs";
|
|
749
|
+
import { dirname as dirname3, resolve as resolve3 } from "path";
|
|
213
750
|
|
|
214
751
|
// src/server/artifact-routes.ts
|
|
215
752
|
import { existsSync } from "fs";
|
|
@@ -217,7 +754,7 @@ import { realpath } from "fs/promises";
|
|
|
217
754
|
import { dirname as dirname2, resolve as resolve2 } from "path";
|
|
218
755
|
|
|
219
756
|
// src/server/utils.ts
|
|
220
|
-
import { isAbsolute, relative, resolve, sep } from "path";
|
|
757
|
+
import { isAbsolute as isAbsolute2, relative as relative2, resolve, sep as sep2 } from "path";
|
|
221
758
|
var LIVE_UPDATES_WEBSOCKET_PATH = "/";
|
|
222
759
|
function broadcastToBrowsers(wsClients, msg) {
|
|
223
760
|
const payload = JSON.stringify(msg);
|
|
@@ -231,8 +768,8 @@ function isWebSocketUpgradeRequest(req) {
|
|
|
231
768
|
function isPathWithinRoots(target, roots) {
|
|
232
769
|
const resolvedTarget = resolve(target);
|
|
233
770
|
return roots.some((root) => {
|
|
234
|
-
const rel =
|
|
235
|
-
return rel === "" || !rel.startsWith(`..${
|
|
771
|
+
const rel = relative2(resolve(root), resolvedTarget);
|
|
772
|
+
return rel === "" || !rel.startsWith(`..${sep2}`) && rel !== ".." && !isAbsolute2(rel);
|
|
236
773
|
});
|
|
237
774
|
}
|
|
238
775
|
|
|
@@ -282,11 +819,14 @@ function reporterTitlePath(test) {
|
|
|
282
819
|
return ["", projectName, testFile ?? "", ...test.titlePath, test.title];
|
|
283
820
|
}
|
|
284
821
|
function resolveBaselineSnapshotPath(routing, test, retry, imageName) {
|
|
285
|
-
const
|
|
822
|
+
const mapping = routing?.containerPathMapping;
|
|
823
|
+
const rawTestFile = test.location?.file;
|
|
824
|
+
const testFile = rawTestFile === void 0 || mapping === void 0 ? rawTestFile : rewriteContainerPath(rawTestFile, mapping);
|
|
286
825
|
const declaration = test.results?.[retry]?.visualDeclarations?.find((candidate) => candidate.visualName === imageName);
|
|
287
826
|
if (routing === void 0 || testFile === void 0 || declaration === void 0) {
|
|
288
827
|
return null;
|
|
289
828
|
}
|
|
829
|
+
const isContainerPath = mapping !== void 0 && testFile !== rawTestFile;
|
|
290
830
|
const targets = resolveBaselineTargets({
|
|
291
831
|
testFile,
|
|
292
832
|
reporterTitlePath: reporterTitlePath(test),
|
|
@@ -296,7 +836,7 @@ function resolveBaselineSnapshotPath(routing, test, retry, imageName) {
|
|
|
296
836
|
testDir: routing.playwrightTestDir ?? dirname2(testFile),
|
|
297
837
|
snapshotDir: routing.playwrightSnapshotDir ?? dirname2(testFile),
|
|
298
838
|
projectName: test.projectName ?? test.browser,
|
|
299
|
-
snapshotSuffix: process.platform,
|
|
839
|
+
snapshotSuffix: isContainerPath ? "linux" : process.platform,
|
|
300
840
|
snapshotPathTemplate: routing.playwrightSnapshotPathTemplate,
|
|
301
841
|
toHaveScreenshotPathTemplate: routing.playwrightToHaveScreenshotPathTemplate
|
|
302
842
|
},
|
|
@@ -419,7 +959,19 @@ function handleSync(ctx) {
|
|
|
419
959
|
};
|
|
420
960
|
broadcastToBrowsers(ctx.wsClients, message);
|
|
421
961
|
}
|
|
422
|
-
function
|
|
962
|
+
function applyContainerPathMapping(rawData, mapping) {
|
|
963
|
+
return {
|
|
964
|
+
...rawData,
|
|
965
|
+
playwrightSnapshotDir: rawData.playwrightSnapshotDir === void 0 ? void 0 : rewriteContainerPath(rawData.playwrightSnapshotDir, mapping),
|
|
966
|
+
playwrightTestDir: rawData.playwrightTestDir === void 0 ? void 0 : rewriteContainerPath(rawData.playwrightTestDir, mapping),
|
|
967
|
+
playwrightRootDir: rawData.playwrightRootDir === void 0 ? void 0 : rewriteContainerPath(rawData.playwrightRootDir, mapping),
|
|
968
|
+
configFile: rawData.configFile === void 0 ? void 0 : rewriteContainerPath(rawData.configFile, mapping),
|
|
969
|
+
cwd: rawData.cwd === void 0 ? void 0 : rewriteContainerPath(rawData.cwd, mapping)
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
function handleRegister(ctx, rawData) {
|
|
973
|
+
const mapping = ctx.routesContext.containerPathMapping;
|
|
974
|
+
const data = mapping === void 0 ? rawData : applyContainerPathMapping(rawData, mapping);
|
|
423
975
|
const roots = [];
|
|
424
976
|
if (data.playwrightSnapshotDir !== void 0 && data.playwrightSnapshotDir !== "") {
|
|
425
977
|
roots.push(data.playwrightSnapshotDir);
|
|
@@ -447,18 +999,61 @@ function handleRegister(ctx, data) {
|
|
|
447
999
|
if (data.playwrightToHaveScreenshotPathTemplate !== void 0) {
|
|
448
1000
|
ctx.routesContext.approvalRouting.playwrightToHaveScreenshotPathTemplate = data.playwrightToHaveScreenshotPathTemplate;
|
|
449
1001
|
}
|
|
450
|
-
}
|
|
451
|
-
if (data.configFile !== void 0 && data.cwd !== void 0) {
|
|
452
|
-
ctx.routesContext.runContext =
|
|
453
|
-
}
|
|
454
|
-
console.log("[Server] Reporter registered with config:", {
|
|
455
|
-
playwrightSnapshotDir: data.playwrightSnapshotDir,
|
|
456
|
-
playwrightTestDir: data.playwrightTestDir
|
|
1002
|
+
}
|
|
1003
|
+
if (data.configFile !== void 0 && data.cwd !== void 0) {
|
|
1004
|
+
ctx.routesContext.runContext = buildRunContext(data.configFile, data);
|
|
1005
|
+
}
|
|
1006
|
+
console.log("[Server] Reporter registered with config:", {
|
|
1007
|
+
playwrightSnapshotDir: data.playwrightSnapshotDir,
|
|
1008
|
+
playwrightTestDir: data.playwrightTestDir,
|
|
1009
|
+
configFile: data.configFile,
|
|
1010
|
+
cwd: data.cwd
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
function buildRunContext(configFile, data) {
|
|
1014
|
+
const configDir = dirname3(configFile);
|
|
1015
|
+
return {
|
|
1016
|
+
configFile,
|
|
1017
|
+
cwd: configDir,
|
|
1018
|
+
rootDir: data.playwrightRootDir ?? (data.playwrightTestDir === void 0 ? configDir : resolve3(configDir, data.playwrightTestDir))
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// src/server/run-mode.ts
|
|
1023
|
+
async function resolveRunMode(options) {
|
|
1024
|
+
if (options.runMode === "local") return "local";
|
|
1025
|
+
if (options.runMode === "docker") return "docker";
|
|
1026
|
+
if (options.isCI) return "local";
|
|
1027
|
+
if (await options.probeDocker()) return "docker";
|
|
1028
|
+
options.warn?.(
|
|
1029
|
+
"Docker daemon unavailable \u2014 running tests locally; screenshots may differ from CI. Use --run-mode local to silence this warning."
|
|
1030
|
+
);
|
|
1031
|
+
return "local";
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// src/server/launcher-resolver.ts
|
|
1035
|
+
async function resolveRunBackend(options) {
|
|
1036
|
+
const dockerExec = createDockerExec();
|
|
1037
|
+
const resolvedRunMode = await resolveRunMode({
|
|
1038
|
+
runMode: options.runMode ?? "auto",
|
|
1039
|
+
isCI: isCI(),
|
|
1040
|
+
probeDocker: () => probeDockerDaemon(dockerExec),
|
|
1041
|
+
warn: (message) => {
|
|
1042
|
+
console.warn(`[crvy-rprtr] ${message}`);
|
|
1043
|
+
}
|
|
457
1044
|
});
|
|
1045
|
+
const launcher = resolvedRunMode === "docker" ? createDockerLauncher({ port: options.port, docker: options.docker, exec: dockerExec }) : createLocalLauncher({ port: options.port });
|
|
1046
|
+
return {
|
|
1047
|
+
launcher,
|
|
1048
|
+
routesContextOptions: {
|
|
1049
|
+
runInfo: { mode: resolvedRunMode },
|
|
1050
|
+
containerPathMapping: resolvedRunMode === "docker" ? { from: DOCKER_WORK_DIR, to: process.cwd() } : void 0
|
|
1051
|
+
}
|
|
1052
|
+
};
|
|
458
1053
|
}
|
|
459
1054
|
|
|
460
1055
|
// src/server/playwright-config.ts
|
|
461
|
-
import { join as
|
|
1056
|
+
import { join as join4, resolve as resolve4 } from "path";
|
|
462
1057
|
var CONFIG_FILES = [
|
|
463
1058
|
"playwright.config.ts",
|
|
464
1059
|
"playwright.config.mts",
|
|
@@ -470,12 +1065,15 @@ var CONFIG_FILES = [
|
|
|
470
1065
|
async function resolvePlaywrightConfig(cwd) {
|
|
471
1066
|
const matches = await Promise.all(
|
|
472
1067
|
CONFIG_FILES.map(async (file) => {
|
|
473
|
-
const candidate =
|
|
1068
|
+
const candidate = join4(cwd, file);
|
|
474
1069
|
return await fileExists(candidate) ? candidate : null;
|
|
475
1070
|
})
|
|
476
1071
|
);
|
|
477
1072
|
return matches.find((path) => path !== null) ?? null;
|
|
478
1073
|
}
|
|
1074
|
+
function resolveSeedConfigFile(option, cwd) {
|
|
1075
|
+
return option === void 0 ? resolvePlaywrightConfig(cwd) : Promise.resolve(resolve4(cwd, option));
|
|
1076
|
+
}
|
|
479
1077
|
|
|
480
1078
|
// src/server/report-persistence.ts
|
|
481
1079
|
function createDebouncedSaver(save, delayMs, setTimeoutFn = (fn, ms) => setTimeout(fn, ms), clearTimeoutFn = (handle) => {
|
|
@@ -547,14 +1145,17 @@ function createRoutesContext(reportData, staticDir, saveReport, options) {
|
|
|
547
1145
|
playwrightTestDir: options.playwrightTestDir,
|
|
548
1146
|
playwrightSnapshotDir: options.playwrightSnapshotDir,
|
|
549
1147
|
playwrightSnapshotPathTemplate: options.playwrightSnapshotPathTemplate,
|
|
550
|
-
playwrightToHaveScreenshotPathTemplate: options.playwrightToHaveScreenshotPathTemplate
|
|
1148
|
+
playwrightToHaveScreenshotPathTemplate: options.playwrightToHaveScreenshotPathTemplate,
|
|
1149
|
+
containerPathMapping: options.containerPathMapping
|
|
551
1150
|
},
|
|
552
|
-
runContext: void 0
|
|
1151
|
+
runContext: void 0,
|
|
1152
|
+
runInfo: options.runInfo,
|
|
1153
|
+
containerPathMapping: options.containerPathMapping
|
|
553
1154
|
};
|
|
554
1155
|
}
|
|
555
1156
|
|
|
556
1157
|
// src/server/routes.ts
|
|
557
|
-
import { join as
|
|
1158
|
+
import { join as join5 } from "path";
|
|
558
1159
|
|
|
559
1160
|
// src/server/run-routes.ts
|
|
560
1161
|
function handleRunRoutes(pathname, method, runController, req) {
|
|
@@ -576,6 +1177,10 @@ async function handleApiRun(runController, req) {
|
|
|
576
1177
|
if (parsed === null) {
|
|
577
1178
|
return Response.json({ ok: false, error: "Invalid request body" }, { status: 400 });
|
|
578
1179
|
}
|
|
1180
|
+
const preparation = await runController.prepareRun();
|
|
1181
|
+
if (!preparation.ok) {
|
|
1182
|
+
return Response.json({ ok: false, reason: preparation.reason }, { status: 409 });
|
|
1183
|
+
}
|
|
579
1184
|
const result = runController.start(parsed);
|
|
580
1185
|
if (result.ok) return Response.json(result);
|
|
581
1186
|
const status = result.reason === "no-tests" ? 400 : 409;
|
|
@@ -589,7 +1194,7 @@ function handleApiStop(runController) {
|
|
|
589
1194
|
|
|
590
1195
|
// src/server/routes.ts
|
|
591
1196
|
async function handleRoot(ctx) {
|
|
592
|
-
const html = await respondWithFile(
|
|
1197
|
+
const html = await respondWithFile(join5(ctx.staticDir, "index.html"), "text/html");
|
|
593
1198
|
return html ?? new Response("Not Found", { status: 404 });
|
|
594
1199
|
}
|
|
595
1200
|
async function handleAppCss() {
|
|
@@ -606,13 +1211,14 @@ async function handleSrcFiles(req) {
|
|
|
606
1211
|
function handleApiReport(ctx) {
|
|
607
1212
|
return Response.json({
|
|
608
1213
|
...ctx.reportData,
|
|
609
|
-
runEnabled: ctx.runContext !== void 0
|
|
1214
|
+
runEnabled: ctx.runContext !== void 0,
|
|
1215
|
+
runMode: ctx.runInfo?.mode
|
|
610
1216
|
});
|
|
611
1217
|
}
|
|
612
1218
|
var APPROVAL_TARGET_ERROR = "Could not resolve approval target";
|
|
613
1219
|
function actualPathFromUrl(ctx, actualUrl) {
|
|
614
1220
|
if (actualUrl.startsWith("/screenshots/")) {
|
|
615
|
-
return
|
|
1221
|
+
return join5(ctx.reportData.screenshotDir, actualUrl.slice("/screenshots/".length));
|
|
616
1222
|
}
|
|
617
1223
|
if (actualUrl.startsWith("/file/")) {
|
|
618
1224
|
return decodeURIComponent(actualUrl.slice("/file/".length));
|
|
@@ -734,7 +1340,7 @@ async function handleScreenshots(ctx, req) {
|
|
|
734
1340
|
}
|
|
735
1341
|
async function handleDist(ctx, req) {
|
|
736
1342
|
const path = new URL(req.url).pathname.slice("/dist/".length);
|
|
737
|
-
const filePath =
|
|
1343
|
+
const filePath = join5(ctx.staticDir, path);
|
|
738
1344
|
const contentType = filePath.endsWith(".css") ? "text/css" : filePath.endsWith(".js") ? "application/javascript" : filePath.endsWith(".svelte") ? "text/plain" : "application/octet-stream";
|
|
739
1345
|
const file = await respondWithFile(filePath, contentType);
|
|
740
1346
|
return file ?? new Response("Not Found", { status: 404 });
|
|
@@ -779,216 +1385,56 @@ function handleHttpRequest(ctx, req, runController) {
|
|
|
779
1385
|
return Promise.resolve(new Response("Not Found", { status: 404 }));
|
|
780
1386
|
}
|
|
781
1387
|
|
|
782
|
-
// src/server/
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
return { cmd: "npx", args: ["playwright", ...playwrightArgs] };
|
|
800
|
-
}
|
|
801
|
-
function descriptorLocation(d) {
|
|
802
|
-
return d.column === void 0 ? `${d.file}:${d.line}` : `${d.file}:${d.line}:${d.column}`;
|
|
803
|
-
}
|
|
804
|
-
function sharedProject(tests) {
|
|
805
|
-
const names = new Set(tests.map((t) => t.projectName ?? ""));
|
|
806
|
-
if (names.size === 1) {
|
|
807
|
-
const name = [...names][0];
|
|
808
|
-
return name === "" ? void 0 : name;
|
|
809
|
-
}
|
|
810
|
-
return void 0;
|
|
811
|
-
}
|
|
812
|
-
function gteMinor(version, major, minor) {
|
|
813
|
-
const match = /^(\d+)\.(\d+)/.exec(version.trim());
|
|
814
|
-
if (match === null) return false;
|
|
815
|
-
const maj = parseInt(match[1], 10);
|
|
816
|
-
const min = parseInt(match[2], 10);
|
|
817
|
-
if (maj !== major) return maj > major;
|
|
818
|
-
return min >= minor;
|
|
819
|
-
}
|
|
820
|
-
function resolvePlaywrightVersion(cwd) {
|
|
821
|
-
try {
|
|
822
|
-
const req = createRequire(join4(cwd, "package.json"));
|
|
823
|
-
const pkgPath = req.resolve("@playwright/test/package.json");
|
|
824
|
-
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
825
|
-
return typeof pkg === "object" && pkg !== null && "version" in pkg && typeof pkg.version === "string" ? pkg.version : null;
|
|
826
|
-
} catch {
|
|
827
|
-
return null;
|
|
828
|
-
}
|
|
829
|
-
}
|
|
830
|
-
function buildTestListEntries(tests) {
|
|
831
|
-
return tests.map((d) => {
|
|
832
|
-
const loc = descriptorLocation(d);
|
|
833
|
-
const title = d.titlePath.join(" \u203A ");
|
|
834
|
-
const prefix = d.projectName !== void 0 && d.projectName !== "" ? `[${d.projectName}] \u203A ` : "";
|
|
835
|
-
return `${prefix}${loc} \u203A ${title}`;
|
|
1388
|
+
// src/server/server-factories.ts
|
|
1389
|
+
function createServerRunController(routesContext, wsClients, reportData, port, setRunFiltered, saveReport, launcher) {
|
|
1390
|
+
return new RunController({
|
|
1391
|
+
getRunContext: () => routesContext.runContext ?? null,
|
|
1392
|
+
port,
|
|
1393
|
+
broadcast: (message) => {
|
|
1394
|
+
broadcastToBrowsers(wsClients, message);
|
|
1395
|
+
},
|
|
1396
|
+
setReportRunning: (running) => {
|
|
1397
|
+
reportData.isRunning = running;
|
|
1398
|
+
},
|
|
1399
|
+
setRunFiltered,
|
|
1400
|
+
containerPathMapping: routesContext.containerPathMapping,
|
|
1401
|
+
saveReport,
|
|
1402
|
+
spawn: createRealSpawn(),
|
|
1403
|
+
timers: createRealTimers(),
|
|
1404
|
+
launcher
|
|
836
1405
|
});
|
|
837
1406
|
}
|
|
838
|
-
function
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
}
|
|
843
|
-
function defaultDeleteTempFile(path) {
|
|
844
|
-
try {
|
|
845
|
-
unlinkSync(path);
|
|
846
|
-
} catch {
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
function resolveReporterDefault(cwd) {
|
|
850
|
-
try {
|
|
851
|
-
return createRequire(join4(cwd, "package.json")).resolve("@crvy/rprtr");
|
|
852
|
-
} catch {
|
|
853
|
-
}
|
|
854
|
-
try {
|
|
855
|
-
return createRequire(import.meta.url).resolve("@crvy/rprtr");
|
|
856
|
-
} catch {
|
|
857
|
-
return null;
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
function buildSpawnEnv(port) {
|
|
861
|
-
const env = {};
|
|
862
|
-
for (const [key, value] of Object.entries(process.env)) {
|
|
863
|
-
if (key === "CI") continue;
|
|
864
|
-
env[key] = value;
|
|
865
|
-
}
|
|
866
|
-
env.CRVY_RPRTR_SERVER_URL = `ws://localhost:${port}`;
|
|
867
|
-
env.PLAYWRIGHT_HTML_OPEN = "never";
|
|
868
|
-
return env;
|
|
869
|
-
}
|
|
870
|
-
var RunController = class {
|
|
871
|
-
constructor(deps) {
|
|
872
|
-
this.deps = deps;
|
|
873
|
-
}
|
|
874
|
-
child = null;
|
|
875
|
-
sigkillTimer = null;
|
|
876
|
-
testListPath = null;
|
|
877
|
-
get isRunning() {
|
|
878
|
-
return this.child !== null;
|
|
879
|
-
}
|
|
880
|
-
supportsTestList(cwd) {
|
|
881
|
-
const getVersion = this.deps.getPlaywrightVersion ?? resolvePlaywrightVersion;
|
|
882
|
-
const version = getVersion(cwd);
|
|
883
|
-
return version !== null && gteMinor(version, 1, 56);
|
|
884
|
-
}
|
|
885
|
-
cleanupTempFile() {
|
|
886
|
-
if (this.testListPath !== null) {
|
|
887
|
-
const del = this.deps.deleteTempFile ?? defaultDeleteTempFile;
|
|
888
|
-
del(this.testListPath);
|
|
889
|
-
this.testListPath = null;
|
|
890
|
-
}
|
|
891
|
-
}
|
|
892
|
-
start(filters) {
|
|
893
|
-
const ctx = this.deps.getRunContext();
|
|
894
|
-
if (ctx === null) return { ok: false, reason: "no-config" };
|
|
895
|
-
if (this.child !== null) return { ok: false, reason: "already-running" };
|
|
896
|
-
if (filters.tests !== void 0 && filters.tests.length === 0) {
|
|
897
|
-
return { ok: false, reason: "no-tests" };
|
|
898
|
-
}
|
|
899
|
-
const resolveReporter = this.deps.resolveReporter ?? resolveReporterDefault;
|
|
900
|
-
const reporterModule = resolveReporter(ctx.cwd);
|
|
901
|
-
const tests = filters.tests;
|
|
902
|
-
const useTestList = tests !== void 0 && tests.length > 1 && this.supportsTestList(ctx.cwd);
|
|
903
|
-
const args = ["test", "--config", ctx.configFile];
|
|
904
|
-
if (reporterModule !== null) args.push("--reporter", reporterModule);
|
|
905
|
-
if (useTestList && tests !== void 0) {
|
|
906
|
-
const content = buildTestListEntries(tests).join("\n");
|
|
907
|
-
const writeTemp = this.deps.writeTempFile ?? defaultWriteTempFile;
|
|
908
|
-
this.testListPath = writeTemp(content);
|
|
909
|
-
args.push("--test-list", this.testListPath);
|
|
910
|
-
} else if (tests !== void 0 && tests.length > 0) {
|
|
911
|
-
const project = sharedProject(tests);
|
|
912
|
-
if (project !== void 0) args.push("--project", project);
|
|
913
|
-
for (const d of tests) args.push(descriptorLocation(d));
|
|
914
|
-
}
|
|
915
|
-
const resolveLaunch = this.deps.resolveLaunch ?? resolvePlaywrightLaunch;
|
|
916
|
-
const { cmd, args: launchArgs } = resolveLaunch(ctx.cwd, args);
|
|
917
|
-
let child;
|
|
918
|
-
try {
|
|
919
|
-
child = this.deps.spawn(cmd, launchArgs, { cwd: ctx.cwd, env: buildSpawnEnv(this.deps.port), stdio: "inherit" });
|
|
920
|
-
} catch (err) {
|
|
921
|
-
this.cleanupTempFile();
|
|
922
|
-
throw err;
|
|
923
|
-
}
|
|
924
|
-
this.child = child;
|
|
925
|
-
child.on("exit", (code) => {
|
|
926
|
-
this.handleChildExit(code);
|
|
927
|
-
});
|
|
928
|
-
child.on("error", () => {
|
|
929
|
-
this.handleChildExit(null);
|
|
930
|
-
});
|
|
931
|
-
this.deps.setReportRunning(true);
|
|
932
|
-
this.deps.setRunFiltered?.(filters.tests !== void 0);
|
|
933
|
-
this.deps.broadcast({ type: "run-status", data: { running: true } });
|
|
934
|
-
return { ok: true };
|
|
935
|
-
}
|
|
936
|
-
stop() {
|
|
937
|
-
if (this.child === null) return { ok: false, reason: "not-running" };
|
|
938
|
-
if (this.sigkillTimer !== null) this.deps.timers.clearTimeout(this.sigkillTimer);
|
|
939
|
-
this.child.kill("SIGTERM");
|
|
940
|
-
this.sigkillTimer = this.deps.timers.setTimeout(() => {
|
|
941
|
-
if (this.child !== null) this.child.kill("SIGKILL");
|
|
942
|
-
}, STOP_GRACE_MS);
|
|
943
|
-
return { ok: true };
|
|
944
|
-
}
|
|
945
|
-
dispose() {
|
|
946
|
-
if (this.child === null) return;
|
|
947
|
-
if (this.sigkillTimer !== null) this.deps.timers.clearTimeout(this.sigkillTimer);
|
|
948
|
-
this.sigkillTimer = null;
|
|
949
|
-
this.child.kill("SIGKILL");
|
|
950
|
-
this.cleanupTempFile();
|
|
951
|
-
}
|
|
952
|
-
handleChildExit(code) {
|
|
953
|
-
if (this.child === null) return;
|
|
954
|
-
if (this.sigkillTimer !== null) {
|
|
955
|
-
this.deps.timers.clearTimeout(this.sigkillTimer);
|
|
956
|
-
this.sigkillTimer = null;
|
|
957
|
-
}
|
|
958
|
-
this.child = null;
|
|
959
|
-
this.cleanupTempFile();
|
|
960
|
-
if (code !== null && code !== 0) {
|
|
961
|
-
console.warn(`[RunController] playwright test exited with code ${code}`);
|
|
962
|
-
}
|
|
963
|
-
this.deps.setReportRunning(false);
|
|
964
|
-
this.deps.broadcast({ type: "run-status", data: { running: false } });
|
|
965
|
-
void this.deps.saveReport?.();
|
|
966
|
-
}
|
|
967
|
-
};
|
|
968
|
-
function createRealSpawn() {
|
|
969
|
-
return (cmd, args, opts) => {
|
|
970
|
-
const cp = spawn(cmd, args, opts);
|
|
971
|
-
return {
|
|
972
|
-
on: (event, cb) => cp.on(event, cb),
|
|
973
|
-
kill: (signal) => {
|
|
974
|
-
const sig = KNOWN_SIGNALS[signal];
|
|
975
|
-
if (sig !== void 0) cp.kill(sig);
|
|
976
|
-
}
|
|
977
|
-
};
|
|
1407
|
+
function createCloseHandler(persistence, runController) {
|
|
1408
|
+
return async () => {
|
|
1409
|
+
await persistence.dispose();
|
|
1410
|
+
runController.dispose();
|
|
978
1411
|
};
|
|
979
1412
|
}
|
|
980
|
-
function
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
1413
|
+
function createRunControllerAndHandlers(routesContext, wsClients, reportData, currentRunIds, port, persistence, launcher) {
|
|
1414
|
+
let isFilteredRun = false;
|
|
1415
|
+
const runController = createServerRunController(
|
|
1416
|
+
routesContext,
|
|
1417
|
+
wsClients,
|
|
1418
|
+
reportData,
|
|
1419
|
+
port,
|
|
1420
|
+
(filtered) => {
|
|
1421
|
+
isFilteredRun = filtered;
|
|
987
1422
|
},
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
1423
|
+
persistence.saveReport,
|
|
1424
|
+
launcher
|
|
1425
|
+
);
|
|
1426
|
+
const getHandlerContext = () => ({
|
|
1427
|
+
reportData,
|
|
1428
|
+
wsClients,
|
|
1429
|
+
currentRunIds,
|
|
1430
|
+
isFilteredRun,
|
|
1431
|
+
saveReport: persistence.saveReport,
|
|
1432
|
+
scheduleReportSave: persistence.scheduleReportSave,
|
|
1433
|
+
approvalRouting: routesContext.approvalRouting,
|
|
1434
|
+
routesContext,
|
|
1435
|
+
runController
|
|
1436
|
+
});
|
|
1437
|
+
return { runController, getHandlerContext };
|
|
992
1438
|
}
|
|
993
1439
|
|
|
994
1440
|
// src/server/app.ts
|
|
@@ -1080,19 +1526,19 @@ function createWebSocketMessageHandler(getHandlerContext) {
|
|
|
1080
1526
|
};
|
|
1081
1527
|
}
|
|
1082
1528
|
async function resolveStaticDir(staticDir) {
|
|
1083
|
-
const currentDir =
|
|
1529
|
+
const currentDir = dirname4(fileURLToPath(import.meta.url));
|
|
1084
1530
|
const candidates = staticDir === void 0 ? [
|
|
1085
1531
|
currentDir,
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
] : [staticDir,
|
|
1532
|
+
join6(currentDir, "dist"),
|
|
1533
|
+
join6(currentDir, "..", "dist"),
|
|
1534
|
+
join6(currentDir, "..", "..", "dist"),
|
|
1535
|
+
join6(currentDir, ".."),
|
|
1536
|
+
join6(currentDir, "..", "..")
|
|
1537
|
+
] : [staticDir, join6(staticDir, "dist")];
|
|
1092
1538
|
const resolvedCandidates = await Promise.all(
|
|
1093
1539
|
candidates.map(async (candidate) => ({
|
|
1094
1540
|
candidate,
|
|
1095
|
-
exists: await fileExists(
|
|
1541
|
+
exists: await fileExists(join6(candidate, "index.html"))
|
|
1096
1542
|
}))
|
|
1097
1543
|
);
|
|
1098
1544
|
const resolved = resolvedCandidates.find(({ exists }) => exists);
|
|
@@ -1103,34 +1549,31 @@ async function resolveStaticDir(staticDir) {
|
|
|
1103
1549
|
}
|
|
1104
1550
|
async function resolveReportPath(reportPath) {
|
|
1105
1551
|
if (await isDirectory(reportPath)) {
|
|
1106
|
-
return { reportFile:
|
|
1552
|
+
return { reportFile: join6(reportPath, "report.json"), offlineReportDir: reportPath };
|
|
1107
1553
|
}
|
|
1108
|
-
return { reportFile: reportPath, offlineReportDir:
|
|
1554
|
+
return { reportFile: reportPath, offlineReportDir: dirname4(reportPath) };
|
|
1109
1555
|
}
|
|
1110
1556
|
async function seedRunContext(routesContext, options) {
|
|
1111
1557
|
if (routesContext.runContext !== void 0) {
|
|
1112
1558
|
return;
|
|
1113
1559
|
}
|
|
1114
|
-
const configFile = options.playwrightConfig
|
|
1560
|
+
const configFile = await resolveSeedConfigFile(options.playwrightConfig, process.cwd());
|
|
1115
1561
|
if (configFile !== null) {
|
|
1116
1562
|
routesContext.runContext = { configFile, cwd: process.cwd() };
|
|
1117
1563
|
}
|
|
1118
1564
|
}
|
|
1119
|
-
function
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
},
|
|
1129
|
-
setRunFiltered,
|
|
1130
|
-
saveReport,
|
|
1131
|
-
spawn: createRealSpawn(),
|
|
1132
|
-
timers: createRealTimers()
|
|
1565
|
+
async function setupRoutesContext(options, reportData, staticDir, saveReport, port) {
|
|
1566
|
+
const { launcher, routesContextOptions } = await resolveRunBackend({
|
|
1567
|
+
runMode: options.runMode,
|
|
1568
|
+
docker: options.docker,
|
|
1569
|
+
port
|
|
1570
|
+
});
|
|
1571
|
+
const routesContext = createRoutesContext(reportData, staticDir, saveReport, {
|
|
1572
|
+
...options,
|
|
1573
|
+
...routesContextOptions
|
|
1133
1574
|
});
|
|
1575
|
+
await seedRunContext(routesContext, options);
|
|
1576
|
+
return { routesContext, launcher };
|
|
1134
1577
|
}
|
|
1135
1578
|
async function createServerApp(options = {}) {
|
|
1136
1579
|
const port = options.port ?? 3e3;
|
|
@@ -1141,30 +1584,22 @@ async function createServerApp(options = {}) {
|
|
|
1141
1584
|
const wsClients = /* @__PURE__ */ new Set();
|
|
1142
1585
|
const currentRunIds = /* @__PURE__ */ new Set();
|
|
1143
1586
|
const persistence = createReportPersistence(reportFile, reportData);
|
|
1144
|
-
const routesContext
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1587
|
+
const { routesContext, launcher } = await setupRoutesContext(
|
|
1588
|
+
options,
|
|
1589
|
+
reportData,
|
|
1590
|
+
staticDir,
|
|
1591
|
+
persistence.saveReport,
|
|
1592
|
+
port
|
|
1593
|
+
);
|
|
1594
|
+
const { runController, getHandlerContext } = createRunControllerAndHandlers(
|
|
1148
1595
|
routesContext,
|
|
1149
1596
|
wsClients,
|
|
1150
1597
|
reportData,
|
|
1598
|
+
currentRunIds,
|
|
1151
1599
|
port,
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
},
|
|
1155
|
-
persistence.saveReport
|
|
1600
|
+
persistence,
|
|
1601
|
+
launcher
|
|
1156
1602
|
);
|
|
1157
|
-
const getHandlerContext = () => ({
|
|
1158
|
-
reportData,
|
|
1159
|
-
wsClients,
|
|
1160
|
-
currentRunIds,
|
|
1161
|
-
isFilteredRun,
|
|
1162
|
-
saveReport: persistence.saveReport,
|
|
1163
|
-
scheduleReportSave: persistence.scheduleReportSave,
|
|
1164
|
-
approvalRouting: routesContext.approvalRouting,
|
|
1165
|
-
routesContext,
|
|
1166
|
-
runController
|
|
1167
|
-
});
|
|
1168
1603
|
const handleRequest = (req) => handleHttpRequest(routesContext, req, runController);
|
|
1169
1604
|
const handleWebSocketMessage = createWebSocketMessageHandler(getHandlerContext);
|
|
1170
1605
|
await loadReport(reportFile, reportData);
|
|
@@ -1177,12 +1612,6 @@ async function createServerApp(options = {}) {
|
|
|
1177
1612
|
handleWebSocketMessage
|
|
1178
1613
|
};
|
|
1179
1614
|
}
|
|
1180
|
-
function createCloseHandler(persistence, runController) {
|
|
1181
|
-
return async () => {
|
|
1182
|
-
await persistence.dispose();
|
|
1183
|
-
runController.dispose();
|
|
1184
|
-
};
|
|
1185
|
-
}
|
|
1186
1615
|
|
|
1187
1616
|
// src/server/bun-adapter.ts
|
|
1188
1617
|
function logWebSocketError(prefix, error) {
|
|
@@ -1359,7 +1788,7 @@ function attachWebSocketServer(server, app) {
|
|
|
1359
1788
|
});
|
|
1360
1789
|
}
|
|
1361
1790
|
async function listen(server, port) {
|
|
1362
|
-
await new Promise((
|
|
1791
|
+
await new Promise((resolve5, reject) => {
|
|
1363
1792
|
const onError = (error) => {
|
|
1364
1793
|
server.off("error", onError);
|
|
1365
1794
|
reject(error);
|
|
@@ -1367,7 +1796,7 @@ async function listen(server, port) {
|
|
|
1367
1796
|
server.on("error", onError);
|
|
1368
1797
|
server.listen(port, () => {
|
|
1369
1798
|
server.off("error", onError);
|
|
1370
|
-
|
|
1799
|
+
resolve5();
|
|
1371
1800
|
});
|
|
1372
1801
|
});
|
|
1373
1802
|
}
|