@crvy/rprtr 0.2.4 → 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.
Files changed (49) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +33 -0
  3. package/dist/{chunk-HAFWYUNO.js → chunk-4YJL655E.js} +16 -4
  4. package/dist/{chunk-473CWZ4V.js → chunk-IFHYWPXG.js} +710 -281
  5. package/dist/cli.d.ts +5 -1
  6. package/dist/cli.d.ts.map +1 -1
  7. package/dist/cli.js +24 -4
  8. package/dist/index.css +7 -0
  9. package/dist/index.js +152 -105
  10. package/dist/reporter-artifact-ops.d.ts +0 -2
  11. package/dist/reporter-artifact-ops.d.ts.map +1 -1
  12. package/dist/reporter.cjs +32 -38
  13. package/dist/reporter.d.ts +2 -1
  14. package/dist/reporter.d.ts.map +1 -1
  15. package/dist/reporter.js +27 -43
  16. package/dist/schemas/http.d.ts +2 -0
  17. package/dist/schemas/http.d.ts.map +1 -1
  18. package/dist/schemas.d.ts +14 -0
  19. package/dist/schemas.d.ts.map +1 -1
  20. package/dist/server/app.d.ts +6 -0
  21. package/dist/server/app.d.ts.map +1 -1
  22. package/dist/server/artifact-routes.d.ts.map +1 -1
  23. package/dist/server/docker-launcher.d.ts +27 -0
  24. package/dist/server/docker-launcher.d.ts.map +1 -0
  25. package/dist/server/docker-support.d.ts +66 -0
  26. package/dist/server/docker-support.d.ts.map +1 -0
  27. package/dist/server/handlers.d.ts +1 -1
  28. package/dist/server/handlers.d.ts.map +1 -1
  29. package/dist/server/launcher-resolver.d.ts +23 -0
  30. package/dist/server/launcher-resolver.d.ts.map +1 -0
  31. package/dist/server/playwright-config.d.ts +6 -0
  32. package/dist/server/playwright-config.d.ts.map +1 -1
  33. package/dist/server/routes-context.d.ts +6 -1
  34. package/dist/server/routes-context.d.ts.map +1 -1
  35. package/dist/server/routes.d.ts +7 -0
  36. package/dist/server/routes.d.ts.map +1 -1
  37. package/dist/server/run-controller.d.ts +19 -19
  38. package/dist/server/run-controller.d.ts.map +1 -1
  39. package/dist/server/run-launcher.d.ts +45 -0
  40. package/dist/server/run-launcher.d.ts.map +1 -0
  41. package/dist/server/run-mode.d.ts +15 -0
  42. package/dist/server/run-mode.d.ts.map +1 -0
  43. package/dist/server/server-factories.d.ts +21 -0
  44. package/dist/server/server-factories.d.ts.map +1 -0
  45. package/dist/server.cjs +885 -446
  46. package/dist/server.js +2 -2
  47. package/dist/types.d.ts +2 -0
  48. package/dist/types.d.ts.map +1 -1
  49. 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-HAFWYUNO.js";
19
+ } from "./chunk-4YJL655E.js";
19
20
 
20
21
  // src/server/app.ts
21
- import { dirname as dirname3, join as join5 } from "path";
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 = relative(resolve(root), resolvedTarget);
235
- return rel === "" || !rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute(rel);
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 testFile = test.location?.file;
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 handleRegister(ctx, data) {
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 = { configFile: data.configFile, cwd: data.cwd };
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 join2 } from "path";
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 = join2(cwd, file);
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 join3 } from "path";
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(join3(ctx.staticDir, "index.html"), "text/html");
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 join3(ctx.reportData.screenshotDir, actualUrl.slice("/screenshots/".length));
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 = join3(ctx.staticDir, path);
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/run-controller.ts
783
- import { spawn } from "child_process";
784
- import { readFileSync, unlinkSync, writeFileSync } from "node:fs";
785
- import { createRequire } from "node:module";
786
- import { tmpdir } from "node:os";
787
- import { join as join4 } from "node:path";
788
- import { resolveCommand } from "package-manager-detector/commands";
789
- import { getUserAgent } from "package-manager-detector/detect";
790
- var STOP_GRACE_MS = 5e3;
791
- var KNOWN_SIGNALS = {
792
- SIGTERM: "SIGTERM",
793
- SIGKILL: "SIGKILL"
794
- };
795
- function resolvePlaywrightLaunch(cwd, playwrightArgs) {
796
- const agent = getUserAgent();
797
- const resolved = agent === null ? null : resolveCommand(agent, "execute-local", ["playwright", ...playwrightArgs]);
798
- if (resolved !== null) return { cmd: resolved.command, args: resolved.args };
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 defaultWriteTempFile(content) {
839
- const path = join4(tmpdir(), `crvy-rprtr-test-list-${process.pid}-${Date.now()}.txt`);
840
- writeFileSync(path, content, "utf8");
841
- return path;
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 createRealTimers() {
981
- const pending = [];
982
- return {
983
- setTimeout: (fn, ms) => {
984
- const id = setTimeout(fn, ms);
985
- pending.push(id);
986
- return id;
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
- clearTimeout: () => {
989
- for (const h of pending.splice(0)) clearTimeout(h);
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 = dirname3(fileURLToPath(import.meta.url));
1529
+ const currentDir = dirname4(fileURLToPath(import.meta.url));
1084
1530
  const candidates = staticDir === void 0 ? [
1085
1531
  currentDir,
1086
- join5(currentDir, "dist"),
1087
- join5(currentDir, "..", "dist"),
1088
- join5(currentDir, "..", "..", "dist"),
1089
- join5(currentDir, ".."),
1090
- join5(currentDir, "..", "..")
1091
- ] : [staticDir, join5(staticDir, "dist")];
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(join5(candidate, "index.html"))
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: join5(reportPath, "report.json"), offlineReportDir: reportPath };
1552
+ return { reportFile: join6(reportPath, "report.json"), offlineReportDir: reportPath };
1107
1553
  }
1108
- return { reportFile: reportPath, offlineReportDir: dirname3(reportPath) };
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 ?? await resolvePlaywrightConfig(process.cwd());
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 createServerRunController(routesContext, wsClients, reportData, port, setRunFiltered, saveReport) {
1120
- return new RunController({
1121
- getRunContext: () => routesContext.runContext ?? null,
1122
- port,
1123
- broadcast: (message) => {
1124
- broadcastToBrowsers(wsClients, message);
1125
- },
1126
- setReportRunning: (running) => {
1127
- reportData.isRunning = running;
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 = createRoutesContext(reportData, staticDir, persistence.saveReport, options);
1145
- await seedRunContext(routesContext, options);
1146
- let isFilteredRun = false;
1147
- const runController = createServerRunController(
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
- (filtered) => {
1153
- isFilteredRun = filtered;
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((resolve3, reject) => {
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
- resolve3();
1799
+ resolve5();
1371
1800
  });
1372
1801
  });
1373
1802
  }