@crvy/rprtr 0.2.4 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +55 -0
- package/README.md +41 -0
- package/dist/{chunk-473CWZ4V.js → chunk-4CVHIHAJ.js} +746 -281
- package/dist/{chunk-HAFWYUNO.js → chunk-4YJL655E.js} +16 -4
- package/dist/cli.d.ts +5 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +24 -4
- package/dist/index.css +35 -0
- package/dist/index.js +152 -105
- package/dist/reporter-artifact-ops.d.ts +0 -2
- package/dist/reporter-artifact-ops.d.ts.map +1 -1
- package/dist/reporter.cjs +32 -38
- package/dist/reporter.d.ts +2 -1
- package/dist/reporter.d.ts.map +1 -1
- package/dist/reporter.js +27 -43
- package/dist/schemas/http.d.ts +2 -0
- package/dist/schemas/http.d.ts.map +1 -1
- package/dist/schemas.d.ts +14 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/server/app.d.ts +6 -0
- package/dist/server/app.d.ts.map +1 -1
- package/dist/server/artifact-routes.d.ts.map +1 -1
- package/dist/server/docker-launcher.d.ts +27 -0
- package/dist/server/docker-launcher.d.ts.map +1 -0
- package/dist/server/docker-support.d.ts +88 -0
- package/dist/server/docker-support.d.ts.map +1 -0
- package/dist/server/handlers.d.ts +1 -1
- package/dist/server/handlers.d.ts.map +1 -1
- package/dist/server/launcher-resolver.d.ts +23 -0
- package/dist/server/launcher-resolver.d.ts.map +1 -0
- package/dist/server/playwright-config.d.ts +6 -0
- package/dist/server/playwright-config.d.ts.map +1 -1
- package/dist/server/routes-context.d.ts +6 -1
- package/dist/server/routes-context.d.ts.map +1 -1
- package/dist/server/routes.d.ts +7 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/run-controller.d.ts +19 -19
- package/dist/server/run-controller.d.ts.map +1 -1
- package/dist/server/run-launcher.d.ts +45 -0
- package/dist/server/run-launcher.d.ts.map +1 -0
- package/dist/server/run-mode.d.ts +15 -0
- package/dist/server/run-mode.d.ts.map +1 -0
- package/dist/server/server-factories.d.ts +21 -0
- package/dist/server/server-factories.d.ts.map +1 -0
- package/dist/server.cjs +921 -446
- package/dist/server.js +2 -2
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
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
|
|
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,198 +659,771 @@ function mergeOfflineReportsIntoTests(existingTests, offlineReports, options = {
|
|
|
654
659
|
};
|
|
655
660
|
}
|
|
656
661
|
|
|
657
|
-
// src/server/
|
|
658
|
-
var
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
var
|
|
662
|
-
var
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
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
|
|
693
|
-
|
|
694
|
-
|
|
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
|
-
}
|
|
713
|
-
function templateValue(template, token, value) {
|
|
714
|
-
return template.replace(
|
|
715
|
-
new RegExp(`\\{(.)?${token}\\}`, "g"),
|
|
716
|
-
(_, prefix) => value === "" ? "" : `${prefix ?? ""}${value}`
|
|
717
|
-
);
|
|
718
|
-
}
|
|
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);
|
|
741
|
-
}
|
|
742
|
-
function removeExtension(filePath, extension = (0, import_path3.extname)(filePath)) {
|
|
743
|
-
return filePath.slice(0, filePath.length - extension.length);
|
|
744
|
-
}
|
|
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
|
-
};
|
|
751
692
|
}
|
|
752
|
-
function
|
|
753
|
-
|
|
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
|
+
}
|
|
754
700
|
}
|
|
755
|
-
function
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
}
|
|
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
|
+
}
|
|
762
708
|
}
|
|
763
|
-
function
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
return createResolvedBaselineTarget(input, declaration, nameArgument, extension);
|
|
709
|
+
async function forceRemoveContainer(exec, name) {
|
|
710
|
+
try {
|
|
711
|
+
await exec(["rm", "-f", name]);
|
|
712
|
+
} catch {
|
|
713
|
+
}
|
|
769
714
|
}
|
|
770
|
-
function
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
return createResolvedBaselineTarget(input, declaration, nameArgument, extension);
|
|
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`;
|
|
775
719
|
}
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
if (input.
|
|
786
|
-
|
|
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;
|
|
787
747
|
}
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
748
|
+
};
|
|
749
|
+
function rewriteContainerPath(path, mapping) {
|
|
750
|
+
const normalizedPath = normalizeForMatch(path);
|
|
751
|
+
const normalizedFrom = normalizeForMatch(mapping.from);
|
|
752
|
+
if (normalizedPath === normalizedFrom) return mapping.to;
|
|
753
|
+
if (normalizedPath.startsWith(`${normalizedFrom}/`)) return mapping.to + normalizedPath.slice(normalizedFrom.length);
|
|
754
|
+
return path;
|
|
755
|
+
}
|
|
756
|
+
function normalizeForMatch(p) {
|
|
757
|
+
return p.replace(/\\/g, "/").replace(/^[A-Z](?=:)/, (c) => c.toLowerCase());
|
|
758
|
+
}
|
|
759
|
+
var CONTAINER_TEST_LIST_PATH = "/tmp/crvy-rprtr-test-list.txt";
|
|
760
|
+
var REPORTER_BARE_SPECIFIER = "@crvy/rprtr";
|
|
761
|
+
var PATH_FLAGS = /* @__PURE__ */ new Set(["--config", "--reporter", "--test-list"]);
|
|
762
|
+
function rewritePlaywrightArgs(playwrightArgs, ctx, workDir, warn) {
|
|
763
|
+
const args = [];
|
|
764
|
+
const bindMounts = [];
|
|
765
|
+
for (let i = 0; i < playwrightArgs.length; i++) {
|
|
766
|
+
const flag = playwrightArgs[i];
|
|
767
|
+
if (!PATH_FLAGS.has(flag)) {
|
|
768
|
+
args.push(flag);
|
|
769
|
+
continue;
|
|
770
|
+
}
|
|
771
|
+
const value = playwrightArgs[i + 1];
|
|
772
|
+
if (value === void 0) break;
|
|
773
|
+
i += 1;
|
|
774
|
+
args.push(flag);
|
|
775
|
+
const rewritten = rewriteContainerPath(value, { from: ctx.cwd, to: workDir });
|
|
776
|
+
if (flag === "--reporter" && rewritten === value) {
|
|
777
|
+
args.push(REPORTER_BARE_SPECIFIER);
|
|
778
|
+
} else if (flag === "--test-list" && rewritten === value) {
|
|
779
|
+
args.push(CONTAINER_TEST_LIST_PATH);
|
|
780
|
+
bindMounts.push(`${value}:${CONTAINER_TEST_LIST_PATH}:ro`);
|
|
781
|
+
} else {
|
|
782
|
+
if (flag === "--config" && rewritten === value) {
|
|
783
|
+
warn(`--config "${value}" is outside the project directory and will not resolve inside the container.`);
|
|
784
|
+
}
|
|
785
|
+
args.push(rewritten);
|
|
786
|
+
}
|
|
792
787
|
}
|
|
793
|
-
return
|
|
788
|
+
return { args, bindMounts };
|
|
794
789
|
}
|
|
795
|
-
function
|
|
796
|
-
|
|
790
|
+
function resolvePlaywrightVersion(cwd) {
|
|
791
|
+
try {
|
|
792
|
+
const req = (0, import_node_module.createRequire)((0, import_node_path.join)(cwd, "package.json"));
|
|
793
|
+
const pkgPath = req.resolve("@playwright/test/package.json");
|
|
794
|
+
const pkg = JSON.parse((0, import_node_fs.readFileSync)(pkgPath, "utf8"));
|
|
795
|
+
return typeof pkg === "object" && pkg !== null && "version" in pkg && typeof pkg.version === "string" ? pkg.version : null;
|
|
796
|
+
} catch {
|
|
797
|
+
return null;
|
|
798
|
+
}
|
|
797
799
|
}
|
|
798
|
-
function
|
|
799
|
-
|
|
800
|
+
function rewriteContainerTestDescriptors(tests, mapping) {
|
|
801
|
+
if (tests === void 0 || mapping === void 0) return tests;
|
|
802
|
+
return tests.map((d) => ({ ...d, file: rewriteContainerPath(d.file, mapping) }));
|
|
800
803
|
}
|
|
801
|
-
function
|
|
802
|
-
|
|
804
|
+
function testListEntry(d, file) {
|
|
805
|
+
const loc = d.column === void 0 ? `${file}:${d.line}` : `${file}:${d.line}:${d.column}`;
|
|
806
|
+
const title = d.titlePath.join(" \u203A ");
|
|
807
|
+
const prefix = d.projectName !== void 0 && d.projectName !== "" ? `[${d.projectName}] \u203A ` : "";
|
|
808
|
+
return `${prefix}${loc} \u203A ${title}`;
|
|
803
809
|
}
|
|
804
|
-
function
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
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
|
-
}
|
|
810
|
+
function buildTestListEntries(tests, rootDir, cwd, pathStyle = "host") {
|
|
811
|
+
const convert = (p) => pathStyle === "posix" ? p.replace(/\\/g, "/") : p;
|
|
812
|
+
if (rootDir !== void 0) {
|
|
813
|
+
return tests.map(
|
|
814
|
+
(d) => testListEntry(d, convert((0, import_node_path.isAbsolute)(d.file) ? (0, import_node_path.relative)(rootDir, d.file) || d.file : d.file))
|
|
815
|
+
);
|
|
814
816
|
}
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
const
|
|
819
|
-
return
|
|
817
|
+
return tests.flatMap((d) => {
|
|
818
|
+
if (!(0, import_node_path.isAbsolute)(d.file)) return [testListEntry(d, convert(d.file))];
|
|
819
|
+
const rel = (0, import_node_path.relative)(cwd ?? process.cwd(), d.file);
|
|
820
|
+
const segments = pathStyle === "posix" ? rel.split(/[\\/]/) : rel.split(import_node_path.sep);
|
|
821
|
+
return segments.map((_, i) => testListEntry(d, convert(segments.slice(i).join(import_node_path.sep))));
|
|
820
822
|
});
|
|
821
823
|
}
|
|
822
824
|
|
|
823
|
-
// src/server/
|
|
824
|
-
var
|
|
825
|
-
var
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
825
|
+
// src/server/run-controller.ts
|
|
826
|
+
var import_child_process2 = require("child_process");
|
|
827
|
+
var import_node_fs2 = require("node:fs");
|
|
828
|
+
var import_node_module2 = require("node:module");
|
|
829
|
+
var import_node_os = require("node:os");
|
|
830
|
+
var import_node_path2 = require("node:path");
|
|
831
|
+
|
|
832
|
+
// src/server/run-launcher.ts
|
|
833
|
+
var import_commands = require("package-manager-detector/commands");
|
|
834
|
+
var import_detect2 = require("package-manager-detector/detect");
|
|
835
|
+
function resolvePlaywrightLaunch(cwd, playwrightArgs) {
|
|
836
|
+
const agent = (0, import_detect2.getUserAgent)();
|
|
837
|
+
const resolved = agent === null ? null : (0, import_commands.resolveCommand)(agent, "execute-local", ["playwright", ...playwrightArgs]);
|
|
838
|
+
if (resolved !== null) return { cmd: resolved.command, args: resolved.args };
|
|
839
|
+
return { cmd: "npx", args: ["playwright", ...playwrightArgs] };
|
|
831
840
|
}
|
|
832
|
-
function
|
|
833
|
-
|
|
841
|
+
function buildSpawnEnv(port, baseEnv = process.env) {
|
|
842
|
+
const env = {};
|
|
843
|
+
for (const [key, value] of Object.entries(baseEnv)) {
|
|
844
|
+
if (key === "CI") continue;
|
|
845
|
+
env[key] = value;
|
|
846
|
+
}
|
|
847
|
+
env.CRVY_RPRTR_SERVER_URL = `ws://localhost:${port}`;
|
|
848
|
+
env.PLAYWRIGHT_HTML_OPEN = "never";
|
|
849
|
+
return env;
|
|
834
850
|
}
|
|
835
|
-
function
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
851
|
+
function createLocalLauncher(options) {
|
|
852
|
+
return {
|
|
853
|
+
mode: "local",
|
|
854
|
+
launch({ ctx, playwrightArgs }) {
|
|
855
|
+
const resolve6 = options.resolveLaunch ?? resolvePlaywrightLaunch;
|
|
856
|
+
const { cmd, args } = resolve6(ctx.cwd, playwrightArgs);
|
|
857
|
+
return { cmd, args, env: buildSpawnEnv(options.port, options.env) };
|
|
858
|
+
}
|
|
859
|
+
};
|
|
841
860
|
}
|
|
842
861
|
|
|
843
|
-
// src/server/
|
|
844
|
-
|
|
862
|
+
// src/server/run-controller.ts
|
|
863
|
+
var import_meta = { url: require("url").pathToFileURL(__filename).href };
|
|
864
|
+
var STOP_GRACE_MS = 5e3;
|
|
865
|
+
var KNOWN_SIGNALS = {
|
|
866
|
+
SIGTERM: "SIGTERM",
|
|
867
|
+
SIGKILL: "SIGKILL"
|
|
868
|
+
};
|
|
869
|
+
function sharedProject(tests) {
|
|
870
|
+
const names = new Set(tests.map((t) => t.projectName ?? ""));
|
|
871
|
+
if (names.size === 1) {
|
|
872
|
+
const name = [...names][0];
|
|
873
|
+
return name === "" ? void 0 : name;
|
|
874
|
+
}
|
|
875
|
+
return void 0;
|
|
876
|
+
}
|
|
877
|
+
function gteMinor(version, major, minor) {
|
|
878
|
+
const match = /^(\d+)\.(\d+)/.exec(version.trim());
|
|
879
|
+
if (match === null) return false;
|
|
880
|
+
const maj = parseInt(match[1], 10);
|
|
881
|
+
const min = parseInt(match[2], 10);
|
|
882
|
+
if (maj !== major) return maj > major;
|
|
883
|
+
return min >= minor;
|
|
884
|
+
}
|
|
885
|
+
var defaultWriteTempFile = (content) => {
|
|
886
|
+
const path = (0, import_node_path2.join)((0, import_node_os.tmpdir)(), `crvy-rprtr-test-list-${process.pid}-${Date.now()}.txt`);
|
|
887
|
+
(0, import_node_fs2.writeFileSync)(path, content, "utf8");
|
|
888
|
+
return path;
|
|
889
|
+
};
|
|
890
|
+
function defaultDeleteTempFile(path) {
|
|
845
891
|
try {
|
|
846
|
-
|
|
892
|
+
(0, import_node_fs2.unlinkSync)(path);
|
|
847
893
|
} catch {
|
|
848
|
-
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
function resolveReporterDefault(cwd) {
|
|
897
|
+
try {
|
|
898
|
+
return (0, import_node_module2.createRequire)((0, import_node_path2.join)(cwd, "package.json")).resolve("@crvy/rprtr");
|
|
899
|
+
} catch {
|
|
900
|
+
}
|
|
901
|
+
try {
|
|
902
|
+
return (0, import_node_module2.createRequire)(import_meta.url).resolve("@crvy/rprtr");
|
|
903
|
+
} catch {
|
|
904
|
+
return null;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
var RunController = class {
|
|
908
|
+
constructor(deps) {
|
|
909
|
+
this.deps = deps;
|
|
910
|
+
}
|
|
911
|
+
child = null;
|
|
912
|
+
sigkillTimer = null;
|
|
913
|
+
testListPath = null;
|
|
914
|
+
get isRunning() {
|
|
915
|
+
return this.child !== null;
|
|
916
|
+
}
|
|
917
|
+
supportsTestList(cwd) {
|
|
918
|
+
const getVersion = this.deps.getPlaywrightVersion ?? resolvePlaywrightVersion;
|
|
919
|
+
const version = getVersion(cwd);
|
|
920
|
+
return version !== null && gteMinor(version, 1, 56);
|
|
921
|
+
}
|
|
922
|
+
cleanupTempFile() {
|
|
923
|
+
if (this.testListPath !== null) {
|
|
924
|
+
const del = this.deps.deleteTempFile ?? defaultDeleteTempFile;
|
|
925
|
+
del(this.testListPath);
|
|
926
|
+
this.testListPath = null;
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
buildPlaywrightArgs(ctx, filters, tests) {
|
|
930
|
+
const resolveReporter = this.deps.resolveReporter ?? resolveReporterDefault;
|
|
931
|
+
const reporterModule = resolveReporter(ctx.cwd);
|
|
932
|
+
const useTestList = tests !== void 0 && (tests.length > 1 || this.deps.containerPathMapping !== void 0) && this.supportsTestList(ctx.cwd);
|
|
933
|
+
const args = ["test", "--config", ctx.configFile];
|
|
934
|
+
if (reporterModule !== null) args.push("--reporter", reporterModule);
|
|
935
|
+
if (filters.update === true) args.push("--update-snapshots");
|
|
936
|
+
if (useTestList && tests !== void 0) {
|
|
937
|
+
const content = buildTestListEntries(
|
|
938
|
+
tests,
|
|
939
|
+
ctx.rootDir,
|
|
940
|
+
ctx.cwd,
|
|
941
|
+
this.deps.containerPathMapping === void 0 ? "host" : "posix"
|
|
942
|
+
).join("\n");
|
|
943
|
+
const writeTemp = this.deps.writeTempFile ?? defaultWriteTempFile;
|
|
944
|
+
this.testListPath = writeTemp(content);
|
|
945
|
+
args.push("--test-list", this.testListPath);
|
|
946
|
+
} else if (tests !== void 0 && tests.length > 0) {
|
|
947
|
+
const project = sharedProject(tests);
|
|
948
|
+
if (project !== void 0) args.push(`--project=${project}`);
|
|
949
|
+
for (const d of tests) {
|
|
950
|
+
args.push(d.column === void 0 ? `${d.file}:${d.line}` : `${d.file}:${d.line}:${d.column}`);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
return args;
|
|
954
|
+
}
|
|
955
|
+
start(filters) {
|
|
956
|
+
const ctx = this.deps.getRunContext();
|
|
957
|
+
if (ctx === null) return { ok: false, reason: "no-config" };
|
|
958
|
+
if (this.child !== null) return { ok: false, reason: "already-running" };
|
|
959
|
+
if (filters.tests !== void 0 && filters.tests.length === 0) return { ok: false, reason: "no-tests" };
|
|
960
|
+
if (this.deps.launcher.available === false) return { ok: false, reason: "docker-unavailable" };
|
|
961
|
+
const tests = rewriteContainerTestDescriptors(filters.tests, this.deps.containerPathMapping);
|
|
962
|
+
const args = this.buildPlaywrightArgs(ctx, filters, tests);
|
|
963
|
+
const spec = this.deps.launcher.launch({ ctx, playwrightArgs: args });
|
|
964
|
+
let child;
|
|
965
|
+
try {
|
|
966
|
+
child = this.deps.spawn(spec.cmd, spec.args, { cwd: ctx.cwd, env: spec.env, stdio: "inherit" });
|
|
967
|
+
} catch (err) {
|
|
968
|
+
this.cleanupTempFile();
|
|
969
|
+
throw err;
|
|
970
|
+
}
|
|
971
|
+
this.child = child;
|
|
972
|
+
child.on("exit", (code) => {
|
|
973
|
+
this.handleChildExit(code);
|
|
974
|
+
});
|
|
975
|
+
child.on("error", () => {
|
|
976
|
+
this.handleChildExit(null);
|
|
977
|
+
});
|
|
978
|
+
this.deps.setReportRunning(true);
|
|
979
|
+
this.deps.setRunFiltered?.(filters.tests !== void 0);
|
|
980
|
+
this.deps.broadcast({ type: "run-status", data: { running: true, mode: this.deps.launcher.mode } });
|
|
981
|
+
return { ok: true };
|
|
982
|
+
}
|
|
983
|
+
stop() {
|
|
984
|
+
if (this.child === null) return { ok: false, reason: "not-running" };
|
|
985
|
+
if (this.sigkillTimer !== null) this.deps.timers.clearTimeout(this.sigkillTimer);
|
|
986
|
+
this.child.kill("SIGTERM");
|
|
987
|
+
this.sigkillTimer = this.deps.timers.setTimeout(() => {
|
|
988
|
+
if (this.child !== null) {
|
|
989
|
+
this.child.kill("SIGKILL");
|
|
990
|
+
this.deps.launcher.onForceKill?.();
|
|
991
|
+
}
|
|
992
|
+
}, STOP_GRACE_MS);
|
|
993
|
+
return { ok: true };
|
|
994
|
+
}
|
|
995
|
+
async prepareRun() {
|
|
996
|
+
const launcher = this.deps.launcher;
|
|
997
|
+
if (launcher.prepare === void 0) return { ok: true };
|
|
998
|
+
const ctx = this.deps.getRunContext();
|
|
999
|
+
if (ctx === null) return { ok: true };
|
|
1000
|
+
try {
|
|
1001
|
+
await launcher.prepare({
|
|
1002
|
+
ctx,
|
|
1003
|
+
onProgress: (phase) => {
|
|
1004
|
+
this.deps.broadcast({ type: "run-status", data: { running: true, mode: launcher.mode, phase } });
|
|
1005
|
+
}
|
|
1006
|
+
});
|
|
1007
|
+
return { ok: true };
|
|
1008
|
+
} catch (error) {
|
|
1009
|
+
this.deps.broadcast({ type: "run-status", data: { running: false, mode: launcher.mode } });
|
|
1010
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1011
|
+
console.warn(`[RunController] run preparation failed: ${message}`);
|
|
1012
|
+
return { ok: false, reason: "docker-unavailable" };
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
dispose() {
|
|
1016
|
+
if (this.child === null) return;
|
|
1017
|
+
if (this.sigkillTimer !== null) this.deps.timers.clearTimeout(this.sigkillTimer);
|
|
1018
|
+
this.sigkillTimer = null;
|
|
1019
|
+
this.child.kill("SIGKILL");
|
|
1020
|
+
this.deps.launcher.onForceKill?.();
|
|
1021
|
+
this.cleanupTempFile();
|
|
1022
|
+
}
|
|
1023
|
+
handleChildExit(code) {
|
|
1024
|
+
if (this.child === null) return;
|
|
1025
|
+
if (this.sigkillTimer !== null) this.deps.timers.clearTimeout(this.sigkillTimer);
|
|
1026
|
+
this.sigkillTimer = null;
|
|
1027
|
+
this.child = null;
|
|
1028
|
+
this.cleanupTempFile();
|
|
1029
|
+
if (code !== null && code !== 0) console.warn(`[RunController] playwright test exited with code ${code}`);
|
|
1030
|
+
this.deps.setReportRunning(false);
|
|
1031
|
+
this.deps.broadcast({ type: "run-status", data: { running: false, mode: this.deps.launcher.mode } });
|
|
1032
|
+
void this.deps.saveReport?.();
|
|
1033
|
+
}
|
|
1034
|
+
};
|
|
1035
|
+
function createRealSpawn() {
|
|
1036
|
+
return (cmd, args, opts) => {
|
|
1037
|
+
const cp = (0, import_child_process2.spawn)(cmd, args, opts);
|
|
1038
|
+
return {
|
|
1039
|
+
on: (event, cb) => cp.on(event, cb),
|
|
1040
|
+
kill: (signal) => {
|
|
1041
|
+
const sig = KNOWN_SIGNALS[signal];
|
|
1042
|
+
if (sig !== void 0) cp.kill(sig);
|
|
1043
|
+
}
|
|
1044
|
+
};
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
function createRealTimers() {
|
|
1048
|
+
const pending = [];
|
|
1049
|
+
return {
|
|
1050
|
+
setTimeout: (fn, ms) => {
|
|
1051
|
+
const id = setTimeout(fn, ms);
|
|
1052
|
+
pending.push(id);
|
|
1053
|
+
return id;
|
|
1054
|
+
},
|
|
1055
|
+
clearTimeout: () => {
|
|
1056
|
+
for (const h of pending.splice(0)) clearTimeout(h);
|
|
1057
|
+
}
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
// src/server/docker-launcher.ts
|
|
1062
|
+
var DOCKER_WORK_DIR = "/work";
|
|
1063
|
+
var DOCKER_HOST_GATEWAY = "host.docker.internal";
|
|
1064
|
+
var ENV_DENYLIST = /* @__PURE__ */ new Set([
|
|
1065
|
+
"CI",
|
|
1066
|
+
"PLAYWRIGHT_BROWSERS_PATH",
|
|
1067
|
+
"CRVY_RPRTR_SERVER_URL",
|
|
1068
|
+
"CRVY_RPRTR_PORTABLE_ARTIFACTS",
|
|
1069
|
+
"TZ",
|
|
1070
|
+
"LANG",
|
|
1071
|
+
"LC_ALL",
|
|
1072
|
+
"PLAYWRIGHT_HTML_OPEN",
|
|
1073
|
+
"PATH"
|
|
1074
|
+
]);
|
|
1075
|
+
var WINDOWS_ENV_NOISE = new Set(
|
|
1076
|
+
"SYSTEMROOT COMSPEC WINDIR PATHEXT OS PROGRAMFILES PROGRAMFILES(X86) PROGRAMW6432 PROGRAMDATA ALLUSERSPROFILE PUBLIC APPDATA LOCALAPPDATA TEMP TMP USERPROFILE HOMEDRIVE HOMEPATH USERNAME PSMODULEPATH DRIVERDATA NUMBER_OF_PROCESSORS PROCESSOR_ARCHITECTURE PROCESSOR_IDENTIFIER PROCESSOR_LEVEL PROCESSOR_REVISION".split(
|
|
1077
|
+
" "
|
|
1078
|
+
)
|
|
1079
|
+
);
|
|
1080
|
+
var DockerUnavailableError = class extends Error {
|
|
1081
|
+
constructor() {
|
|
1082
|
+
super("Docker daemon is not available");
|
|
1083
|
+
this.name = "DockerUnavailableError";
|
|
1084
|
+
}
|
|
1085
|
+
};
|
|
1086
|
+
function defaultWarn(message) {
|
|
1087
|
+
console.warn(`[crvy-rprtr] ${message}`);
|
|
1088
|
+
}
|
|
1089
|
+
async function detectAgentName(detect2, cwd) {
|
|
1090
|
+
const detected = await (detect2 ?? detectProjectAgent)(cwd);
|
|
1091
|
+
return detected?.name ?? null;
|
|
1092
|
+
}
|
|
1093
|
+
async function prepareDocker(state, exec, ctx, deps, onProgress) {
|
|
1094
|
+
if (deps.platform === "win32" && !state.warnedWin32) {
|
|
1095
|
+
state.warnedWin32 = true;
|
|
1096
|
+
deps.warn(
|
|
1097
|
+
"Native Windows host detected: docker run mode is experimental on this platform. For CI-identical baselines, run crvy-rprtr from WSL2 with the project stored in the WSL filesystem."
|
|
1098
|
+
);
|
|
1099
|
+
}
|
|
1100
|
+
if (!await probeDockerDaemon(exec)) {
|
|
1101
|
+
state.available = false;
|
|
1102
|
+
throw new DockerUnavailableError();
|
|
1103
|
+
}
|
|
1104
|
+
state.available = true;
|
|
1105
|
+
const image = resolveDockerImage({ image: deps.docker?.image, version: deps.getPlaywrightVersion(ctx.cwd) });
|
|
1106
|
+
if (image === null) {
|
|
1107
|
+
throw new Error("Could not resolve the installed @playwright/test version; set docker.image explicitly.");
|
|
1108
|
+
}
|
|
1109
|
+
state.image = image;
|
|
1110
|
+
state.command = resolveContainerCommand({
|
|
1111
|
+
command: deps.docker?.command,
|
|
1112
|
+
hasCustomImage: deps.docker?.image !== void 0,
|
|
1113
|
+
detectedAgentName: deps.docker?.image === void 0 ? "npm" : await detectAgentName(deps.detectAgent, ctx.cwd),
|
|
1114
|
+
warn: deps.warn
|
|
1115
|
+
});
|
|
1116
|
+
if (!await isDockerImagePresent(exec, image)) {
|
|
1117
|
+
onProgress("pulling");
|
|
1118
|
+
if (!await pullDockerImage(exec, image)) {
|
|
1119
|
+
throw new Error(`Failed to pull docker image: ${image}`);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
function buildDockerRunArgs(ctx, playwrightArgs, deps) {
|
|
1124
|
+
const { args: rewrittenArgs, bindMounts } = rewritePlaywrightArgs(playwrightArgs, ctx, deps.workDir, deps.warn);
|
|
1125
|
+
const args = [
|
|
1126
|
+
"run",
|
|
1127
|
+
"--rm",
|
|
1128
|
+
"--init",
|
|
1129
|
+
"--name",
|
|
1130
|
+
deps.containerName,
|
|
1131
|
+
"--add-host",
|
|
1132
|
+
`${DOCKER_HOST_GATEWAY}:host-gateway`,
|
|
1133
|
+
"--ipc=host"
|
|
1134
|
+
];
|
|
1135
|
+
if (deps.docker?.platform !== void 0) {
|
|
1136
|
+
args.push("--platform", deps.docker.platform);
|
|
1137
|
+
}
|
|
1138
|
+
args.push("-v", `${ctx.cwd}:${deps.workDir}:rw`, "-w", deps.workDir);
|
|
1139
|
+
for (const mount of bindMounts) {
|
|
1140
|
+
args.push("-v", mount);
|
|
1141
|
+
}
|
|
1142
|
+
args.push("-e", `CRVY_RPRTR_SERVER_URL=ws://${DOCKER_HOST_GATEWAY}:${deps.port}`);
|
|
1143
|
+
args.push("-e", "CRVY_RPRTR_PORTABLE_ARTIFACTS=1", "-e", "TZ=UTC", "-e", "LANG=C.UTF-8", "-e", "LC_ALL=C.UTF-8");
|
|
1144
|
+
args.push("-e", "PLAYWRIGHT_HTML_OPEN=never");
|
|
1145
|
+
for (const [key, value] of Object.entries(deps.env)) {
|
|
1146
|
+
const upper = key.toUpperCase();
|
|
1147
|
+
if (ENV_DENYLIST.has(upper) || WINDOWS_ENV_NOISE.has(upper) || value === void 0) continue;
|
|
1148
|
+
args.push("-e", key);
|
|
1149
|
+
}
|
|
1150
|
+
if (deps.docker?.extraArgs !== void 0) args.push(...deps.docker.extraArgs);
|
|
1151
|
+
args.push(deps.image, ...deps.command, "playwright", ...rewrittenArgs);
|
|
1152
|
+
return args;
|
|
1153
|
+
}
|
|
1154
|
+
function stripCi(env) {
|
|
1155
|
+
const out = {};
|
|
1156
|
+
for (const [key, value] of Object.entries(env)) {
|
|
1157
|
+
if (key === "CI") continue;
|
|
1158
|
+
out[key] = value;
|
|
1159
|
+
}
|
|
1160
|
+
return out;
|
|
1161
|
+
}
|
|
1162
|
+
function createState(docker) {
|
|
1163
|
+
return {
|
|
1164
|
+
available: void 0,
|
|
1165
|
+
prepared: null,
|
|
1166
|
+
image: null,
|
|
1167
|
+
command: docker?.command ?? DEFAULT_CONTAINER_COMMAND,
|
|
1168
|
+
warnedWin32: false
|
|
1169
|
+
};
|
|
1170
|
+
}
|
|
1171
|
+
function buildLauncher(state, deps) {
|
|
1172
|
+
return {
|
|
1173
|
+
mode: "docker",
|
|
1174
|
+
get available() {
|
|
1175
|
+
return state.available;
|
|
1176
|
+
},
|
|
1177
|
+
prepare({ ctx, onProgress }) {
|
|
1178
|
+
state.prepared ??= prepareDocker(
|
|
1179
|
+
state,
|
|
1180
|
+
deps.exec,
|
|
1181
|
+
ctx,
|
|
1182
|
+
{
|
|
1183
|
+
docker: deps.docker,
|
|
1184
|
+
getPlaywrightVersion: deps.getVersion,
|
|
1185
|
+
detectAgent: deps.detectAgent,
|
|
1186
|
+
warn: deps.warn,
|
|
1187
|
+
platform: deps.platform
|
|
1188
|
+
},
|
|
1189
|
+
onProgress
|
|
1190
|
+
).catch((error) => {
|
|
1191
|
+
state.prepared = null;
|
|
1192
|
+
state.warnedWin32 = false;
|
|
1193
|
+
throw error;
|
|
1194
|
+
});
|
|
1195
|
+
return state.prepared;
|
|
1196
|
+
},
|
|
1197
|
+
launch({ ctx, playwrightArgs }) {
|
|
1198
|
+
const image = state.image ?? resolveDockerImage({ image: deps.docker?.image, version: deps.getVersion(ctx.cwd) });
|
|
1199
|
+
if (image === null) {
|
|
1200
|
+
throw new Error("Could not resolve the docker image; run prepare() first or set docker.image.");
|
|
1201
|
+
}
|
|
1202
|
+
const args = buildDockerRunArgs(ctx, playwrightArgs, {
|
|
1203
|
+
docker: deps.docker,
|
|
1204
|
+
workDir: deps.workDir,
|
|
1205
|
+
containerName: deps.containerName,
|
|
1206
|
+
port: deps.port,
|
|
1207
|
+
env: deps.baseEnv,
|
|
1208
|
+
image,
|
|
1209
|
+
command: state.command,
|
|
1210
|
+
warn: deps.warn
|
|
1211
|
+
});
|
|
1212
|
+
return { cmd: "docker", args, env: stripCi(deps.baseEnv) };
|
|
1213
|
+
},
|
|
1214
|
+
onForceKill() {
|
|
1215
|
+
void forceRemoveContainer(deps.exec, deps.containerName);
|
|
1216
|
+
}
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
function createDockerLauncher(options) {
|
|
1220
|
+
return buildLauncher(createState(options.docker), {
|
|
1221
|
+
exec: options.exec ?? createDockerExec(),
|
|
1222
|
+
workDir: options.workDir ?? DOCKER_WORK_DIR,
|
|
1223
|
+
containerName: options.containerName ?? `crvy-rprtr-run-${process.pid}`,
|
|
1224
|
+
baseEnv: options.env ?? process.env,
|
|
1225
|
+
getVersion: options.getPlaywrightVersion ?? resolvePlaywrightVersion,
|
|
1226
|
+
detectAgent: options.detectAgent,
|
|
1227
|
+
warn: options.warn ?? defaultWarn,
|
|
1228
|
+
platform: options.platform ?? process.platform,
|
|
1229
|
+
docker: options.docker,
|
|
1230
|
+
port: options.port
|
|
1231
|
+
});
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
// src/server/handlers.ts
|
|
1235
|
+
var import_fs2 = require("fs");
|
|
1236
|
+
var import_path6 = require("path");
|
|
1237
|
+
|
|
1238
|
+
// src/server/artifact-routes.ts
|
|
1239
|
+
var import_fs = require("fs");
|
|
1240
|
+
var import_promises3 = require("fs/promises");
|
|
1241
|
+
var import_path5 = require("path");
|
|
1242
|
+
|
|
1243
|
+
// src/snapshot-path-resolver.ts
|
|
1244
|
+
var import_crypto = require("crypto");
|
|
1245
|
+
var import_path3 = require("path");
|
|
1246
|
+
var DEFAULT_SCREENSHOT_TEMPLATE = "{snapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}{-projectName}{-snapshotSuffix}{ext}";
|
|
1247
|
+
var WINDOWS_FILESYSTEM_FRIENDLY_LENGTH = 60;
|
|
1248
|
+
function isUnsafeFilePathCharacter(character) {
|
|
1249
|
+
const codePoint = character.codePointAt(0);
|
|
1250
|
+
return codePoint !== void 0 && (codePoint <= 44 || codePoint >= 46 && codePoint <= 47 || codePoint >= 58 && codePoint <= 64 || codePoint >= 91 && codePoint <= 96 || codePoint >= 123 && codePoint <= 127);
|
|
1251
|
+
}
|
|
1252
|
+
function sanitizeForFilePath(value) {
|
|
1253
|
+
return Array.from(value).reduce(
|
|
1254
|
+
(state, character) => {
|
|
1255
|
+
const unsafeCharacter = isUnsafeFilePathCharacter(character);
|
|
1256
|
+
return unsafeCharacter ? state.previousCharacterWasUnsafe ? state : {
|
|
1257
|
+
value: `${state.value}-`,
|
|
1258
|
+
previousCharacterWasUnsafe: true
|
|
1259
|
+
} : {
|
|
1260
|
+
value: `${state.value}${character}`,
|
|
1261
|
+
previousCharacterWasUnsafe: false
|
|
1262
|
+
};
|
|
1263
|
+
},
|
|
1264
|
+
{
|
|
1265
|
+
value: "",
|
|
1266
|
+
previousCharacterWasUnsafe: false
|
|
1267
|
+
}
|
|
1268
|
+
).value;
|
|
1269
|
+
}
|
|
1270
|
+
function trimLongString(value, length = WINDOWS_FILESYSTEM_FRIENDLY_LENGTH) {
|
|
1271
|
+
if (value.length <= length) {
|
|
1272
|
+
return value;
|
|
1273
|
+
}
|
|
1274
|
+
const hash = (0, import_crypto.createHash)("sha1").update(value).digest("hex");
|
|
1275
|
+
const middle = `-${hash.slice(0, 5)}-`;
|
|
1276
|
+
const start = Math.floor((length - middle.length) / 2);
|
|
1277
|
+
const end = length - middle.length - start;
|
|
1278
|
+
return value.slice(0, start) + middle + value.slice(-end);
|
|
1279
|
+
}
|
|
1280
|
+
function sanitizeFilePathBeforeExtension(filePath, extension = (0, import_path3.extname)(filePath)) {
|
|
1281
|
+
const base = filePath.slice(0, filePath.length - extension.length);
|
|
1282
|
+
return sanitizeForFilePath(base) + extension;
|
|
1283
|
+
}
|
|
1284
|
+
function addSuffixToFilePath(filePath, suffix) {
|
|
1285
|
+
const extension = (0, import_path3.extname)(filePath);
|
|
1286
|
+
return filePath.slice(0, filePath.length - extension.length) + suffix + extension;
|
|
1287
|
+
}
|
|
1288
|
+
function normalizedSnapshotDir(config) {
|
|
1289
|
+
return (0, import_path3.resolve)(config.configDir, config.snapshotDir);
|
|
1290
|
+
}
|
|
1291
|
+
function templateValue(template, token, value) {
|
|
1292
|
+
return template.replace(
|
|
1293
|
+
new RegExp(`\\{(.)?${token}\\}`, "g"),
|
|
1294
|
+
(_, prefix) => value === "" ? "" : `${prefix ?? ""}${value}`
|
|
1295
|
+
);
|
|
1296
|
+
}
|
|
1297
|
+
function applyTemplate(input, nameArgument, extension) {
|
|
1298
|
+
const template = input.config.toHaveScreenshotPathTemplate ?? input.config.snapshotPathTemplate ?? DEFAULT_SCREENSHOT_TEMPLATE;
|
|
1299
|
+
const relativeTestFilePath = (0, import_path3.relative)(input.config.testDir, input.testFile);
|
|
1300
|
+
const parsed = (0, import_path3.parse)(relativeTestFilePath);
|
|
1301
|
+
const tokens = [
|
|
1302
|
+
["testDir", input.config.testDir],
|
|
1303
|
+
["snapshotDir", normalizedSnapshotDir(input.config)],
|
|
1304
|
+
["snapshotSuffix", input.config.snapshotSuffix],
|
|
1305
|
+
["testFileDir", parsed.dir],
|
|
1306
|
+
["platform", process.platform],
|
|
1307
|
+
["projectName", sanitizeForFilePath(input.config.projectName)],
|
|
1308
|
+
["testName", ""],
|
|
1309
|
+
["testFileName", parsed.base],
|
|
1310
|
+
["testFilePath", relativeTestFilePath],
|
|
1311
|
+
["arg", nameArgument],
|
|
1312
|
+
["ext", extension]
|
|
1313
|
+
];
|
|
1314
|
+
const snapshotPath = tokens.reduce(
|
|
1315
|
+
(currentTemplate, [token, value]) => templateValue(currentTemplate, token, value),
|
|
1316
|
+
template
|
|
1317
|
+
);
|
|
1318
|
+
return (0, import_path3.resolve)(input.config.configDir, snapshotPath);
|
|
1319
|
+
}
|
|
1320
|
+
function removeExtension(filePath, extension = (0, import_path3.extname)(filePath)) {
|
|
1321
|
+
return filePath.slice(0, filePath.length - extension.length);
|
|
1322
|
+
}
|
|
1323
|
+
function snapshotNameParts(declaredName) {
|
|
1324
|
+
const extension = (0, import_path3.extname)(declaredName) || ".png";
|
|
1325
|
+
return {
|
|
1326
|
+
extension,
|
|
1327
|
+
filePath: (0, import_path3.extname)(declaredName) === "" ? `${declaredName}${extension}` : declaredName
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1330
|
+
function filePathForOccurrence(filePath, occurrenceIndex) {
|
|
1331
|
+
return occurrenceIndex === 1 ? filePath : addSuffixToFilePath(filePath, `-${occurrenceIndex - 1}`);
|
|
1332
|
+
}
|
|
1333
|
+
function createResolvedBaselineTarget(input, declaration, nameArgument, extension) {
|
|
1334
|
+
return {
|
|
1335
|
+
visualName: declaration.visualName,
|
|
1336
|
+
attachmentBaseName: declaration.visualName,
|
|
1337
|
+
artifactBaseName: sanitizeForFilePath(declaration.visualName),
|
|
1338
|
+
snapshotPath: applyTemplate(input, nameArgument, extension)
|
|
1339
|
+
};
|
|
1340
|
+
}
|
|
1341
|
+
function resolveStringCallTarget(input, declaration) {
|
|
1342
|
+
const { extension, filePath } = snapshotNameParts(declaration.declaredName);
|
|
1343
|
+
const occurrenceFilePath = filePathForOccurrence(filePath, declaration.occurrenceIndex);
|
|
1344
|
+
const sanitizedNameWithExtension = sanitizeFilePathBeforeExtension(occurrenceFilePath, extension);
|
|
1345
|
+
const nameArgument = removeExtension(sanitizedNameWithExtension, extension);
|
|
1346
|
+
return createResolvedBaselineTarget(input, declaration, nameArgument, extension);
|
|
1347
|
+
}
|
|
1348
|
+
function resolveArrayCallTarget(input, declaration) {
|
|
1349
|
+
const { extension, filePath } = snapshotNameParts(declaration.declaredName);
|
|
1350
|
+
const occurrenceFilePath = filePathForOccurrence(filePath, declaration.occurrenceIndex);
|
|
1351
|
+
const nameArgument = (0, import_path3.join)((0, import_path3.dirname)(occurrenceFilePath), (0, import_path3.basename)(occurrenceFilePath, extension));
|
|
1352
|
+
return createResolvedBaselineTarget(input, declaration, nameArgument, extension);
|
|
1353
|
+
}
|
|
1354
|
+
function resolveNamedTarget(input, declaration) {
|
|
1355
|
+
if (!declaration.declaredName.includes("/")) {
|
|
1356
|
+
return resolveStringCallTarget(input, declaration);
|
|
1357
|
+
}
|
|
1358
|
+
const stringCallTarget = resolveStringCallTarget(input, declaration);
|
|
1359
|
+
const arrayCallTarget = resolveArrayCallTarget(input, declaration);
|
|
1360
|
+
if (stringCallTarget.snapshotPath === arrayCallTarget.snapshotPath) {
|
|
1361
|
+
return stringCallTarget;
|
|
1362
|
+
}
|
|
1363
|
+
if (input.snapshotPathExists === void 0) {
|
|
1364
|
+
return void 0;
|
|
1365
|
+
}
|
|
1366
|
+
const stringCallTargetExists = input.snapshotPathExists(stringCallTarget.snapshotPath);
|
|
1367
|
+
const arrayCallTargetExists = input.snapshotPathExists(arrayCallTarget.snapshotPath);
|
|
1368
|
+
if (stringCallTargetExists === arrayCallTargetExists) {
|
|
1369
|
+
return void 0;
|
|
1370
|
+
}
|
|
1371
|
+
return stringCallTargetExists ? stringCallTarget : arrayCallTarget;
|
|
1372
|
+
}
|
|
1373
|
+
function reporterTitlesWithoutProjectAndFile(reporterTitlePath2) {
|
|
1374
|
+
return reporterTitlePath2.slice(3).filter((part) => part !== "");
|
|
1375
|
+
}
|
|
1376
|
+
function anonymousNameFromTitles(titles, occurrenceIndex) {
|
|
1377
|
+
return sanitizeFilePathBeforeExtension(trimLongString(`${titles.join(" ")} ${occurrenceIndex}.png`), ".png");
|
|
1378
|
+
}
|
|
1379
|
+
function anonymousName(reporterTitlePath2, occurrenceIndex) {
|
|
1380
|
+
return anonymousNameFromTitles(reporterTitlesWithoutProjectAndFile(reporterTitlePath2), occurrenceIndex);
|
|
1381
|
+
}
|
|
1382
|
+
function resolveTarget(input, declaration) {
|
|
1383
|
+
switch (declaration.kind) {
|
|
1384
|
+
case "named":
|
|
1385
|
+
return typeof declaration.declaredName === "string" && declaration.declaredName !== "" ? resolveNamedTarget(input, declaration) : void 0;
|
|
1386
|
+
case "unnamed": {
|
|
1387
|
+
const anonymousFileName = anonymousName(input.reporterTitlePath, declaration.occurrenceIndex);
|
|
1388
|
+
const extension = ".png";
|
|
1389
|
+
const nameArgument = (0, import_path3.join)((0, import_path3.dirname)(anonymousFileName), (0, import_path3.basename)(anonymousFileName, extension));
|
|
1390
|
+
return createResolvedBaselineTarget(input, declaration, nameArgument, extension);
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
function resolveBaselineTargets(input) {
|
|
1395
|
+
return input.declarations.flatMap((declaration) => {
|
|
1396
|
+
const resolvedTarget = resolveTarget(input, declaration);
|
|
1397
|
+
return resolvedTarget === void 0 ? [] : [resolvedTarget];
|
|
1398
|
+
});
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
// src/server/utils.ts
|
|
1402
|
+
var import_path4 = require("path");
|
|
1403
|
+
var LIVE_UPDATES_WEBSOCKET_PATH = "/";
|
|
1404
|
+
function broadcastToBrowsers(wsClients, msg) {
|
|
1405
|
+
const payload = JSON.stringify(msg);
|
|
1406
|
+
wsClients.forEach((ws) => {
|
|
1407
|
+
ws.send(payload);
|
|
1408
|
+
});
|
|
1409
|
+
}
|
|
1410
|
+
function isWebSocketUpgradeRequest(req) {
|
|
1411
|
+
return new URL(req.url).pathname === LIVE_UPDATES_WEBSOCKET_PATH && req.headers.get("upgrade")?.toLowerCase() === "websocket";
|
|
1412
|
+
}
|
|
1413
|
+
function isPathWithinRoots(target, roots) {
|
|
1414
|
+
const resolvedTarget = (0, import_path4.resolve)(target);
|
|
1415
|
+
return roots.some((root) => {
|
|
1416
|
+
const rel = (0, import_path4.relative)((0, import_path4.resolve)(root), resolvedTarget);
|
|
1417
|
+
return rel === "" || !rel.startsWith(`..${import_path4.sep}`) && rel !== ".." && !(0, import_path4.isAbsolute)(rel);
|
|
1418
|
+
});
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
// src/server/artifact-routes.ts
|
|
1422
|
+
async function realpathOrNull(path) {
|
|
1423
|
+
try {
|
|
1424
|
+
return await (0, import_promises3.realpath)(path);
|
|
1425
|
+
} catch {
|
|
1426
|
+
return null;
|
|
849
1427
|
}
|
|
850
1428
|
}
|
|
851
1429
|
async function handleFile(ctx, req) {
|
|
@@ -886,11 +1464,14 @@ function reporterTitlePath(test) {
|
|
|
886
1464
|
return ["", projectName, testFile ?? "", ...test.titlePath, test.title];
|
|
887
1465
|
}
|
|
888
1466
|
function resolveBaselineSnapshotPath(routing, test, retry, imageName) {
|
|
889
|
-
const
|
|
1467
|
+
const mapping = routing?.containerPathMapping;
|
|
1468
|
+
const rawTestFile = test.location?.file;
|
|
1469
|
+
const testFile = rawTestFile === void 0 || mapping === void 0 ? rawTestFile : rewriteContainerPath(rawTestFile, mapping);
|
|
890
1470
|
const declaration = test.results?.[retry]?.visualDeclarations?.find((candidate) => candidate.visualName === imageName);
|
|
891
1471
|
if (routing === void 0 || testFile === void 0 || declaration === void 0) {
|
|
892
1472
|
return null;
|
|
893
1473
|
}
|
|
1474
|
+
const isContainerPath = mapping !== void 0 && testFile !== rawTestFile;
|
|
894
1475
|
const targets = resolveBaselineTargets({
|
|
895
1476
|
testFile,
|
|
896
1477
|
reporterTitlePath: reporterTitlePath(test),
|
|
@@ -900,7 +1481,7 @@ function resolveBaselineSnapshotPath(routing, test, retry, imageName) {
|
|
|
900
1481
|
testDir: routing.playwrightTestDir ?? (0, import_path5.dirname)(testFile),
|
|
901
1482
|
snapshotDir: routing.playwrightSnapshotDir ?? (0, import_path5.dirname)(testFile),
|
|
902
1483
|
projectName: test.projectName ?? test.browser,
|
|
903
|
-
snapshotSuffix: process.platform,
|
|
1484
|
+
snapshotSuffix: isContainerPath ? "linux" : process.platform,
|
|
904
1485
|
snapshotPathTemplate: routing.playwrightSnapshotPathTemplate,
|
|
905
1486
|
toHaveScreenshotPathTemplate: routing.playwrightToHaveScreenshotPathTemplate
|
|
906
1487
|
},
|
|
@@ -1023,7 +1604,19 @@ function handleSync(ctx) {
|
|
|
1023
1604
|
};
|
|
1024
1605
|
broadcastToBrowsers(ctx.wsClients, message);
|
|
1025
1606
|
}
|
|
1026
|
-
function
|
|
1607
|
+
function applyContainerPathMapping(rawData, mapping) {
|
|
1608
|
+
return {
|
|
1609
|
+
...rawData,
|
|
1610
|
+
playwrightSnapshotDir: rawData.playwrightSnapshotDir === void 0 ? void 0 : rewriteContainerPath(rawData.playwrightSnapshotDir, mapping),
|
|
1611
|
+
playwrightTestDir: rawData.playwrightTestDir === void 0 ? void 0 : rewriteContainerPath(rawData.playwrightTestDir, mapping),
|
|
1612
|
+
playwrightRootDir: rawData.playwrightRootDir === void 0 ? void 0 : rewriteContainerPath(rawData.playwrightRootDir, mapping),
|
|
1613
|
+
configFile: rawData.configFile === void 0 ? void 0 : rewriteContainerPath(rawData.configFile, mapping),
|
|
1614
|
+
cwd: rawData.cwd === void 0 ? void 0 : rewriteContainerPath(rawData.cwd, mapping)
|
|
1615
|
+
};
|
|
1616
|
+
}
|
|
1617
|
+
function handleRegister(ctx, rawData) {
|
|
1618
|
+
const mapping = ctx.routesContext.containerPathMapping;
|
|
1619
|
+
const data = mapping === void 0 ? rawData : applyContainerPathMapping(rawData, mapping);
|
|
1027
1620
|
const roots = [];
|
|
1028
1621
|
if (data.playwrightSnapshotDir !== void 0 && data.playwrightSnapshotDir !== "") {
|
|
1029
1622
|
roots.push(data.playwrightSnapshotDir);
|
|
@@ -1053,16 +1646,65 @@ function handleRegister(ctx, data) {
|
|
|
1053
1646
|
}
|
|
1054
1647
|
}
|
|
1055
1648
|
if (data.configFile !== void 0 && data.cwd !== void 0) {
|
|
1056
|
-
ctx.routesContext.runContext =
|
|
1649
|
+
ctx.routesContext.runContext = buildRunContext(data.configFile, data);
|
|
1057
1650
|
}
|
|
1058
1651
|
console.log("[Server] Reporter registered with config:", {
|
|
1059
1652
|
playwrightSnapshotDir: data.playwrightSnapshotDir,
|
|
1060
|
-
playwrightTestDir: data.playwrightTestDir
|
|
1653
|
+
playwrightTestDir: data.playwrightTestDir,
|
|
1654
|
+
configFile: data.configFile,
|
|
1655
|
+
cwd: data.cwd
|
|
1656
|
+
});
|
|
1657
|
+
}
|
|
1658
|
+
function buildRunContext(configFile, data) {
|
|
1659
|
+
const configDir = (0, import_path6.dirname)(configFile);
|
|
1660
|
+
return {
|
|
1661
|
+
configFile,
|
|
1662
|
+
cwd: configDir,
|
|
1663
|
+
rootDir: data.playwrightRootDir ?? (data.playwrightTestDir === void 0 ? configDir : (0, import_path6.resolve)(configDir, data.playwrightTestDir))
|
|
1664
|
+
};
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
// src/ci.ts
|
|
1668
|
+
function isCI(env = process.env) {
|
|
1669
|
+
const ci = env.CI;
|
|
1670
|
+
return ci !== void 0 && ci !== "" && ci !== "false" && ci !== "0";
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
// src/server/run-mode.ts
|
|
1674
|
+
async function resolveRunMode(options) {
|
|
1675
|
+
if (options.runMode === "local") return "local";
|
|
1676
|
+
if (options.runMode === "docker") return "docker";
|
|
1677
|
+
if (options.isCI) return "local";
|
|
1678
|
+
if (await options.probeDocker()) return "docker";
|
|
1679
|
+
options.warn?.(
|
|
1680
|
+
"Docker daemon unavailable \u2014 running tests locally; screenshots may differ from CI. Use --run-mode local to silence this warning."
|
|
1681
|
+
);
|
|
1682
|
+
return "local";
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
// src/server/launcher-resolver.ts
|
|
1686
|
+
async function resolveRunBackend(options) {
|
|
1687
|
+
const dockerExec = createDockerExec();
|
|
1688
|
+
const resolvedRunMode = await resolveRunMode({
|
|
1689
|
+
runMode: options.runMode ?? "auto",
|
|
1690
|
+
isCI: isCI(),
|
|
1691
|
+
probeDocker: () => probeDockerDaemon(dockerExec),
|
|
1692
|
+
warn: (message) => {
|
|
1693
|
+
console.warn(`[crvy-rprtr] ${message}`);
|
|
1694
|
+
}
|
|
1061
1695
|
});
|
|
1696
|
+
const launcher = resolvedRunMode === "docker" ? createDockerLauncher({ port: options.port, docker: options.docker, exec: dockerExec }) : createLocalLauncher({ port: options.port });
|
|
1697
|
+
return {
|
|
1698
|
+
launcher,
|
|
1699
|
+
routesContextOptions: {
|
|
1700
|
+
runInfo: { mode: resolvedRunMode },
|
|
1701
|
+
containerPathMapping: resolvedRunMode === "docker" ? { from: DOCKER_WORK_DIR, to: process.cwd() } : void 0
|
|
1702
|
+
}
|
|
1703
|
+
};
|
|
1062
1704
|
}
|
|
1063
1705
|
|
|
1064
1706
|
// src/server/playwright-config.ts
|
|
1065
|
-
var
|
|
1707
|
+
var import_path7 = require("path");
|
|
1066
1708
|
var CONFIG_FILES = [
|
|
1067
1709
|
"playwright.config.ts",
|
|
1068
1710
|
"playwright.config.mts",
|
|
@@ -1074,12 +1716,15 @@ var CONFIG_FILES = [
|
|
|
1074
1716
|
async function resolvePlaywrightConfig(cwd) {
|
|
1075
1717
|
const matches = await Promise.all(
|
|
1076
1718
|
CONFIG_FILES.map(async (file) => {
|
|
1077
|
-
const candidate = (0,
|
|
1719
|
+
const candidate = (0, import_path7.join)(cwd, file);
|
|
1078
1720
|
return await fileExists(candidate) ? candidate : null;
|
|
1079
1721
|
})
|
|
1080
1722
|
);
|
|
1081
1723
|
return matches.find((path) => path !== null) ?? null;
|
|
1082
1724
|
}
|
|
1725
|
+
function resolveSeedConfigFile(option, cwd) {
|
|
1726
|
+
return option === void 0 ? resolvePlaywrightConfig(cwd) : Promise.resolve((0, import_path7.resolve)(cwd, option));
|
|
1727
|
+
}
|
|
1083
1728
|
|
|
1084
1729
|
// src/server/report-persistence.ts
|
|
1085
1730
|
function createDebouncedSaver(save, delayMs, setTimeoutFn = (fn, ms) => setTimeout(fn, ms), clearTimeoutFn = (handle) => {
|
|
@@ -1151,14 +1796,17 @@ function createRoutesContext(reportData, staticDir, saveReport, options) {
|
|
|
1151
1796
|
playwrightTestDir: options.playwrightTestDir,
|
|
1152
1797
|
playwrightSnapshotDir: options.playwrightSnapshotDir,
|
|
1153
1798
|
playwrightSnapshotPathTemplate: options.playwrightSnapshotPathTemplate,
|
|
1154
|
-
playwrightToHaveScreenshotPathTemplate: options.playwrightToHaveScreenshotPathTemplate
|
|
1799
|
+
playwrightToHaveScreenshotPathTemplate: options.playwrightToHaveScreenshotPathTemplate,
|
|
1800
|
+
containerPathMapping: options.containerPathMapping
|
|
1155
1801
|
},
|
|
1156
|
-
runContext: void 0
|
|
1802
|
+
runContext: void 0,
|
|
1803
|
+
runInfo: options.runInfo,
|
|
1804
|
+
containerPathMapping: options.containerPathMapping
|
|
1157
1805
|
};
|
|
1158
1806
|
}
|
|
1159
1807
|
|
|
1160
1808
|
// src/server/routes.ts
|
|
1161
|
-
var
|
|
1809
|
+
var import_path8 = require("path");
|
|
1162
1810
|
|
|
1163
1811
|
// src/server/run-routes.ts
|
|
1164
1812
|
function handleRunRoutes(pathname, method, runController, req) {
|
|
@@ -1180,6 +1828,10 @@ async function handleApiRun(runController, req) {
|
|
|
1180
1828
|
if (parsed === null) {
|
|
1181
1829
|
return Response.json({ ok: false, error: "Invalid request body" }, { status: 400 });
|
|
1182
1830
|
}
|
|
1831
|
+
const preparation = await runController.prepareRun();
|
|
1832
|
+
if (!preparation.ok) {
|
|
1833
|
+
return Response.json({ ok: false, reason: preparation.reason }, { status: 409 });
|
|
1834
|
+
}
|
|
1183
1835
|
const result = runController.start(parsed);
|
|
1184
1836
|
if (result.ok) return Response.json(result);
|
|
1185
1837
|
const status = result.reason === "no-tests" ? 400 : 409;
|
|
@@ -1193,7 +1845,7 @@ function handleApiStop(runController) {
|
|
|
1193
1845
|
|
|
1194
1846
|
// src/server/routes.ts
|
|
1195
1847
|
async function handleRoot(ctx) {
|
|
1196
|
-
const html = await respondWithFile((0,
|
|
1848
|
+
const html = await respondWithFile((0, import_path8.join)(ctx.staticDir, "index.html"), "text/html");
|
|
1197
1849
|
return html ?? new Response("Not Found", { status: 404 });
|
|
1198
1850
|
}
|
|
1199
1851
|
async function handleAppCss() {
|
|
@@ -1210,13 +1862,14 @@ async function handleSrcFiles(req) {
|
|
|
1210
1862
|
function handleApiReport(ctx) {
|
|
1211
1863
|
return Response.json({
|
|
1212
1864
|
...ctx.reportData,
|
|
1213
|
-
runEnabled: ctx.runContext !== void 0
|
|
1865
|
+
runEnabled: ctx.runContext !== void 0,
|
|
1866
|
+
runMode: ctx.runInfo?.mode
|
|
1214
1867
|
});
|
|
1215
1868
|
}
|
|
1216
1869
|
var APPROVAL_TARGET_ERROR = "Could not resolve approval target";
|
|
1217
1870
|
function actualPathFromUrl(ctx, actualUrl) {
|
|
1218
1871
|
if (actualUrl.startsWith("/screenshots/")) {
|
|
1219
|
-
return (0,
|
|
1872
|
+
return (0, import_path8.join)(ctx.reportData.screenshotDir, actualUrl.slice("/screenshots/".length));
|
|
1220
1873
|
}
|
|
1221
1874
|
if (actualUrl.startsWith("/file/")) {
|
|
1222
1875
|
return decodeURIComponent(actualUrl.slice("/file/".length));
|
|
@@ -1338,7 +1991,7 @@ async function handleScreenshots(ctx, req) {
|
|
|
1338
1991
|
}
|
|
1339
1992
|
async function handleDist(ctx, req) {
|
|
1340
1993
|
const path = new URL(req.url).pathname.slice("/dist/".length);
|
|
1341
|
-
const filePath = (0,
|
|
1994
|
+
const filePath = (0, import_path8.join)(ctx.staticDir, path);
|
|
1342
1995
|
const contentType = filePath.endsWith(".css") ? "text/css" : filePath.endsWith(".js") ? "application/javascript" : filePath.endsWith(".svelte") ? "text/plain" : "application/octet-stream";
|
|
1343
1996
|
const file = await respondWithFile(filePath, contentType);
|
|
1344
1997
|
return file ?? new Response("Not Found", { status: 404 });
|
|
@@ -1383,217 +2036,56 @@ function handleHttpRequest(ctx, req, runController) {
|
|
|
1383
2036
|
return Promise.resolve(new Response("Not Found", { status: 404 }));
|
|
1384
2037
|
}
|
|
1385
2038
|
|
|
1386
|
-
// src/server/
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
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}`;
|
|
2039
|
+
// src/server/server-factories.ts
|
|
2040
|
+
function createServerRunController(routesContext, wsClients, reportData, port, setRunFiltered, saveReport, launcher) {
|
|
2041
|
+
return new RunController({
|
|
2042
|
+
getRunContext: () => routesContext.runContext ?? null,
|
|
2043
|
+
port,
|
|
2044
|
+
broadcast: (message) => {
|
|
2045
|
+
broadcastToBrowsers(wsClients, message);
|
|
2046
|
+
},
|
|
2047
|
+
setReportRunning: (running) => {
|
|
2048
|
+
reportData.isRunning = running;
|
|
2049
|
+
},
|
|
2050
|
+
setRunFiltered,
|
|
2051
|
+
containerPathMapping: routesContext.containerPathMapping,
|
|
2052
|
+
saveReport,
|
|
2053
|
+
spawn: createRealSpawn(),
|
|
2054
|
+
timers: createRealTimers(),
|
|
2055
|
+
launcher
|
|
1441
2056
|
});
|
|
1442
2057
|
}
|
|
1443
|
-
function
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
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
|
-
};
|
|
2058
|
+
function createCloseHandler(persistence, runController) {
|
|
2059
|
+
return async () => {
|
|
2060
|
+
await persistence.dispose();
|
|
2061
|
+
runController.dispose();
|
|
1583
2062
|
};
|
|
1584
2063
|
}
|
|
1585
|
-
function
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
2064
|
+
function createRunControllerAndHandlers(routesContext, wsClients, reportData, currentRunIds, port, persistence, launcher) {
|
|
2065
|
+
let isFilteredRun = false;
|
|
2066
|
+
const runController = createServerRunController(
|
|
2067
|
+
routesContext,
|
|
2068
|
+
wsClients,
|
|
2069
|
+
reportData,
|
|
2070
|
+
port,
|
|
2071
|
+
(filtered) => {
|
|
2072
|
+
isFilteredRun = filtered;
|
|
1592
2073
|
},
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
2074
|
+
persistence.saveReport,
|
|
2075
|
+
launcher
|
|
2076
|
+
);
|
|
2077
|
+
const getHandlerContext = () => ({
|
|
2078
|
+
reportData,
|
|
2079
|
+
wsClients,
|
|
2080
|
+
currentRunIds,
|
|
2081
|
+
isFilteredRun,
|
|
2082
|
+
saveReport: persistence.saveReport,
|
|
2083
|
+
scheduleReportSave: persistence.scheduleReportSave,
|
|
2084
|
+
approvalRouting: routesContext.approvalRouting,
|
|
2085
|
+
routesContext,
|
|
2086
|
+
runController
|
|
2087
|
+
});
|
|
2088
|
+
return { runController, getHandlerContext };
|
|
1597
2089
|
}
|
|
1598
2090
|
|
|
1599
2091
|
// src/server/app.ts
|
|
@@ -1686,19 +2178,19 @@ function createWebSocketMessageHandler(getHandlerContext) {
|
|
|
1686
2178
|
};
|
|
1687
2179
|
}
|
|
1688
2180
|
async function resolveStaticDir(staticDir) {
|
|
1689
|
-
const currentDir = (0,
|
|
2181
|
+
const currentDir = (0, import_path9.dirname)((0, import_url.fileURLToPath)(import_meta2.url));
|
|
1690
2182
|
const candidates = staticDir === void 0 ? [
|
|
1691
2183
|
currentDir,
|
|
1692
|
-
(0,
|
|
1693
|
-
(0,
|
|
1694
|
-
(0,
|
|
1695
|
-
(0,
|
|
1696
|
-
(0,
|
|
1697
|
-
] : [staticDir, (0,
|
|
2184
|
+
(0, import_path9.join)(currentDir, "dist"),
|
|
2185
|
+
(0, import_path9.join)(currentDir, "..", "dist"),
|
|
2186
|
+
(0, import_path9.join)(currentDir, "..", "..", "dist"),
|
|
2187
|
+
(0, import_path9.join)(currentDir, ".."),
|
|
2188
|
+
(0, import_path9.join)(currentDir, "..", "..")
|
|
2189
|
+
] : [staticDir, (0, import_path9.join)(staticDir, "dist")];
|
|
1698
2190
|
const resolvedCandidates = await Promise.all(
|
|
1699
2191
|
candidates.map(async (candidate) => ({
|
|
1700
2192
|
candidate,
|
|
1701
|
-
exists: await fileExists((0,
|
|
2193
|
+
exists: await fileExists((0, import_path9.join)(candidate, "index.html"))
|
|
1702
2194
|
}))
|
|
1703
2195
|
);
|
|
1704
2196
|
const resolved = resolvedCandidates.find(({ exists }) => exists);
|
|
@@ -1709,34 +2201,31 @@ async function resolveStaticDir(staticDir) {
|
|
|
1709
2201
|
}
|
|
1710
2202
|
async function resolveReportPath(reportPath) {
|
|
1711
2203
|
if (await isDirectory(reportPath)) {
|
|
1712
|
-
return { reportFile: (0,
|
|
2204
|
+
return { reportFile: (0, import_path9.join)(reportPath, "report.json"), offlineReportDir: reportPath };
|
|
1713
2205
|
}
|
|
1714
|
-
return { reportFile: reportPath, offlineReportDir: (0,
|
|
2206
|
+
return { reportFile: reportPath, offlineReportDir: (0, import_path9.dirname)(reportPath) };
|
|
1715
2207
|
}
|
|
1716
2208
|
async function seedRunContext(routesContext, options) {
|
|
1717
2209
|
if (routesContext.runContext !== void 0) {
|
|
1718
2210
|
return;
|
|
1719
2211
|
}
|
|
1720
|
-
const configFile = options.playwrightConfig
|
|
2212
|
+
const configFile = await resolveSeedConfigFile(options.playwrightConfig, process.cwd());
|
|
1721
2213
|
if (configFile !== null) {
|
|
1722
2214
|
routesContext.runContext = { configFile, cwd: process.cwd() };
|
|
1723
2215
|
}
|
|
1724
2216
|
}
|
|
1725
|
-
function
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
},
|
|
1735
|
-
setRunFiltered,
|
|
1736
|
-
saveReport,
|
|
1737
|
-
spawn: createRealSpawn(),
|
|
1738
|
-
timers: createRealTimers()
|
|
2217
|
+
async function setupRoutesContext(options, reportData, staticDir, saveReport, port) {
|
|
2218
|
+
const { launcher, routesContextOptions } = await resolveRunBackend({
|
|
2219
|
+
runMode: options.runMode,
|
|
2220
|
+
docker: options.docker,
|
|
2221
|
+
port
|
|
2222
|
+
});
|
|
2223
|
+
const routesContext = createRoutesContext(reportData, staticDir, saveReport, {
|
|
2224
|
+
...options,
|
|
2225
|
+
...routesContextOptions
|
|
1739
2226
|
});
|
|
2227
|
+
await seedRunContext(routesContext, options);
|
|
2228
|
+
return { routesContext, launcher };
|
|
1740
2229
|
}
|
|
1741
2230
|
async function createServerApp(options = {}) {
|
|
1742
2231
|
const port = options.port ?? 3e3;
|
|
@@ -1747,30 +2236,22 @@ async function createServerApp(options = {}) {
|
|
|
1747
2236
|
const wsClients = /* @__PURE__ */ new Set();
|
|
1748
2237
|
const currentRunIds = /* @__PURE__ */ new Set();
|
|
1749
2238
|
const persistence = createReportPersistence(reportFile, reportData);
|
|
1750
|
-
const routesContext
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
2239
|
+
const { routesContext, launcher } = await setupRoutesContext(
|
|
2240
|
+
options,
|
|
2241
|
+
reportData,
|
|
2242
|
+
staticDir,
|
|
2243
|
+
persistence.saveReport,
|
|
2244
|
+
port
|
|
2245
|
+
);
|
|
2246
|
+
const { runController, getHandlerContext } = createRunControllerAndHandlers(
|
|
1754
2247
|
routesContext,
|
|
1755
2248
|
wsClients,
|
|
1756
2249
|
reportData,
|
|
2250
|
+
currentRunIds,
|
|
1757
2251
|
port,
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
},
|
|
1761
|
-
persistence.saveReport
|
|
2252
|
+
persistence,
|
|
2253
|
+
launcher
|
|
1762
2254
|
);
|
|
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
2255
|
const handleRequest = (req) => handleHttpRequest(routesContext, req, runController);
|
|
1775
2256
|
const handleWebSocketMessage = createWebSocketMessageHandler(getHandlerContext);
|
|
1776
2257
|
await loadReport(reportFile, reportData);
|
|
@@ -1783,12 +2264,6 @@ async function createServerApp(options = {}) {
|
|
|
1783
2264
|
handleWebSocketMessage
|
|
1784
2265
|
};
|
|
1785
2266
|
}
|
|
1786
|
-
function createCloseHandler(persistence, runController) {
|
|
1787
|
-
return async () => {
|
|
1788
|
-
await persistence.dispose();
|
|
1789
|
-
runController.dispose();
|
|
1790
|
-
};
|
|
1791
|
-
}
|
|
1792
2267
|
|
|
1793
2268
|
// src/server/bun-adapter.ts
|
|
1794
2269
|
function logWebSocketError(prefix, error) {
|
|
@@ -1965,7 +2440,7 @@ function attachWebSocketServer(server, app) {
|
|
|
1965
2440
|
});
|
|
1966
2441
|
}
|
|
1967
2442
|
async function listen(server, port) {
|
|
1968
|
-
await new Promise((
|
|
2443
|
+
await new Promise((resolve6, reject) => {
|
|
1969
2444
|
const onError = (error) => {
|
|
1970
2445
|
server.off("error", onError);
|
|
1971
2446
|
reject(error);
|
|
@@ -1973,7 +2448,7 @@ async function listen(server, port) {
|
|
|
1973
2448
|
server.on("error", onError);
|
|
1974
2449
|
server.listen(port, () => {
|
|
1975
2450
|
server.off("error", onError);
|
|
1976
|
-
|
|
2451
|
+
resolve6();
|
|
1977
2452
|
});
|
|
1978
2453
|
});
|
|
1979
2454
|
}
|