@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.
Files changed (49) hide show
  1. package/CHANGELOG.md +41 -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 +7 -0
  6. package/dist/cli.d.ts.map +1 -1
  7. package/dist/cli.js +57 -6
  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
package/dist/server.cjs CHANGED
@@ -35,7 +35,7 @@ __export(server_exports, {
35
35
  module.exports = __toCommonJS(server_exports);
36
36
 
37
37
  // src/server/app.ts
38
- var import_path8 = require("path");
38
+ var import_path9 = require("path");
39
39
  var import_url = require("url");
40
40
 
41
41
  // src/offline-reports.ts
@@ -276,13 +276,14 @@ var RunTestDescriptorSchema = import_zod.z.object({
276
276
  titlePath: import_zod.z.array(import_zod.z.string())
277
277
  });
278
278
  var RunRequestBodySchema = import_zod.z.object({
279
- tests: import_zod.z.array(RunTestDescriptorSchema).optional()
279
+ tests: import_zod.z.array(RunTestDescriptorSchema).optional(),
280
+ update: import_zod.z.boolean().optional()
280
281
  });
281
282
  var RunResponseSchema = import_zod.z.discriminatedUnion("ok", [
282
283
  import_zod.z.object({ ok: import_zod.z.literal(true) }),
283
284
  import_zod.z.object({
284
285
  ok: import_zod.z.literal(false),
285
- reason: import_zod.z.enum(["no-config", "already-running", "no-tests"])
286
+ reason: import_zod.z.enum(["no-config", "already-running", "no-tests", "docker-unavailable"])
286
287
  })
287
288
  ]);
288
289
  var StopResponseSchema = import_zod.z.discriminatedUnion("ok", [
@@ -389,7 +390,9 @@ var WebSocketMessageSchema = import_zod2.z.discriminatedUnion("type", [
389
390
  import_zod2.z.object({
390
391
  type: import_zod2.z.literal("run-status"),
391
392
  data: import_zod2.z.object({
392
- running: import_zod2.z.boolean()
393
+ running: import_zod2.z.boolean(),
394
+ mode: import_zod2.z.enum(["local", "docker"]).optional(),
395
+ phase: import_zod2.z.string().optional()
393
396
  })
394
397
  })
395
398
  ]);
@@ -419,6 +422,7 @@ var RunEndDataSchema = import_zod2.z.object({
419
422
  var RegisterDataSchema = import_zod2.z.object({
420
423
  playwrightSnapshotDir: import_zod2.z.string().optional(),
421
424
  playwrightTestDir: import_zod2.z.string().optional(),
425
+ playwrightRootDir: import_zod2.z.string().optional(),
422
426
  playwrightSnapshotPathTemplate: import_zod2.z.string().optional(),
423
427
  playwrightToHaveScreenshotPathTemplate: import_zod2.z.string().optional(),
424
428
  configFile: import_zod2.z.string().optional(),
@@ -451,7 +455,8 @@ var ReportApiResponseSchema = import_zod2.z.object({
451
455
  tests: import_zod2.z.record(import_zod2.z.string(), TestDataSchema),
452
456
  isUpdateMode: import_zod2.z.boolean().optional(),
453
457
  isRunning: import_zod2.z.boolean().optional(),
454
- runEnabled: import_zod2.z.boolean().optional()
458
+ runEnabled: import_zod2.z.boolean().optional(),
459
+ runMode: import_zod2.z.enum(["local", "docker"]).optional()
455
460
  });
456
461
  var ClientBootstrapDataSchema = import_zod2.z.object({
457
462
  report: ReportApiResponseSchema.extend({
@@ -654,201 +659,738 @@ function mergeOfflineReportsIntoTests(existingTests, offlineReports, options = {
654
659
  };
655
660
  }
656
661
 
657
- // src/server/handlers.ts
658
- var import_fs2 = require("fs");
659
-
660
- // src/server/artifact-routes.ts
661
- var import_fs = require("fs");
662
- var import_promises3 = require("fs/promises");
663
- var import_path5 = require("path");
664
-
665
- // src/snapshot-path-resolver.ts
666
- var import_crypto = require("crypto");
667
- var import_path3 = require("path");
668
- var DEFAULT_SCREENSHOT_TEMPLATE = "{snapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}{-projectName}{-snapshotSuffix}{ext}";
669
- var WINDOWS_FILESYSTEM_FRIENDLY_LENGTH = 60;
670
- function isUnsafeFilePathCharacter(character) {
671
- const codePoint = character.codePointAt(0);
672
- return codePoint !== void 0 && (codePoint <= 44 || codePoint >= 46 && codePoint <= 47 || codePoint >= 58 && codePoint <= 64 || codePoint >= 91 && codePoint <= 96 || codePoint >= 123 && codePoint <= 127);
673
- }
674
- function sanitizeForFilePath(value) {
675
- return Array.from(value).reduce(
676
- (state, character) => {
677
- const unsafeCharacter = isUnsafeFilePathCharacter(character);
678
- return unsafeCharacter ? state.previousCharacterWasUnsafe ? state : {
679
- value: `${state.value}-`,
680
- previousCharacterWasUnsafe: true
681
- } : {
682
- value: `${state.value}${character}`,
683
- previousCharacterWasUnsafe: false
684
- };
685
- },
686
- {
687
- value: "",
688
- previousCharacterWasUnsafe: false
689
- }
690
- ).value;
662
+ // src/server/docker-support.ts
663
+ var import_child_process = require("child_process");
664
+ var import_node_fs = require("node:fs");
665
+ var import_node_module = require("node:module");
666
+ var import_node_path = require("node:path");
667
+ var import_detect = require("package-manager-detector/detect");
668
+ function createDockerExec() {
669
+ return (args) => new Promise((resolve6, reject) => {
670
+ const child = (0, import_child_process.spawn)("docker", args, { stdio: ["ignore", "pipe", "pipe"] });
671
+ const stdout = [];
672
+ const stderr = [];
673
+ child.stdout.on("data", (chunk) => stdout.push(chunk));
674
+ child.stderr.on("data", (chunk) => stderr.push(chunk));
675
+ child.on("error", reject);
676
+ child.on("close", (code) => {
677
+ resolve6({
678
+ exitCode: code ?? 1,
679
+ stdout: Buffer.concat(stdout).toString(),
680
+ stderr: Buffer.concat(stderr).toString()
681
+ });
682
+ });
683
+ });
691
684
  }
692
- function trimLongString(value, length = WINDOWS_FILESYSTEM_FRIENDLY_LENGTH) {
693
- if (value.length <= length) {
694
- return value;
685
+ async function probeDockerDaemon(exec) {
686
+ try {
687
+ const result = await exec(["info"]);
688
+ return result.exitCode === 0;
689
+ } catch {
690
+ return false;
695
691
  }
696
- const hash = (0, import_crypto.createHash)("sha1").update(value).digest("hex");
697
- const middle = `-${hash.slice(0, 5)}-`;
698
- const start = Math.floor((length - middle.length) / 2);
699
- const end = length - middle.length - start;
700
- return value.slice(0, start) + middle + value.slice(-end);
701
- }
702
- function sanitizeFilePathBeforeExtension(filePath, extension = (0, import_path3.extname)(filePath)) {
703
- const base = filePath.slice(0, filePath.length - extension.length);
704
- return sanitizeForFilePath(base) + extension;
705
- }
706
- function addSuffixToFilePath(filePath, suffix) {
707
- const extension = (0, import_path3.extname)(filePath);
708
- return filePath.slice(0, filePath.length - extension.length) + suffix + extension;
709
- }
710
- function normalizedSnapshotDir(config) {
711
- return (0, import_path3.resolve)(config.configDir, config.snapshotDir);
712
692
  }
713
- function templateValue(template, token, value) {
714
- return template.replace(
715
- new RegExp(`\\{(.)?${token}\\}`, "g"),
716
- (_, prefix) => value === "" ? "" : `${prefix ?? ""}${value}`
717
- );
693
+ async function isDockerImagePresent(exec, image) {
694
+ try {
695
+ const result = await exec(["image", "inspect", image]);
696
+ return result.exitCode === 0;
697
+ } catch {
698
+ return false;
699
+ }
718
700
  }
719
- function applyTemplate(input, nameArgument, extension) {
720
- const template = input.config.toHaveScreenshotPathTemplate ?? input.config.snapshotPathTemplate ?? DEFAULT_SCREENSHOT_TEMPLATE;
721
- const relativeTestFilePath = (0, import_path3.relative)(input.config.testDir, input.testFile);
722
- const parsed = (0, import_path3.parse)(relativeTestFilePath);
723
- const tokens = [
724
- ["testDir", input.config.testDir],
725
- ["snapshotDir", normalizedSnapshotDir(input.config)],
726
- ["snapshotSuffix", input.config.snapshotSuffix],
727
- ["testFileDir", parsed.dir],
728
- ["platform", process.platform],
729
- ["projectName", sanitizeForFilePath(input.config.projectName)],
730
- ["testName", ""],
731
- ["testFileName", parsed.base],
732
- ["testFilePath", relativeTestFilePath],
733
- ["arg", nameArgument],
734
- ["ext", extension]
735
- ];
736
- const snapshotPath = tokens.reduce(
737
- (currentTemplate, [token, value]) => templateValue(currentTemplate, token, value),
738
- template
739
- );
740
- return (0, import_path3.resolve)(input.config.configDir, snapshotPath);
701
+ async function pullDockerImage(exec, image) {
702
+ try {
703
+ const result = await exec(["pull", image]);
704
+ return result.exitCode === 0;
705
+ } catch {
706
+ return false;
707
+ }
741
708
  }
742
- function removeExtension(filePath, extension = (0, import_path3.extname)(filePath)) {
743
- return filePath.slice(0, filePath.length - extension.length);
709
+ async function forceRemoveContainer(exec, name) {
710
+ try {
711
+ await exec(["rm", "-f", name]);
712
+ } catch {
713
+ }
744
714
  }
745
- function snapshotNameParts(declaredName) {
746
- const extension = (0, import_path3.extname)(declaredName) || ".png";
747
- return {
748
- extension,
749
- filePath: (0, import_path3.extname)(declaredName) === "" ? `${declaredName}${extension}` : declaredName
750
- };
715
+ function resolveDockerImage(options) {
716
+ if (options.image !== void 0 && options.image !== "") return options.image;
717
+ if (options.version === null) return null;
718
+ return `mcr.microsoft.com/playwright:v${options.version}-noble`;
751
719
  }
752
- function filePathForOccurrence(filePath, occurrenceIndex) {
753
- return occurrenceIndex === 1 ? filePath : addSuffixToFilePath(filePath, `-${occurrenceIndex - 1}`);
720
+ var CONTAINER_INVOKERS = {
721
+ npm: ["npx"],
722
+ pnpm: ["pnpm", "exec"],
723
+ yarn: ["yarn"],
724
+ bun: ["bunx"]
725
+ };
726
+ var DEFAULT_CONTAINER_COMMAND = ["npx"];
727
+ function resolveContainerCommand(input) {
728
+ if (input.command !== void 0 && input.command.length > 0) return input.command;
729
+ if (!input.hasCustomImage) return [...DEFAULT_CONTAINER_COMMAND];
730
+ if (input.detectedAgentName === void 0 || input.detectedAgentName === null) {
731
+ input.warn?.("Could not detect a package manager for the custom docker image; falling back to npx.");
732
+ return [...DEFAULT_CONTAINER_COMMAND];
733
+ }
734
+ const invoker = CONTAINER_INVOKERS[input.detectedAgentName];
735
+ if (invoker === void 0) {
736
+ input.warn?.(`Package manager "${input.detectedAgentName}" is not supported in docker mode; falling back to npx.`);
737
+ return [...DEFAULT_CONTAINER_COMMAND];
738
+ }
739
+ return [...invoker];
740
+ }
741
+ var detectProjectAgent = async (cwd) => {
742
+ try {
743
+ const result = await (0, import_detect.detect)({ cwd, strategies: ["lockfile", "packageManager-field"] });
744
+ return result === null ? null : { name: result.name, agent: result.agent };
745
+ } catch {
746
+ return null;
747
+ }
748
+ };
749
+ function rewriteContainerPath(path, mapping) {
750
+ if (path === mapping.from) return mapping.to;
751
+ if (path.startsWith(`${mapping.from}/`)) return mapping.to + path.slice(mapping.from.length);
752
+ return path;
754
753
  }
755
- function createResolvedBaselineTarget(input, declaration, nameArgument, extension) {
756
- return {
757
- visualName: declaration.visualName,
758
- attachmentBaseName: declaration.visualName,
759
- artifactBaseName: sanitizeForFilePath(declaration.visualName),
760
- snapshotPath: applyTemplate(input, nameArgument, extension)
761
- };
754
+ function resolvePlaywrightVersion(cwd) {
755
+ try {
756
+ const req = (0, import_node_module.createRequire)((0, import_node_path.join)(cwd, "package.json"));
757
+ const pkgPath = req.resolve("@playwright/test/package.json");
758
+ const pkg = JSON.parse((0, import_node_fs.readFileSync)(pkgPath, "utf8"));
759
+ return typeof pkg === "object" && pkg !== null && "version" in pkg && typeof pkg.version === "string" ? pkg.version : null;
760
+ } catch {
761
+ return null;
762
+ }
762
763
  }
763
- function resolveStringCallTarget(input, declaration) {
764
- const { extension, filePath } = snapshotNameParts(declaration.declaredName);
765
- const occurrenceFilePath = filePathForOccurrence(filePath, declaration.occurrenceIndex);
766
- const sanitizedNameWithExtension = sanitizeFilePathBeforeExtension(occurrenceFilePath, extension);
767
- const nameArgument = removeExtension(sanitizedNameWithExtension, extension);
768
- return createResolvedBaselineTarget(input, declaration, nameArgument, extension);
764
+ function rewriteContainerTestDescriptors(tests, mapping) {
765
+ if (tests === void 0 || mapping === void 0) return tests;
766
+ return tests.map((d) => ({ ...d, file: rewriteContainerPath(d.file, mapping) }));
769
767
  }
770
- function resolveArrayCallTarget(input, declaration) {
771
- const { extension, filePath } = snapshotNameParts(declaration.declaredName);
772
- const occurrenceFilePath = filePathForOccurrence(filePath, declaration.occurrenceIndex);
773
- const nameArgument = (0, import_path3.join)((0, import_path3.dirname)(occurrenceFilePath), (0, import_path3.basename)(occurrenceFilePath, extension));
774
- return createResolvedBaselineTarget(input, declaration, nameArgument, extension);
768
+ function testListEntry(d, file) {
769
+ const loc = d.column === void 0 ? `${file}:${d.line}` : `${file}:${d.line}:${d.column}`;
770
+ const title = d.titlePath.join(" \u203A ");
771
+ const prefix = d.projectName !== void 0 && d.projectName !== "" ? `[${d.projectName}] \u203A ` : "";
772
+ return `${prefix}${loc} \u203A ${title}`;
775
773
  }
776
- function resolveNamedTarget(input, declaration) {
777
- if (!declaration.declaredName.includes("/")) {
778
- return resolveStringCallTarget(input, declaration);
779
- }
780
- const stringCallTarget = resolveStringCallTarget(input, declaration);
781
- const arrayCallTarget = resolveArrayCallTarget(input, declaration);
782
- if (stringCallTarget.snapshotPath === arrayCallTarget.snapshotPath) {
783
- return stringCallTarget;
784
- }
785
- if (input.snapshotPathExists === void 0) {
786
- return void 0;
787
- }
788
- const stringCallTargetExists = input.snapshotPathExists(stringCallTarget.snapshotPath);
789
- const arrayCallTargetExists = input.snapshotPathExists(arrayCallTarget.snapshotPath);
790
- if (stringCallTargetExists === arrayCallTargetExists) {
791
- return void 0;
774
+ function buildTestListEntries(tests, rootDir, cwd) {
775
+ if (rootDir !== void 0) {
776
+ return tests.map((d) => testListEntry(d, (0, import_node_path.isAbsolute)(d.file) ? (0, import_node_path.relative)(rootDir, d.file) || d.file : d.file));
792
777
  }
793
- return stringCallTargetExists ? stringCallTarget : arrayCallTarget;
794
- }
795
- function reporterTitlesWithoutProjectAndFile(reporterTitlePath2) {
796
- return reporterTitlePath2.slice(3).filter((part) => part !== "");
797
- }
798
- function anonymousNameFromTitles(titles, occurrenceIndex) {
799
- return sanitizeFilePathBeforeExtension(trimLongString(`${titles.join(" ")} ${occurrenceIndex}.png`), ".png");
778
+ return tests.flatMap((d) => {
779
+ if (!(0, import_node_path.isAbsolute)(d.file)) return [testListEntry(d, d.file)];
780
+ const rel = (0, import_node_path.relative)(cwd ?? process.cwd(), d.file);
781
+ const segments = rel.split(import_node_path.sep);
782
+ return segments.map((_, i) => testListEntry(d, segments.slice(i).join(import_node_path.sep)));
783
+ });
800
784
  }
801
- function anonymousName(reporterTitlePath2, occurrenceIndex) {
802
- return anonymousNameFromTitles(reporterTitlesWithoutProjectAndFile(reporterTitlePath2), occurrenceIndex);
785
+
786
+ // src/server/run-controller.ts
787
+ var import_child_process2 = require("child_process");
788
+ var import_node_fs2 = require("node:fs");
789
+ var import_node_module2 = require("node:module");
790
+ var import_node_os = require("node:os");
791
+ var import_node_path2 = require("node:path");
792
+
793
+ // src/server/run-launcher.ts
794
+ var import_commands = require("package-manager-detector/commands");
795
+ var import_detect2 = require("package-manager-detector/detect");
796
+ function resolvePlaywrightLaunch(cwd, playwrightArgs) {
797
+ const agent = (0, import_detect2.getUserAgent)();
798
+ const resolved = agent === null ? null : (0, import_commands.resolveCommand)(agent, "execute-local", ["playwright", ...playwrightArgs]);
799
+ if (resolved !== null) return { cmd: resolved.command, args: resolved.args };
800
+ return { cmd: "npx", args: ["playwright", ...playwrightArgs] };
803
801
  }
804
- function resolveTarget(input, declaration) {
805
- switch (declaration.kind) {
806
- case "named":
807
- return typeof declaration.declaredName === "string" && declaration.declaredName !== "" ? resolveNamedTarget(input, declaration) : void 0;
808
- case "unnamed": {
809
- const anonymousFileName = anonymousName(input.reporterTitlePath, declaration.occurrenceIndex);
810
- const extension = ".png";
811
- const nameArgument = (0, import_path3.join)((0, import_path3.dirname)(anonymousFileName), (0, import_path3.basename)(anonymousFileName, extension));
812
- return createResolvedBaselineTarget(input, declaration, nameArgument, extension);
813
- }
802
+ function buildSpawnEnv(port, baseEnv = process.env) {
803
+ const env = {};
804
+ for (const [key, value] of Object.entries(baseEnv)) {
805
+ if (key === "CI") continue;
806
+ env[key] = value;
814
807
  }
808
+ env.CRVY_RPRTR_SERVER_URL = `ws://localhost:${port}`;
809
+ env.PLAYWRIGHT_HTML_OPEN = "never";
810
+ return env;
815
811
  }
816
- function resolveBaselineTargets(input) {
817
- return input.declarations.flatMap((declaration) => {
818
- const resolvedTarget = resolveTarget(input, declaration);
819
- return resolvedTarget === void 0 ? [] : [resolvedTarget];
820
- });
812
+ function createLocalLauncher(options) {
813
+ return {
814
+ mode: "local",
815
+ launch({ ctx, playwrightArgs }) {
816
+ const resolve6 = options.resolveLaunch ?? resolvePlaywrightLaunch;
817
+ const { cmd, args } = resolve6(ctx.cwd, playwrightArgs);
818
+ return { cmd, args, env: buildSpawnEnv(options.port, options.env) };
819
+ }
820
+ };
821
821
  }
822
822
 
823
- // src/server/utils.ts
824
- var import_path4 = require("path");
825
- var LIVE_UPDATES_WEBSOCKET_PATH = "/";
826
- function broadcastToBrowsers(wsClients, msg) {
827
- const payload = JSON.stringify(msg);
828
- wsClients.forEach((ws) => {
829
- ws.send(payload);
830
- });
823
+ // src/server/run-controller.ts
824
+ var import_meta = { url: require("url").pathToFileURL(__filename).href };
825
+ var STOP_GRACE_MS = 5e3;
826
+ var KNOWN_SIGNALS = {
827
+ SIGTERM: "SIGTERM",
828
+ SIGKILL: "SIGKILL"
829
+ };
830
+ function sharedProject(tests) {
831
+ const names = new Set(tests.map((t) => t.projectName ?? ""));
832
+ if (names.size === 1) {
833
+ const name = [...names][0];
834
+ return name === "" ? void 0 : name;
835
+ }
836
+ return void 0;
831
837
  }
832
- function isWebSocketUpgradeRequest(req) {
833
- return new URL(req.url).pathname === LIVE_UPDATES_WEBSOCKET_PATH && req.headers.get("upgrade")?.toLowerCase() === "websocket";
838
+ function gteMinor(version, major, minor) {
839
+ const match = /^(\d+)\.(\d+)/.exec(version.trim());
840
+ if (match === null) return false;
841
+ const maj = parseInt(match[1], 10);
842
+ const min = parseInt(match[2], 10);
843
+ if (maj !== major) return maj > major;
844
+ return min >= minor;
834
845
  }
835
- function isPathWithinRoots(target, roots) {
836
- const resolvedTarget = (0, import_path4.resolve)(target);
837
- return roots.some((root) => {
838
- const rel = (0, import_path4.relative)((0, import_path4.resolve)(root), resolvedTarget);
839
- return rel === "" || !rel.startsWith(`..${import_path4.sep}`) && rel !== ".." && !(0, import_path4.isAbsolute)(rel);
840
- });
846
+ var defaultWriteTempFile = (content) => {
847
+ const path = (0, import_node_path2.join)((0, import_node_os.tmpdir)(), `crvy-rprtr-test-list-${process.pid}-${Date.now()}.txt`);
848
+ (0, import_node_fs2.writeFileSync)(path, content, "utf8");
849
+ return path;
850
+ };
851
+ function defaultDeleteTempFile(path) {
852
+ try {
853
+ (0, import_node_fs2.unlinkSync)(path);
854
+ } catch {
855
+ }
841
856
  }
842
-
843
- // src/server/artifact-routes.ts
844
- async function realpathOrNull(path) {
857
+ function resolveReporterDefault(cwd) {
845
858
  try {
846
- return await (0, import_promises3.realpath)(path);
859
+ return (0, import_node_module2.createRequire)((0, import_node_path2.join)(cwd, "package.json")).resolve("@crvy/rprtr");
860
+ } catch {
861
+ }
862
+ try {
863
+ return (0, import_node_module2.createRequire)(import_meta.url).resolve("@crvy/rprtr");
847
864
  } catch {
848
865
  return null;
849
866
  }
850
867
  }
851
- async function handleFile(ctx, req) {
868
+ var RunController = class {
869
+ constructor(deps) {
870
+ this.deps = deps;
871
+ }
872
+ child = null;
873
+ sigkillTimer = null;
874
+ testListPath = null;
875
+ get isRunning() {
876
+ return this.child !== null;
877
+ }
878
+ supportsTestList(cwd) {
879
+ const getVersion = this.deps.getPlaywrightVersion ?? resolvePlaywrightVersion;
880
+ const version = getVersion(cwd);
881
+ return version !== null && gteMinor(version, 1, 56);
882
+ }
883
+ cleanupTempFile() {
884
+ if (this.testListPath !== null) {
885
+ const del = this.deps.deleteTempFile ?? defaultDeleteTempFile;
886
+ del(this.testListPath);
887
+ this.testListPath = null;
888
+ }
889
+ }
890
+ buildPlaywrightArgs(ctx, filters, tests) {
891
+ const resolveReporter = this.deps.resolveReporter ?? resolveReporterDefault;
892
+ const reporterModule = resolveReporter(ctx.cwd);
893
+ const useTestList = tests !== void 0 && (tests.length > 1 || this.deps.containerPathMapping !== void 0) && this.supportsTestList(ctx.cwd);
894
+ const args = ["test", "--config", ctx.configFile];
895
+ if (reporterModule !== null) args.push("--reporter", reporterModule);
896
+ if (filters.update === true) args.push("--update-snapshots");
897
+ if (useTestList && tests !== void 0) {
898
+ const content = buildTestListEntries(tests, ctx.rootDir, ctx.cwd).join("\n");
899
+ const writeTemp = this.deps.writeTempFile ?? defaultWriteTempFile;
900
+ this.testListPath = writeTemp(content);
901
+ args.push("--test-list", this.testListPath);
902
+ } else if (tests !== void 0 && tests.length > 0) {
903
+ const project = sharedProject(tests);
904
+ if (project !== void 0) args.push(`--project=${project}`);
905
+ for (const d of tests) {
906
+ args.push(d.column === void 0 ? `${d.file}:${d.line}` : `${d.file}:${d.line}:${d.column}`);
907
+ }
908
+ }
909
+ return args;
910
+ }
911
+ start(filters) {
912
+ const ctx = this.deps.getRunContext();
913
+ if (ctx === null) return { ok: false, reason: "no-config" };
914
+ if (this.child !== null) return { ok: false, reason: "already-running" };
915
+ if (filters.tests !== void 0 && filters.tests.length === 0) return { ok: false, reason: "no-tests" };
916
+ if (this.deps.launcher.available === false) return { ok: false, reason: "docker-unavailable" };
917
+ const tests = rewriteContainerTestDescriptors(filters.tests, this.deps.containerPathMapping);
918
+ const args = this.buildPlaywrightArgs(ctx, filters, tests);
919
+ const spec = this.deps.launcher.launch({ ctx, playwrightArgs: args });
920
+ let child;
921
+ try {
922
+ child = this.deps.spawn(spec.cmd, spec.args, { cwd: ctx.cwd, env: spec.env, stdio: "inherit" });
923
+ } catch (err) {
924
+ this.cleanupTempFile();
925
+ throw err;
926
+ }
927
+ this.child = child;
928
+ child.on("exit", (code) => {
929
+ this.handleChildExit(code);
930
+ });
931
+ child.on("error", () => {
932
+ this.handleChildExit(null);
933
+ });
934
+ this.deps.setReportRunning(true);
935
+ this.deps.setRunFiltered?.(filters.tests !== void 0);
936
+ this.deps.broadcast({ type: "run-status", data: { running: true, mode: this.deps.launcher.mode } });
937
+ return { ok: true };
938
+ }
939
+ stop() {
940
+ if (this.child === null) return { ok: false, reason: "not-running" };
941
+ if (this.sigkillTimer !== null) this.deps.timers.clearTimeout(this.sigkillTimer);
942
+ this.child.kill("SIGTERM");
943
+ this.sigkillTimer = this.deps.timers.setTimeout(() => {
944
+ if (this.child !== null) {
945
+ this.child.kill("SIGKILL");
946
+ this.deps.launcher.onForceKill?.();
947
+ }
948
+ }, STOP_GRACE_MS);
949
+ return { ok: true };
950
+ }
951
+ async prepareRun() {
952
+ const launcher = this.deps.launcher;
953
+ if (launcher.prepare === void 0) return { ok: true };
954
+ const ctx = this.deps.getRunContext();
955
+ if (ctx === null) return { ok: true };
956
+ try {
957
+ await launcher.prepare({
958
+ ctx,
959
+ onProgress: (phase) => {
960
+ this.deps.broadcast({ type: "run-status", data: { running: true, mode: launcher.mode, phase } });
961
+ }
962
+ });
963
+ return { ok: true };
964
+ } catch (error) {
965
+ this.deps.broadcast({ type: "run-status", data: { running: false, mode: launcher.mode } });
966
+ const message = error instanceof Error ? error.message : String(error);
967
+ console.warn(`[RunController] run preparation failed: ${message}`);
968
+ return { ok: false, reason: "docker-unavailable" };
969
+ }
970
+ }
971
+ dispose() {
972
+ if (this.child === null) return;
973
+ if (this.sigkillTimer !== null) this.deps.timers.clearTimeout(this.sigkillTimer);
974
+ this.sigkillTimer = null;
975
+ this.child.kill("SIGKILL");
976
+ this.deps.launcher.onForceKill?.();
977
+ this.cleanupTempFile();
978
+ }
979
+ handleChildExit(code) {
980
+ if (this.child === null) return;
981
+ if (this.sigkillTimer !== null) this.deps.timers.clearTimeout(this.sigkillTimer);
982
+ this.sigkillTimer = null;
983
+ this.child = null;
984
+ this.cleanupTempFile();
985
+ if (code !== null && code !== 0) console.warn(`[RunController] playwright test exited with code ${code}`);
986
+ this.deps.setReportRunning(false);
987
+ this.deps.broadcast({ type: "run-status", data: { running: false, mode: this.deps.launcher.mode } });
988
+ void this.deps.saveReport?.();
989
+ }
990
+ };
991
+ function createRealSpawn() {
992
+ return (cmd, args, opts) => {
993
+ const cp = (0, import_child_process2.spawn)(cmd, args, opts);
994
+ return {
995
+ on: (event, cb) => cp.on(event, cb),
996
+ kill: (signal) => {
997
+ const sig = KNOWN_SIGNALS[signal];
998
+ if (sig !== void 0) cp.kill(sig);
999
+ }
1000
+ };
1001
+ };
1002
+ }
1003
+ function createRealTimers() {
1004
+ const pending = [];
1005
+ return {
1006
+ setTimeout: (fn, ms) => {
1007
+ const id = setTimeout(fn, ms);
1008
+ pending.push(id);
1009
+ return id;
1010
+ },
1011
+ clearTimeout: () => {
1012
+ for (const h of pending.splice(0)) clearTimeout(h);
1013
+ }
1014
+ };
1015
+ }
1016
+
1017
+ // src/server/docker-launcher.ts
1018
+ var DOCKER_WORK_DIR = "/work";
1019
+ var DOCKER_HOST_GATEWAY = "host.docker.internal";
1020
+ var ENV_DENYLIST = /* @__PURE__ */ new Set([
1021
+ "CI",
1022
+ "PLAYWRIGHT_BROWSERS_PATH",
1023
+ "CRVY_RPRTR_SERVER_URL",
1024
+ "CRVY_RPRTR_PORTABLE_ARTIFACTS",
1025
+ "TZ",
1026
+ "LANG",
1027
+ "LC_ALL",
1028
+ "PLAYWRIGHT_HTML_OPEN"
1029
+ ]);
1030
+ var DockerUnavailableError = class extends Error {
1031
+ constructor() {
1032
+ super("Docker daemon is not available");
1033
+ this.name = "DockerUnavailableError";
1034
+ }
1035
+ };
1036
+ function defaultWarn(message) {
1037
+ console.warn(`[crvy-rprtr] ${message}`);
1038
+ }
1039
+ async function detectAgentName(detect2, cwd) {
1040
+ const detected = await (detect2 ?? detectProjectAgent)(cwd);
1041
+ return detected?.name ?? null;
1042
+ }
1043
+ async function prepareDocker(state, exec, ctx, deps, onProgress) {
1044
+ if (!await probeDockerDaemon(exec)) {
1045
+ state.available = false;
1046
+ throw new DockerUnavailableError();
1047
+ }
1048
+ state.available = true;
1049
+ const image = resolveDockerImage({ image: deps.docker?.image, version: deps.getPlaywrightVersion(ctx.cwd) });
1050
+ if (image === null) {
1051
+ throw new Error("Could not resolve the installed @playwright/test version; set docker.image explicitly.");
1052
+ }
1053
+ state.image = image;
1054
+ state.command = resolveContainerCommand({
1055
+ command: deps.docker?.command,
1056
+ hasCustomImage: deps.docker?.image !== void 0,
1057
+ detectedAgentName: deps.docker?.image === void 0 ? "npm" : await detectAgentName(deps.detectAgent, ctx.cwd),
1058
+ warn: deps.warn
1059
+ });
1060
+ if (!await isDockerImagePresent(exec, image)) {
1061
+ onProgress("pulling");
1062
+ if (!await pullDockerImage(exec, image)) {
1063
+ throw new Error(`Failed to pull docker image: ${image}`);
1064
+ }
1065
+ }
1066
+ }
1067
+ var REPORTER_BARE_SPECIFIER = "@crvy/rprtr";
1068
+ var PATH_FLAGS = /* @__PURE__ */ new Set(["--config", "--reporter", "--test-list"]);
1069
+ function rewritePlaywrightArgs(playwrightArgs, ctx, workDir, warn) {
1070
+ const args = [];
1071
+ const bindMounts = [];
1072
+ for (let i = 0; i < playwrightArgs.length; i++) {
1073
+ const flag = playwrightArgs[i];
1074
+ if (!PATH_FLAGS.has(flag)) {
1075
+ args.push(flag);
1076
+ continue;
1077
+ }
1078
+ const value = playwrightArgs[i + 1];
1079
+ if (value === void 0) break;
1080
+ i += 1;
1081
+ args.push(flag);
1082
+ const rewritten = rewriteContainerPath(value, { from: ctx.cwd, to: workDir });
1083
+ if (flag === "--reporter" && rewritten === value) {
1084
+ args.push(REPORTER_BARE_SPECIFIER);
1085
+ } else if (flag === "--test-list" && rewritten === value) {
1086
+ args.push(value);
1087
+ bindMounts.push(`${value}:${value}:ro`);
1088
+ } else {
1089
+ if (flag === "--config" && rewritten === value) {
1090
+ warn(`--config "${value}" is outside the project directory and will not resolve inside the container.`);
1091
+ }
1092
+ args.push(rewritten);
1093
+ }
1094
+ }
1095
+ return { args, bindMounts };
1096
+ }
1097
+ function buildDockerRunArgs(ctx, playwrightArgs, deps) {
1098
+ const { args: rewrittenArgs, bindMounts } = rewritePlaywrightArgs(playwrightArgs, ctx, deps.workDir, deps.warn);
1099
+ const args = [
1100
+ "run",
1101
+ "--rm",
1102
+ "--init",
1103
+ "--name",
1104
+ deps.containerName,
1105
+ "--add-host",
1106
+ `${DOCKER_HOST_GATEWAY}:host-gateway`,
1107
+ "--ipc=host"
1108
+ ];
1109
+ if (deps.docker?.platform !== void 0) {
1110
+ args.push("--platform", deps.docker.platform);
1111
+ }
1112
+ args.push("-v", `${ctx.cwd}:${deps.workDir}:rw`, "-w", deps.workDir);
1113
+ for (const mount of bindMounts) {
1114
+ args.push("-v", mount);
1115
+ }
1116
+ args.push("-e", `CRVY_RPRTR_SERVER_URL=ws://${DOCKER_HOST_GATEWAY}:${deps.port}`);
1117
+ args.push("-e", "CRVY_RPRTR_PORTABLE_ARTIFACTS=1", "-e", "TZ=UTC", "-e", "LANG=C.UTF-8", "-e", "LC_ALL=C.UTF-8");
1118
+ args.push("-e", "PLAYWRIGHT_HTML_OPEN=never");
1119
+ for (const [key, value] of Object.entries(deps.env)) {
1120
+ if (ENV_DENYLIST.has(key) || value === void 0) continue;
1121
+ args.push("-e", key);
1122
+ }
1123
+ if (deps.docker?.extraArgs !== void 0) args.push(...deps.docker.extraArgs);
1124
+ args.push(deps.image, ...deps.command, "playwright", ...rewrittenArgs);
1125
+ return args;
1126
+ }
1127
+ function stripCi(env) {
1128
+ const out = {};
1129
+ for (const [key, value] of Object.entries(env)) {
1130
+ if (key === "CI") continue;
1131
+ out[key] = value;
1132
+ }
1133
+ return out;
1134
+ }
1135
+ function createState(docker) {
1136
+ return { available: void 0, prepared: null, image: null, command: docker?.command ?? DEFAULT_CONTAINER_COMMAND };
1137
+ }
1138
+ function buildLauncher(state, deps) {
1139
+ return {
1140
+ mode: "docker",
1141
+ get available() {
1142
+ return state.available;
1143
+ },
1144
+ prepare({ ctx, onProgress }) {
1145
+ state.prepared ??= prepareDocker(
1146
+ state,
1147
+ deps.exec,
1148
+ ctx,
1149
+ {
1150
+ docker: deps.docker,
1151
+ getPlaywrightVersion: deps.getVersion,
1152
+ detectAgent: deps.detectAgent,
1153
+ warn: deps.warn
1154
+ },
1155
+ onProgress
1156
+ ).catch((error) => {
1157
+ state.prepared = null;
1158
+ throw error;
1159
+ });
1160
+ return state.prepared;
1161
+ },
1162
+ launch({ ctx, playwrightArgs }) {
1163
+ const image = state.image ?? resolveDockerImage({ image: deps.docker?.image, version: deps.getVersion(ctx.cwd) });
1164
+ if (image === null) {
1165
+ throw new Error("Could not resolve the docker image; run prepare() first or set docker.image.");
1166
+ }
1167
+ const args = buildDockerRunArgs(ctx, playwrightArgs, {
1168
+ docker: deps.docker,
1169
+ workDir: deps.workDir,
1170
+ containerName: deps.containerName,
1171
+ port: deps.port,
1172
+ env: deps.baseEnv,
1173
+ image,
1174
+ command: state.command,
1175
+ warn: deps.warn
1176
+ });
1177
+ return { cmd: "docker", args, env: stripCi(deps.baseEnv) };
1178
+ },
1179
+ onForceKill() {
1180
+ void forceRemoveContainer(deps.exec, deps.containerName);
1181
+ }
1182
+ };
1183
+ }
1184
+ function createDockerLauncher(options) {
1185
+ return buildLauncher(createState(options.docker), {
1186
+ exec: options.exec ?? createDockerExec(),
1187
+ workDir: options.workDir ?? DOCKER_WORK_DIR,
1188
+ containerName: options.containerName ?? `crvy-rprtr-run-${process.pid}`,
1189
+ baseEnv: options.env ?? process.env,
1190
+ getVersion: options.getPlaywrightVersion ?? resolvePlaywrightVersion,
1191
+ detectAgent: options.detectAgent,
1192
+ warn: options.warn ?? defaultWarn,
1193
+ docker: options.docker,
1194
+ port: options.port
1195
+ });
1196
+ }
1197
+
1198
+ // src/server/handlers.ts
1199
+ var import_fs2 = require("fs");
1200
+ var import_path6 = require("path");
1201
+
1202
+ // src/server/artifact-routes.ts
1203
+ var import_fs = require("fs");
1204
+ var import_promises3 = require("fs/promises");
1205
+ var import_path5 = require("path");
1206
+
1207
+ // src/snapshot-path-resolver.ts
1208
+ var import_crypto = require("crypto");
1209
+ var import_path3 = require("path");
1210
+ var DEFAULT_SCREENSHOT_TEMPLATE = "{snapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}{-projectName}{-snapshotSuffix}{ext}";
1211
+ var WINDOWS_FILESYSTEM_FRIENDLY_LENGTH = 60;
1212
+ function isUnsafeFilePathCharacter(character) {
1213
+ const codePoint = character.codePointAt(0);
1214
+ return codePoint !== void 0 && (codePoint <= 44 || codePoint >= 46 && codePoint <= 47 || codePoint >= 58 && codePoint <= 64 || codePoint >= 91 && codePoint <= 96 || codePoint >= 123 && codePoint <= 127);
1215
+ }
1216
+ function sanitizeForFilePath(value) {
1217
+ return Array.from(value).reduce(
1218
+ (state, character) => {
1219
+ const unsafeCharacter = isUnsafeFilePathCharacter(character);
1220
+ return unsafeCharacter ? state.previousCharacterWasUnsafe ? state : {
1221
+ value: `${state.value}-`,
1222
+ previousCharacterWasUnsafe: true
1223
+ } : {
1224
+ value: `${state.value}${character}`,
1225
+ previousCharacterWasUnsafe: false
1226
+ };
1227
+ },
1228
+ {
1229
+ value: "",
1230
+ previousCharacterWasUnsafe: false
1231
+ }
1232
+ ).value;
1233
+ }
1234
+ function trimLongString(value, length = WINDOWS_FILESYSTEM_FRIENDLY_LENGTH) {
1235
+ if (value.length <= length) {
1236
+ return value;
1237
+ }
1238
+ const hash = (0, import_crypto.createHash)("sha1").update(value).digest("hex");
1239
+ const middle = `-${hash.slice(0, 5)}-`;
1240
+ const start = Math.floor((length - middle.length) / 2);
1241
+ const end = length - middle.length - start;
1242
+ return value.slice(0, start) + middle + value.slice(-end);
1243
+ }
1244
+ function sanitizeFilePathBeforeExtension(filePath, extension = (0, import_path3.extname)(filePath)) {
1245
+ const base = filePath.slice(0, filePath.length - extension.length);
1246
+ return sanitizeForFilePath(base) + extension;
1247
+ }
1248
+ function addSuffixToFilePath(filePath, suffix) {
1249
+ const extension = (0, import_path3.extname)(filePath);
1250
+ return filePath.slice(0, filePath.length - extension.length) + suffix + extension;
1251
+ }
1252
+ function normalizedSnapshotDir(config) {
1253
+ return (0, import_path3.resolve)(config.configDir, config.snapshotDir);
1254
+ }
1255
+ function templateValue(template, token, value) {
1256
+ return template.replace(
1257
+ new RegExp(`\\{(.)?${token}\\}`, "g"),
1258
+ (_, prefix) => value === "" ? "" : `${prefix ?? ""}${value}`
1259
+ );
1260
+ }
1261
+ function applyTemplate(input, nameArgument, extension) {
1262
+ const template = input.config.toHaveScreenshotPathTemplate ?? input.config.snapshotPathTemplate ?? DEFAULT_SCREENSHOT_TEMPLATE;
1263
+ const relativeTestFilePath = (0, import_path3.relative)(input.config.testDir, input.testFile);
1264
+ const parsed = (0, import_path3.parse)(relativeTestFilePath);
1265
+ const tokens = [
1266
+ ["testDir", input.config.testDir],
1267
+ ["snapshotDir", normalizedSnapshotDir(input.config)],
1268
+ ["snapshotSuffix", input.config.snapshotSuffix],
1269
+ ["testFileDir", parsed.dir],
1270
+ ["platform", process.platform],
1271
+ ["projectName", sanitizeForFilePath(input.config.projectName)],
1272
+ ["testName", ""],
1273
+ ["testFileName", parsed.base],
1274
+ ["testFilePath", relativeTestFilePath],
1275
+ ["arg", nameArgument],
1276
+ ["ext", extension]
1277
+ ];
1278
+ const snapshotPath = tokens.reduce(
1279
+ (currentTemplate, [token, value]) => templateValue(currentTemplate, token, value),
1280
+ template
1281
+ );
1282
+ return (0, import_path3.resolve)(input.config.configDir, snapshotPath);
1283
+ }
1284
+ function removeExtension(filePath, extension = (0, import_path3.extname)(filePath)) {
1285
+ return filePath.slice(0, filePath.length - extension.length);
1286
+ }
1287
+ function snapshotNameParts(declaredName) {
1288
+ const extension = (0, import_path3.extname)(declaredName) || ".png";
1289
+ return {
1290
+ extension,
1291
+ filePath: (0, import_path3.extname)(declaredName) === "" ? `${declaredName}${extension}` : declaredName
1292
+ };
1293
+ }
1294
+ function filePathForOccurrence(filePath, occurrenceIndex) {
1295
+ return occurrenceIndex === 1 ? filePath : addSuffixToFilePath(filePath, `-${occurrenceIndex - 1}`);
1296
+ }
1297
+ function createResolvedBaselineTarget(input, declaration, nameArgument, extension) {
1298
+ return {
1299
+ visualName: declaration.visualName,
1300
+ attachmentBaseName: declaration.visualName,
1301
+ artifactBaseName: sanitizeForFilePath(declaration.visualName),
1302
+ snapshotPath: applyTemplate(input, nameArgument, extension)
1303
+ };
1304
+ }
1305
+ function resolveStringCallTarget(input, declaration) {
1306
+ const { extension, filePath } = snapshotNameParts(declaration.declaredName);
1307
+ const occurrenceFilePath = filePathForOccurrence(filePath, declaration.occurrenceIndex);
1308
+ const sanitizedNameWithExtension = sanitizeFilePathBeforeExtension(occurrenceFilePath, extension);
1309
+ const nameArgument = removeExtension(sanitizedNameWithExtension, extension);
1310
+ return createResolvedBaselineTarget(input, declaration, nameArgument, extension);
1311
+ }
1312
+ function resolveArrayCallTarget(input, declaration) {
1313
+ const { extension, filePath } = snapshotNameParts(declaration.declaredName);
1314
+ const occurrenceFilePath = filePathForOccurrence(filePath, declaration.occurrenceIndex);
1315
+ const nameArgument = (0, import_path3.join)((0, import_path3.dirname)(occurrenceFilePath), (0, import_path3.basename)(occurrenceFilePath, extension));
1316
+ return createResolvedBaselineTarget(input, declaration, nameArgument, extension);
1317
+ }
1318
+ function resolveNamedTarget(input, declaration) {
1319
+ if (!declaration.declaredName.includes("/")) {
1320
+ return resolveStringCallTarget(input, declaration);
1321
+ }
1322
+ const stringCallTarget = resolveStringCallTarget(input, declaration);
1323
+ const arrayCallTarget = resolveArrayCallTarget(input, declaration);
1324
+ if (stringCallTarget.snapshotPath === arrayCallTarget.snapshotPath) {
1325
+ return stringCallTarget;
1326
+ }
1327
+ if (input.snapshotPathExists === void 0) {
1328
+ return void 0;
1329
+ }
1330
+ const stringCallTargetExists = input.snapshotPathExists(stringCallTarget.snapshotPath);
1331
+ const arrayCallTargetExists = input.snapshotPathExists(arrayCallTarget.snapshotPath);
1332
+ if (stringCallTargetExists === arrayCallTargetExists) {
1333
+ return void 0;
1334
+ }
1335
+ return stringCallTargetExists ? stringCallTarget : arrayCallTarget;
1336
+ }
1337
+ function reporterTitlesWithoutProjectAndFile(reporterTitlePath2) {
1338
+ return reporterTitlePath2.slice(3).filter((part) => part !== "");
1339
+ }
1340
+ function anonymousNameFromTitles(titles, occurrenceIndex) {
1341
+ return sanitizeFilePathBeforeExtension(trimLongString(`${titles.join(" ")} ${occurrenceIndex}.png`), ".png");
1342
+ }
1343
+ function anonymousName(reporterTitlePath2, occurrenceIndex) {
1344
+ return anonymousNameFromTitles(reporterTitlesWithoutProjectAndFile(reporterTitlePath2), occurrenceIndex);
1345
+ }
1346
+ function resolveTarget(input, declaration) {
1347
+ switch (declaration.kind) {
1348
+ case "named":
1349
+ return typeof declaration.declaredName === "string" && declaration.declaredName !== "" ? resolveNamedTarget(input, declaration) : void 0;
1350
+ case "unnamed": {
1351
+ const anonymousFileName = anonymousName(input.reporterTitlePath, declaration.occurrenceIndex);
1352
+ const extension = ".png";
1353
+ const nameArgument = (0, import_path3.join)((0, import_path3.dirname)(anonymousFileName), (0, import_path3.basename)(anonymousFileName, extension));
1354
+ return createResolvedBaselineTarget(input, declaration, nameArgument, extension);
1355
+ }
1356
+ }
1357
+ }
1358
+ function resolveBaselineTargets(input) {
1359
+ return input.declarations.flatMap((declaration) => {
1360
+ const resolvedTarget = resolveTarget(input, declaration);
1361
+ return resolvedTarget === void 0 ? [] : [resolvedTarget];
1362
+ });
1363
+ }
1364
+
1365
+ // src/server/utils.ts
1366
+ var import_path4 = require("path");
1367
+ var LIVE_UPDATES_WEBSOCKET_PATH = "/";
1368
+ function broadcastToBrowsers(wsClients, msg) {
1369
+ const payload = JSON.stringify(msg);
1370
+ wsClients.forEach((ws) => {
1371
+ ws.send(payload);
1372
+ });
1373
+ }
1374
+ function isWebSocketUpgradeRequest(req) {
1375
+ return new URL(req.url).pathname === LIVE_UPDATES_WEBSOCKET_PATH && req.headers.get("upgrade")?.toLowerCase() === "websocket";
1376
+ }
1377
+ function isPathWithinRoots(target, roots) {
1378
+ const resolvedTarget = (0, import_path4.resolve)(target);
1379
+ return roots.some((root) => {
1380
+ const rel = (0, import_path4.relative)((0, import_path4.resolve)(root), resolvedTarget);
1381
+ return rel === "" || !rel.startsWith(`..${import_path4.sep}`) && rel !== ".." && !(0, import_path4.isAbsolute)(rel);
1382
+ });
1383
+ }
1384
+
1385
+ // src/server/artifact-routes.ts
1386
+ async function realpathOrNull(path) {
1387
+ try {
1388
+ return await (0, import_promises3.realpath)(path);
1389
+ } catch {
1390
+ return null;
1391
+ }
1392
+ }
1393
+ async function handleFile(ctx, req) {
852
1394
  const notFound = () => new Response("Not Found", { status: 404 });
853
1395
  let rawDecoded;
854
1396
  let decodedPath;
@@ -886,11 +1428,14 @@ function reporterTitlePath(test) {
886
1428
  return ["", projectName, testFile ?? "", ...test.titlePath, test.title];
887
1429
  }
888
1430
  function resolveBaselineSnapshotPath(routing, test, retry, imageName) {
889
- const testFile = test.location?.file;
1431
+ const mapping = routing?.containerPathMapping;
1432
+ const rawTestFile = test.location?.file;
1433
+ const testFile = rawTestFile === void 0 || mapping === void 0 ? rawTestFile : rewriteContainerPath(rawTestFile, mapping);
890
1434
  const declaration = test.results?.[retry]?.visualDeclarations?.find((candidate) => candidate.visualName === imageName);
891
1435
  if (routing === void 0 || testFile === void 0 || declaration === void 0) {
892
1436
  return null;
893
1437
  }
1438
+ const isContainerPath = mapping !== void 0 && testFile !== rawTestFile;
894
1439
  const targets = resolveBaselineTargets({
895
1440
  testFile,
896
1441
  reporterTitlePath: reporterTitlePath(test),
@@ -900,7 +1445,7 @@ function resolveBaselineSnapshotPath(routing, test, retry, imageName) {
900
1445
  testDir: routing.playwrightTestDir ?? (0, import_path5.dirname)(testFile),
901
1446
  snapshotDir: routing.playwrightSnapshotDir ?? (0, import_path5.dirname)(testFile),
902
1447
  projectName: test.projectName ?? test.browser,
903
- snapshotSuffix: process.platform,
1448
+ snapshotSuffix: isContainerPath ? "linux" : process.platform,
904
1449
  snapshotPathTemplate: routing.playwrightSnapshotPathTemplate,
905
1450
  toHaveScreenshotPathTemplate: routing.playwrightToHaveScreenshotPathTemplate
906
1451
  },
@@ -1023,7 +1568,19 @@ function handleSync(ctx) {
1023
1568
  };
1024
1569
  broadcastToBrowsers(ctx.wsClients, message);
1025
1570
  }
1026
- function handleRegister(ctx, data) {
1571
+ function applyContainerPathMapping(rawData, mapping) {
1572
+ return {
1573
+ ...rawData,
1574
+ playwrightSnapshotDir: rawData.playwrightSnapshotDir === void 0 ? void 0 : rewriteContainerPath(rawData.playwrightSnapshotDir, mapping),
1575
+ playwrightTestDir: rawData.playwrightTestDir === void 0 ? void 0 : rewriteContainerPath(rawData.playwrightTestDir, mapping),
1576
+ playwrightRootDir: rawData.playwrightRootDir === void 0 ? void 0 : rewriteContainerPath(rawData.playwrightRootDir, mapping),
1577
+ configFile: rawData.configFile === void 0 ? void 0 : rewriteContainerPath(rawData.configFile, mapping),
1578
+ cwd: rawData.cwd === void 0 ? void 0 : rewriteContainerPath(rawData.cwd, mapping)
1579
+ };
1580
+ }
1581
+ function handleRegister(ctx, rawData) {
1582
+ const mapping = ctx.routesContext.containerPathMapping;
1583
+ const data = mapping === void 0 ? rawData : applyContainerPathMapping(rawData, mapping);
1027
1584
  const roots = [];
1028
1585
  if (data.playwrightSnapshotDir !== void 0 && data.playwrightSnapshotDir !== "") {
1029
1586
  roots.push(data.playwrightSnapshotDir);
@@ -1053,16 +1610,65 @@ function handleRegister(ctx, data) {
1053
1610
  }
1054
1611
  }
1055
1612
  if (data.configFile !== void 0 && data.cwd !== void 0) {
1056
- ctx.routesContext.runContext = { configFile: data.configFile, cwd: data.cwd };
1613
+ ctx.routesContext.runContext = buildRunContext(data.configFile, data);
1057
1614
  }
1058
1615
  console.log("[Server] Reporter registered with config:", {
1059
1616
  playwrightSnapshotDir: data.playwrightSnapshotDir,
1060
- playwrightTestDir: data.playwrightTestDir
1617
+ playwrightTestDir: data.playwrightTestDir,
1618
+ configFile: data.configFile,
1619
+ cwd: data.cwd
1620
+ });
1621
+ }
1622
+ function buildRunContext(configFile, data) {
1623
+ const configDir = (0, import_path6.dirname)(configFile);
1624
+ return {
1625
+ configFile,
1626
+ cwd: configDir,
1627
+ rootDir: data.playwrightRootDir ?? (data.playwrightTestDir === void 0 ? configDir : (0, import_path6.resolve)(configDir, data.playwrightTestDir))
1628
+ };
1629
+ }
1630
+
1631
+ // src/ci.ts
1632
+ function isCI(env = process.env) {
1633
+ const ci = env.CI;
1634
+ return ci !== void 0 && ci !== "" && ci !== "false" && ci !== "0";
1635
+ }
1636
+
1637
+ // src/server/run-mode.ts
1638
+ async function resolveRunMode(options) {
1639
+ if (options.runMode === "local") return "local";
1640
+ if (options.runMode === "docker") return "docker";
1641
+ if (options.isCI) return "local";
1642
+ if (await options.probeDocker()) return "docker";
1643
+ options.warn?.(
1644
+ "Docker daemon unavailable \u2014 running tests locally; screenshots may differ from CI. Use --run-mode local to silence this warning."
1645
+ );
1646
+ return "local";
1647
+ }
1648
+
1649
+ // src/server/launcher-resolver.ts
1650
+ async function resolveRunBackend(options) {
1651
+ const dockerExec = createDockerExec();
1652
+ const resolvedRunMode = await resolveRunMode({
1653
+ runMode: options.runMode ?? "auto",
1654
+ isCI: isCI(),
1655
+ probeDocker: () => probeDockerDaemon(dockerExec),
1656
+ warn: (message) => {
1657
+ console.warn(`[crvy-rprtr] ${message}`);
1658
+ }
1061
1659
  });
1660
+ const launcher = resolvedRunMode === "docker" ? createDockerLauncher({ port: options.port, docker: options.docker, exec: dockerExec }) : createLocalLauncher({ port: options.port });
1661
+ return {
1662
+ launcher,
1663
+ routesContextOptions: {
1664
+ runInfo: { mode: resolvedRunMode },
1665
+ containerPathMapping: resolvedRunMode === "docker" ? { from: DOCKER_WORK_DIR, to: process.cwd() } : void 0
1666
+ }
1667
+ };
1062
1668
  }
1063
1669
 
1064
1670
  // src/server/playwright-config.ts
1065
- var import_path6 = require("path");
1671
+ var import_path7 = require("path");
1066
1672
  var CONFIG_FILES = [
1067
1673
  "playwright.config.ts",
1068
1674
  "playwright.config.mts",
@@ -1074,12 +1680,15 @@ var CONFIG_FILES = [
1074
1680
  async function resolvePlaywrightConfig(cwd) {
1075
1681
  const matches = await Promise.all(
1076
1682
  CONFIG_FILES.map(async (file) => {
1077
- const candidate = (0, import_path6.join)(cwd, file);
1683
+ const candidate = (0, import_path7.join)(cwd, file);
1078
1684
  return await fileExists(candidate) ? candidate : null;
1079
1685
  })
1080
1686
  );
1081
1687
  return matches.find((path) => path !== null) ?? null;
1082
1688
  }
1689
+ function resolveSeedConfigFile(option, cwd) {
1690
+ return option === void 0 ? resolvePlaywrightConfig(cwd) : Promise.resolve((0, import_path7.resolve)(cwd, option));
1691
+ }
1083
1692
 
1084
1693
  // src/server/report-persistence.ts
1085
1694
  function createDebouncedSaver(save, delayMs, setTimeoutFn = (fn, ms) => setTimeout(fn, ms), clearTimeoutFn = (handle) => {
@@ -1151,14 +1760,17 @@ function createRoutesContext(reportData, staticDir, saveReport, options) {
1151
1760
  playwrightTestDir: options.playwrightTestDir,
1152
1761
  playwrightSnapshotDir: options.playwrightSnapshotDir,
1153
1762
  playwrightSnapshotPathTemplate: options.playwrightSnapshotPathTemplate,
1154
- playwrightToHaveScreenshotPathTemplate: options.playwrightToHaveScreenshotPathTemplate
1763
+ playwrightToHaveScreenshotPathTemplate: options.playwrightToHaveScreenshotPathTemplate,
1764
+ containerPathMapping: options.containerPathMapping
1155
1765
  },
1156
- runContext: void 0
1766
+ runContext: void 0,
1767
+ runInfo: options.runInfo,
1768
+ containerPathMapping: options.containerPathMapping
1157
1769
  };
1158
1770
  }
1159
1771
 
1160
1772
  // src/server/routes.ts
1161
- var import_path7 = require("path");
1773
+ var import_path8 = require("path");
1162
1774
 
1163
1775
  // src/server/run-routes.ts
1164
1776
  function handleRunRoutes(pathname, method, runController, req) {
@@ -1180,6 +1792,10 @@ async function handleApiRun(runController, req) {
1180
1792
  if (parsed === null) {
1181
1793
  return Response.json({ ok: false, error: "Invalid request body" }, { status: 400 });
1182
1794
  }
1795
+ const preparation = await runController.prepareRun();
1796
+ if (!preparation.ok) {
1797
+ return Response.json({ ok: false, reason: preparation.reason }, { status: 409 });
1798
+ }
1183
1799
  const result = runController.start(parsed);
1184
1800
  if (result.ok) return Response.json(result);
1185
1801
  const status = result.reason === "no-tests" ? 400 : 409;
@@ -1193,7 +1809,7 @@ function handleApiStop(runController) {
1193
1809
 
1194
1810
  // src/server/routes.ts
1195
1811
  async function handleRoot(ctx) {
1196
- const html = await respondWithFile((0, import_path7.join)(ctx.staticDir, "index.html"), "text/html");
1812
+ const html = await respondWithFile((0, import_path8.join)(ctx.staticDir, "index.html"), "text/html");
1197
1813
  return html ?? new Response("Not Found", { status: 404 });
1198
1814
  }
1199
1815
  async function handleAppCss() {
@@ -1210,13 +1826,14 @@ async function handleSrcFiles(req) {
1210
1826
  function handleApiReport(ctx) {
1211
1827
  return Response.json({
1212
1828
  ...ctx.reportData,
1213
- runEnabled: ctx.runContext !== void 0
1829
+ runEnabled: ctx.runContext !== void 0,
1830
+ runMode: ctx.runInfo?.mode
1214
1831
  });
1215
1832
  }
1216
1833
  var APPROVAL_TARGET_ERROR = "Could not resolve approval target";
1217
1834
  function actualPathFromUrl(ctx, actualUrl) {
1218
1835
  if (actualUrl.startsWith("/screenshots/")) {
1219
- return (0, import_path7.join)(ctx.reportData.screenshotDir, actualUrl.slice("/screenshots/".length));
1836
+ return (0, import_path8.join)(ctx.reportData.screenshotDir, actualUrl.slice("/screenshots/".length));
1220
1837
  }
1221
1838
  if (actualUrl.startsWith("/file/")) {
1222
1839
  return decodeURIComponent(actualUrl.slice("/file/".length));
@@ -1338,7 +1955,7 @@ async function handleScreenshots(ctx, req) {
1338
1955
  }
1339
1956
  async function handleDist(ctx, req) {
1340
1957
  const path = new URL(req.url).pathname.slice("/dist/".length);
1341
- const filePath = (0, import_path7.join)(ctx.staticDir, path);
1958
+ const filePath = (0, import_path8.join)(ctx.staticDir, path);
1342
1959
  const contentType = filePath.endsWith(".css") ? "text/css" : filePath.endsWith(".js") ? "application/javascript" : filePath.endsWith(".svelte") ? "text/plain" : "application/octet-stream";
1343
1960
  const file = await respondWithFile(filePath, contentType);
1344
1961
  return file ?? new Response("Not Found", { status: 404 });
@@ -1383,217 +2000,56 @@ function handleHttpRequest(ctx, req, runController) {
1383
2000
  return Promise.resolve(new Response("Not Found", { status: 404 }));
1384
2001
  }
1385
2002
 
1386
- // src/server/run-controller.ts
1387
- var import_child_process = require("child_process");
1388
- var import_node_fs = require("node:fs");
1389
- var import_node_module = require("node:module");
1390
- var import_node_os = require("node:os");
1391
- var import_node_path = require("node:path");
1392
- var import_commands = require("package-manager-detector/commands");
1393
- var import_detect = require("package-manager-detector/detect");
1394
- var import_meta = { url: require("url").pathToFileURL(__filename).href };
1395
- var STOP_GRACE_MS = 5e3;
1396
- var KNOWN_SIGNALS = {
1397
- SIGTERM: "SIGTERM",
1398
- SIGKILL: "SIGKILL"
1399
- };
1400
- function resolvePlaywrightLaunch(cwd, playwrightArgs) {
1401
- const agent = (0, import_detect.getUserAgent)();
1402
- const resolved = agent === null ? null : (0, import_commands.resolveCommand)(agent, "execute-local", ["playwright", ...playwrightArgs]);
1403
- if (resolved !== null) return { cmd: resolved.command, args: resolved.args };
1404
- return { cmd: "npx", args: ["playwright", ...playwrightArgs] };
1405
- }
1406
- function descriptorLocation(d) {
1407
- return d.column === void 0 ? `${d.file}:${d.line}` : `${d.file}:${d.line}:${d.column}`;
1408
- }
1409
- function sharedProject(tests) {
1410
- const names = new Set(tests.map((t) => t.projectName ?? ""));
1411
- if (names.size === 1) {
1412
- const name = [...names][0];
1413
- return name === "" ? void 0 : name;
1414
- }
1415
- return void 0;
1416
- }
1417
- function gteMinor(version, major, minor) {
1418
- const match = /^(\d+)\.(\d+)/.exec(version.trim());
1419
- if (match === null) return false;
1420
- const maj = parseInt(match[1], 10);
1421
- const min = parseInt(match[2], 10);
1422
- if (maj !== major) return maj > major;
1423
- return min >= minor;
1424
- }
1425
- function resolvePlaywrightVersion(cwd) {
1426
- try {
1427
- const req = (0, import_node_module.createRequire)((0, import_node_path.join)(cwd, "package.json"));
1428
- const pkgPath = req.resolve("@playwright/test/package.json");
1429
- const pkg = JSON.parse((0, import_node_fs.readFileSync)(pkgPath, "utf8"));
1430
- return typeof pkg === "object" && pkg !== null && "version" in pkg && typeof pkg.version === "string" ? pkg.version : null;
1431
- } catch {
1432
- return null;
1433
- }
1434
- }
1435
- function buildTestListEntries(tests) {
1436
- return tests.map((d) => {
1437
- const loc = descriptorLocation(d);
1438
- const title = d.titlePath.join(" \u203A ");
1439
- const prefix = d.projectName !== void 0 && d.projectName !== "" ? `[${d.projectName}] \u203A ` : "";
1440
- return `${prefix}${loc} \u203A ${title}`;
2003
+ // src/server/server-factories.ts
2004
+ function createServerRunController(routesContext, wsClients, reportData, port, setRunFiltered, saveReport, launcher) {
2005
+ return new RunController({
2006
+ getRunContext: () => routesContext.runContext ?? null,
2007
+ port,
2008
+ broadcast: (message) => {
2009
+ broadcastToBrowsers(wsClients, message);
2010
+ },
2011
+ setReportRunning: (running) => {
2012
+ reportData.isRunning = running;
2013
+ },
2014
+ setRunFiltered,
2015
+ containerPathMapping: routesContext.containerPathMapping,
2016
+ saveReport,
2017
+ spawn: createRealSpawn(),
2018
+ timers: createRealTimers(),
2019
+ launcher
1441
2020
  });
1442
2021
  }
1443
- function defaultWriteTempFile(content) {
1444
- const path = (0, import_node_path.join)((0, import_node_os.tmpdir)(), `crvy-rprtr-test-list-${process.pid}-${Date.now()}.txt`);
1445
- (0, import_node_fs.writeFileSync)(path, content, "utf8");
1446
- return path;
1447
- }
1448
- function defaultDeleteTempFile(path) {
1449
- try {
1450
- (0, import_node_fs.unlinkSync)(path);
1451
- } catch {
1452
- }
1453
- }
1454
- function resolveReporterDefault(cwd) {
1455
- try {
1456
- return (0, import_node_module.createRequire)((0, import_node_path.join)(cwd, "package.json")).resolve("@crvy/rprtr");
1457
- } catch {
1458
- }
1459
- try {
1460
- return (0, import_node_module.createRequire)(import_meta.url).resolve("@crvy/rprtr");
1461
- } catch {
1462
- return null;
1463
- }
1464
- }
1465
- function buildSpawnEnv(port) {
1466
- const env = {};
1467
- for (const [key, value] of Object.entries(process.env)) {
1468
- if (key === "CI") continue;
1469
- env[key] = value;
1470
- }
1471
- env.CRVY_RPRTR_SERVER_URL = `ws://localhost:${port}`;
1472
- env.PLAYWRIGHT_HTML_OPEN = "never";
1473
- return env;
1474
- }
1475
- var RunController = class {
1476
- constructor(deps) {
1477
- this.deps = deps;
1478
- }
1479
- child = null;
1480
- sigkillTimer = null;
1481
- testListPath = null;
1482
- get isRunning() {
1483
- return this.child !== null;
1484
- }
1485
- supportsTestList(cwd) {
1486
- const getVersion = this.deps.getPlaywrightVersion ?? resolvePlaywrightVersion;
1487
- const version = getVersion(cwd);
1488
- return version !== null && gteMinor(version, 1, 56);
1489
- }
1490
- cleanupTempFile() {
1491
- if (this.testListPath !== null) {
1492
- const del = this.deps.deleteTempFile ?? defaultDeleteTempFile;
1493
- del(this.testListPath);
1494
- this.testListPath = null;
1495
- }
1496
- }
1497
- start(filters) {
1498
- const ctx = this.deps.getRunContext();
1499
- if (ctx === null) return { ok: false, reason: "no-config" };
1500
- if (this.child !== null) return { ok: false, reason: "already-running" };
1501
- if (filters.tests !== void 0 && filters.tests.length === 0) {
1502
- return { ok: false, reason: "no-tests" };
1503
- }
1504
- const resolveReporter = this.deps.resolveReporter ?? resolveReporterDefault;
1505
- const reporterModule = resolveReporter(ctx.cwd);
1506
- const tests = filters.tests;
1507
- const useTestList = tests !== void 0 && tests.length > 1 && this.supportsTestList(ctx.cwd);
1508
- const args = ["test", "--config", ctx.configFile];
1509
- if (reporterModule !== null) args.push("--reporter", reporterModule);
1510
- if (useTestList && tests !== void 0) {
1511
- const content = buildTestListEntries(tests).join("\n");
1512
- const writeTemp = this.deps.writeTempFile ?? defaultWriteTempFile;
1513
- this.testListPath = writeTemp(content);
1514
- args.push("--test-list", this.testListPath);
1515
- } else if (tests !== void 0 && tests.length > 0) {
1516
- const project = sharedProject(tests);
1517
- if (project !== void 0) args.push("--project", project);
1518
- for (const d of tests) args.push(descriptorLocation(d));
1519
- }
1520
- const resolveLaunch = this.deps.resolveLaunch ?? resolvePlaywrightLaunch;
1521
- const { cmd, args: launchArgs } = resolveLaunch(ctx.cwd, args);
1522
- let child;
1523
- try {
1524
- child = this.deps.spawn(cmd, launchArgs, { cwd: ctx.cwd, env: buildSpawnEnv(this.deps.port), stdio: "inherit" });
1525
- } catch (err) {
1526
- this.cleanupTempFile();
1527
- throw err;
1528
- }
1529
- this.child = child;
1530
- child.on("exit", (code) => {
1531
- this.handleChildExit(code);
1532
- });
1533
- child.on("error", () => {
1534
- this.handleChildExit(null);
1535
- });
1536
- this.deps.setReportRunning(true);
1537
- this.deps.setRunFiltered?.(filters.tests !== void 0);
1538
- this.deps.broadcast({ type: "run-status", data: { running: true } });
1539
- return { ok: true };
1540
- }
1541
- stop() {
1542
- if (this.child === null) return { ok: false, reason: "not-running" };
1543
- if (this.sigkillTimer !== null) this.deps.timers.clearTimeout(this.sigkillTimer);
1544
- this.child.kill("SIGTERM");
1545
- this.sigkillTimer = this.deps.timers.setTimeout(() => {
1546
- if (this.child !== null) this.child.kill("SIGKILL");
1547
- }, STOP_GRACE_MS);
1548
- return { ok: true };
1549
- }
1550
- dispose() {
1551
- if (this.child === null) return;
1552
- if (this.sigkillTimer !== null) this.deps.timers.clearTimeout(this.sigkillTimer);
1553
- this.sigkillTimer = null;
1554
- this.child.kill("SIGKILL");
1555
- this.cleanupTempFile();
1556
- }
1557
- handleChildExit(code) {
1558
- if (this.child === null) return;
1559
- if (this.sigkillTimer !== null) {
1560
- this.deps.timers.clearTimeout(this.sigkillTimer);
1561
- this.sigkillTimer = null;
1562
- }
1563
- this.child = null;
1564
- this.cleanupTempFile();
1565
- if (code !== null && code !== 0) {
1566
- console.warn(`[RunController] playwright test exited with code ${code}`);
1567
- }
1568
- this.deps.setReportRunning(false);
1569
- this.deps.broadcast({ type: "run-status", data: { running: false } });
1570
- void this.deps.saveReport?.();
1571
- }
1572
- };
1573
- function createRealSpawn() {
1574
- return (cmd, args, opts) => {
1575
- const cp = (0, import_child_process.spawn)(cmd, args, opts);
1576
- return {
1577
- on: (event, cb) => cp.on(event, cb),
1578
- kill: (signal) => {
1579
- const sig = KNOWN_SIGNALS[signal];
1580
- if (sig !== void 0) cp.kill(sig);
1581
- }
1582
- };
2022
+ function createCloseHandler(persistence, runController) {
2023
+ return async () => {
2024
+ await persistence.dispose();
2025
+ runController.dispose();
1583
2026
  };
1584
2027
  }
1585
- function createRealTimers() {
1586
- const pending = [];
1587
- return {
1588
- setTimeout: (fn, ms) => {
1589
- const id = setTimeout(fn, ms);
1590
- pending.push(id);
1591
- return id;
2028
+ function createRunControllerAndHandlers(routesContext, wsClients, reportData, currentRunIds, port, persistence, launcher) {
2029
+ let isFilteredRun = false;
2030
+ const runController = createServerRunController(
2031
+ routesContext,
2032
+ wsClients,
2033
+ reportData,
2034
+ port,
2035
+ (filtered) => {
2036
+ isFilteredRun = filtered;
1592
2037
  },
1593
- clearTimeout: () => {
1594
- for (const h of pending.splice(0)) clearTimeout(h);
1595
- }
1596
- };
2038
+ persistence.saveReport,
2039
+ launcher
2040
+ );
2041
+ const getHandlerContext = () => ({
2042
+ reportData,
2043
+ wsClients,
2044
+ currentRunIds,
2045
+ isFilteredRun,
2046
+ saveReport: persistence.saveReport,
2047
+ scheduleReportSave: persistence.scheduleReportSave,
2048
+ approvalRouting: routesContext.approvalRouting,
2049
+ routesContext,
2050
+ runController
2051
+ });
2052
+ return { runController, getHandlerContext };
1597
2053
  }
1598
2054
 
1599
2055
  // src/server/app.ts
@@ -1686,19 +2142,19 @@ function createWebSocketMessageHandler(getHandlerContext) {
1686
2142
  };
1687
2143
  }
1688
2144
  async function resolveStaticDir(staticDir) {
1689
- const currentDir = (0, import_path8.dirname)((0, import_url.fileURLToPath)(import_meta2.url));
2145
+ const currentDir = (0, import_path9.dirname)((0, import_url.fileURLToPath)(import_meta2.url));
1690
2146
  const candidates = staticDir === void 0 ? [
1691
2147
  currentDir,
1692
- (0, import_path8.join)(currentDir, "dist"),
1693
- (0, import_path8.join)(currentDir, "..", "dist"),
1694
- (0, import_path8.join)(currentDir, "..", "..", "dist"),
1695
- (0, import_path8.join)(currentDir, ".."),
1696
- (0, import_path8.join)(currentDir, "..", "..")
1697
- ] : [staticDir, (0, import_path8.join)(staticDir, "dist")];
2148
+ (0, import_path9.join)(currentDir, "dist"),
2149
+ (0, import_path9.join)(currentDir, "..", "dist"),
2150
+ (0, import_path9.join)(currentDir, "..", "..", "dist"),
2151
+ (0, import_path9.join)(currentDir, ".."),
2152
+ (0, import_path9.join)(currentDir, "..", "..")
2153
+ ] : [staticDir, (0, import_path9.join)(staticDir, "dist")];
1698
2154
  const resolvedCandidates = await Promise.all(
1699
2155
  candidates.map(async (candidate) => ({
1700
2156
  candidate,
1701
- exists: await fileExists((0, import_path8.join)(candidate, "index.html"))
2157
+ exists: await fileExists((0, import_path9.join)(candidate, "index.html"))
1702
2158
  }))
1703
2159
  );
1704
2160
  const resolved = resolvedCandidates.find(({ exists }) => exists);
@@ -1709,34 +2165,31 @@ async function resolveStaticDir(staticDir) {
1709
2165
  }
1710
2166
  async function resolveReportPath(reportPath) {
1711
2167
  if (await isDirectory(reportPath)) {
1712
- return { reportFile: (0, import_path8.join)(reportPath, "report.json"), offlineReportDir: reportPath };
2168
+ return { reportFile: (0, import_path9.join)(reportPath, "report.json"), offlineReportDir: reportPath };
1713
2169
  }
1714
- return { reportFile: reportPath, offlineReportDir: (0, import_path8.dirname)(reportPath) };
2170
+ return { reportFile: reportPath, offlineReportDir: (0, import_path9.dirname)(reportPath) };
1715
2171
  }
1716
2172
  async function seedRunContext(routesContext, options) {
1717
2173
  if (routesContext.runContext !== void 0) {
1718
2174
  return;
1719
2175
  }
1720
- const configFile = options.playwrightConfig ?? await resolvePlaywrightConfig(process.cwd());
2176
+ const configFile = await resolveSeedConfigFile(options.playwrightConfig, process.cwd());
1721
2177
  if (configFile !== null) {
1722
2178
  routesContext.runContext = { configFile, cwd: process.cwd() };
1723
2179
  }
1724
2180
  }
1725
- function createServerRunController(routesContext, wsClients, reportData, port, setRunFiltered, saveReport) {
1726
- return new RunController({
1727
- getRunContext: () => routesContext.runContext ?? null,
1728
- port,
1729
- broadcast: (message) => {
1730
- broadcastToBrowsers(wsClients, message);
1731
- },
1732
- setReportRunning: (running) => {
1733
- reportData.isRunning = running;
1734
- },
1735
- setRunFiltered,
1736
- saveReport,
1737
- spawn: createRealSpawn(),
1738
- timers: createRealTimers()
2181
+ async function setupRoutesContext(options, reportData, staticDir, saveReport, port) {
2182
+ const { launcher, routesContextOptions } = await resolveRunBackend({
2183
+ runMode: options.runMode,
2184
+ docker: options.docker,
2185
+ port
2186
+ });
2187
+ const routesContext = createRoutesContext(reportData, staticDir, saveReport, {
2188
+ ...options,
2189
+ ...routesContextOptions
1739
2190
  });
2191
+ await seedRunContext(routesContext, options);
2192
+ return { routesContext, launcher };
1740
2193
  }
1741
2194
  async function createServerApp(options = {}) {
1742
2195
  const port = options.port ?? 3e3;
@@ -1747,30 +2200,22 @@ async function createServerApp(options = {}) {
1747
2200
  const wsClients = /* @__PURE__ */ new Set();
1748
2201
  const currentRunIds = /* @__PURE__ */ new Set();
1749
2202
  const persistence = createReportPersistence(reportFile, reportData);
1750
- const routesContext = createRoutesContext(reportData, staticDir, persistence.saveReport, options);
1751
- await seedRunContext(routesContext, options);
1752
- let isFilteredRun = false;
1753
- const runController = createServerRunController(
2203
+ const { routesContext, launcher } = await setupRoutesContext(
2204
+ options,
2205
+ reportData,
2206
+ staticDir,
2207
+ persistence.saveReport,
2208
+ port
2209
+ );
2210
+ const { runController, getHandlerContext } = createRunControllerAndHandlers(
1754
2211
  routesContext,
1755
2212
  wsClients,
1756
2213
  reportData,
2214
+ currentRunIds,
1757
2215
  port,
1758
- (filtered) => {
1759
- isFilteredRun = filtered;
1760
- },
1761
- persistence.saveReport
2216
+ persistence,
2217
+ launcher
1762
2218
  );
1763
- const getHandlerContext = () => ({
1764
- reportData,
1765
- wsClients,
1766
- currentRunIds,
1767
- isFilteredRun,
1768
- saveReport: persistence.saveReport,
1769
- scheduleReportSave: persistence.scheduleReportSave,
1770
- approvalRouting: routesContext.approvalRouting,
1771
- routesContext,
1772
- runController
1773
- });
1774
2219
  const handleRequest = (req) => handleHttpRequest(routesContext, req, runController);
1775
2220
  const handleWebSocketMessage = createWebSocketMessageHandler(getHandlerContext);
1776
2221
  await loadReport(reportFile, reportData);
@@ -1783,12 +2228,6 @@ async function createServerApp(options = {}) {
1783
2228
  handleWebSocketMessage
1784
2229
  };
1785
2230
  }
1786
- function createCloseHandler(persistence, runController) {
1787
- return async () => {
1788
- await persistence.dispose();
1789
- runController.dispose();
1790
- };
1791
- }
1792
2231
 
1793
2232
  // src/server/bun-adapter.ts
1794
2233
  function logWebSocketError(prefix, error) {
@@ -1965,7 +2404,7 @@ function attachWebSocketServer(server, app) {
1965
2404
  });
1966
2405
  }
1967
2406
  async function listen(server, port) {
1968
- await new Promise((resolve4, reject) => {
2407
+ await new Promise((resolve6, reject) => {
1969
2408
  const onError = (error) => {
1970
2409
  server.off("error", onError);
1971
2410
  reject(error);
@@ -1973,7 +2412,7 @@ async function listen(server, port) {
1973
2412
  server.on("error", onError);
1974
2413
  server.listen(port, () => {
1975
2414
  server.off("error", onError);
1976
- resolve4();
2415
+ resolve6();
1977
2416
  });
1978
2417
  });
1979
2418
  }