@crvy/rprtr 0.2.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -0
- package/README.md +33 -0
- package/dist/{chunk-HAFWYUNO.js → chunk-4YJL655E.js} +16 -4
- package/dist/{chunk-473CWZ4V.js → chunk-IFHYWPXG.js} +710 -281
- package/dist/cli.d.ts +7 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +57 -6
- package/dist/index.css +7 -0
- package/dist/index.js +152 -105
- package/dist/reporter-artifact-ops.d.ts +0 -2
- package/dist/reporter-artifact-ops.d.ts.map +1 -1
- package/dist/reporter.cjs +32 -38
- package/dist/reporter.d.ts +2 -1
- package/dist/reporter.d.ts.map +1 -1
- package/dist/reporter.js +27 -43
- package/dist/schemas/http.d.ts +2 -0
- package/dist/schemas/http.d.ts.map +1 -1
- package/dist/schemas.d.ts +14 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/server/app.d.ts +6 -0
- package/dist/server/app.d.ts.map +1 -1
- package/dist/server/artifact-routes.d.ts.map +1 -1
- package/dist/server/docker-launcher.d.ts +27 -0
- package/dist/server/docker-launcher.d.ts.map +1 -0
- package/dist/server/docker-support.d.ts +66 -0
- package/dist/server/docker-support.d.ts.map +1 -0
- package/dist/server/handlers.d.ts +1 -1
- package/dist/server/handlers.d.ts.map +1 -1
- package/dist/server/launcher-resolver.d.ts +23 -0
- package/dist/server/launcher-resolver.d.ts.map +1 -0
- package/dist/server/playwright-config.d.ts +6 -0
- package/dist/server/playwright-config.d.ts.map +1 -1
- package/dist/server/routes-context.d.ts +6 -1
- package/dist/server/routes-context.d.ts.map +1 -1
- package/dist/server/routes.d.ts +7 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/run-controller.d.ts +19 -19
- package/dist/server/run-controller.d.ts.map +1 -1
- package/dist/server/run-launcher.d.ts +45 -0
- package/dist/server/run-launcher.d.ts.map +1 -0
- package/dist/server/run-mode.d.ts +15 -0
- package/dist/server/run-mode.d.ts.map +1 -0
- package/dist/server/server-factories.d.ts +21 -0
- package/dist/server/server-factories.d.ts.map +1 -0
- package/dist/server.cjs +885 -446
- package/dist/server.js +2 -2
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
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,201 +659,738 @@ 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
692
|
}
|
|
713
|
-
function
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
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
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
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
|
|
743
|
-
|
|
709
|
+
async function forceRemoveContainer(exec, name) {
|
|
710
|
+
try {
|
|
711
|
+
await exec(["rm", "-f", name]);
|
|
712
|
+
} catch {
|
|
713
|
+
}
|
|
744
714
|
}
|
|
745
|
-
function
|
|
746
|
-
|
|
747
|
-
return
|
|
748
|
-
|
|
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
|
-
|
|
753
|
-
|
|
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
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
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
|
|
764
|
-
|
|
765
|
-
|
|
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
|
|
771
|
-
const
|
|
772
|
-
const
|
|
773
|
-
const
|
|
774
|
-
return
|
|
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
|
|
777
|
-
if (
|
|
778
|
-
return
|
|
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
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
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
|
-
|
|
802
|
-
|
|
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
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
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
|
|
817
|
-
return
|
|
818
|
-
|
|
819
|
-
|
|
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/
|
|
824
|
-
var
|
|
825
|
-
var
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
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
|
|
833
|
-
|
|
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
|
-
|
|
836
|
-
const
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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/
|
|
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}`;
|
|
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
|
|
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
|
-
};
|
|
2022
|
+
function createCloseHandler(persistence, runController) {
|
|
2023
|
+
return async () => {
|
|
2024
|
+
await persistence.dispose();
|
|
2025
|
+
runController.dispose();
|
|
1583
2026
|
};
|
|
1584
2027
|
}
|
|
1585
|
-
function
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
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
|
-
|
|
1594
|
-
|
|
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,
|
|
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,
|
|
1693
|
-
(0,
|
|
1694
|
-
(0,
|
|
1695
|
-
(0,
|
|
1696
|
-
(0,
|
|
1697
|
-
] : [staticDir, (0,
|
|
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,
|
|
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,
|
|
2168
|
+
return { reportFile: (0, import_path9.join)(reportPath, "report.json"), offlineReportDir: reportPath };
|
|
1713
2169
|
}
|
|
1714
|
-
return { reportFile: reportPath, offlineReportDir: (0,
|
|
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
|
|
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
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
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
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
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
|
-
|
|
1759
|
-
|
|
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((
|
|
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
|
-
|
|
2415
|
+
resolve6();
|
|
1977
2416
|
});
|
|
1978
2417
|
});
|
|
1979
2418
|
}
|