Refactor: Remove unused readJsonFile helper, improve server process handling in Playwright setup, and optimize superuser booking and draft count logic.

This commit is contained in:
Jeppe Bundgaard
2026-04-22 11:49:44 +02:00
parent 7bdfb43635
commit 6e3506ff28
31 changed files with 868 additions and 600 deletions
+169 -26
View File
@@ -1,17 +1,20 @@
import { execFile, spawn } from "node:child_process";
import fs from "node:fs/promises";
import path from "node:path";
import readline from "node:readline";
import { promisify } from "node:util";
const execFileAsync = promisify(execFile);
const devPort = Number(process.env.PLAYWRIGHT_DEV_PORT || 5173);
const baseURL = `http://localhost:${devPort}`;
const devHost = process.env.PLAYWRIGHT_DEV_HOST || "127.0.0.1";
const baseURL = process.env.PLAYWRIGHT_BASE_URL || `http://${devHost}:${devPort}`;
const runtimeNamespace = String(process.env.PLAYWRIGHT_ARTIFACT_NAMESPACE || `port-${devPort}`)
.trim()
.replace(/[^a-zA-Z0-9._-]+/g, "-");
const pidFile = path.resolve(process.cwd(), "output/playwright", `dev-server-${runtimeNamespace}.json`);
const posViewsDirectory = path.resolve(process.cwd(), "src/views/dashboards/departmentDashboard/modules/Pos");
const shopComponentsDirectory = path.resolve(process.cwd(), "src/components/shop");
const serverOutputLimit = 80;
async function getListeningProcessOnWindows(port) {
const { stdout } = await execFileAsync("netstat", ["-ano", "-p", "tcp"], {
@@ -35,6 +38,59 @@ async function getListeningProcessOnWindows(port) {
};
}
async function readUnixProcessDetails(pid, fallbackCommandLine = "") {
const [rawCommandLine, currentWorkingDirectory] = await Promise.all([
fs.readFile(`/proc/${pid}/cmdline`).catch(() => Buffer.alloc(0)),
fs.readlink(`/proc/${pid}/cwd`).catch(() => ""),
]);
const commandLine = rawCommandLine.toString("utf8").split("\0").filter(Boolean).join(" ").trim();
const commandContext = [commandLine || fallbackCommandLine, currentWorkingDirectory].filter(Boolean).join(" | ");
return {
Id: pid,
CommandLine: commandContext || fallbackCommandLine,
};
}
function parseSsListeningProcess(stdout, port) {
const line = stdout
.split(/\r?\n/)
.find((entry) => entry.includes("LISTEN") && entry.includes(`:${port}`) && entry.includes("pid="));
if (!line) {
return null;
}
const pidMatch = line.match(/pid=(\d+)/);
if (!pidMatch) {
return null;
}
const commandMatch = line.match(/users:\(\("([^"]+)"/);
return {
pid: Number(pidMatch[1]),
commandLine: commandMatch ? commandMatch[1] : "",
};
}
function parseNetstatListeningProcess(stdout, port) {
const line = stdout
.split(/\r?\n/)
.find((entry) => entry.includes("LISTEN") && entry.match(new RegExp(`:${port}\\s`)));
if (!line) {
return null;
}
const pidMatch = line.match(/\s(\d+)\/([^\s]+)\s*$/);
if (!pidMatch) {
return null;
}
return {
pid: Number(pidMatch[1]),
commandLine: pidMatch[2],
};
}
async function getListeningProcessOnUnix(port) {
try {
const { stdout } = await execFileAsync("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-Fp", "-Fc"], {
@@ -47,13 +103,36 @@ async function getListeningProcessOnUnix(port) {
return null;
}
return {
Id: Number(pidMatch[1]),
CommandLine: commandMatch ? commandMatch[1] : "",
};
return readUnixProcessDetails(Number(pidMatch[1]), commandMatch ? commandMatch[1] : "");
} catch {
return null;
// Fall through to other tools that are commonly available on Linux runners.
}
try {
const { stdout } = await execFileAsync("ss", ["-ltnp", `sport = :${port}`], {
cwd: process.cwd(),
});
const processMatch = parseSsListeningProcess(stdout, port);
if (processMatch) {
return readUnixProcessDetails(processMatch.pid, processMatch.commandLine);
}
} catch {
// Fall through to netstat.
}
try {
const { stdout } = await execFileAsync("netstat", ["-ltnp"], {
cwd: process.cwd(),
});
const processMatch = parseNetstatListeningProcess(stdout, port);
if (processMatch) {
return readUnixProcessDetails(processMatch.pid, processMatch.commandLine);
}
} catch {
// No more fallbacks available.
}
return null;
}
async function getListeningProcess(port) {
@@ -85,23 +164,82 @@ async function killProcessTree(pid) {
}
}
async function waitForServerReady(url, timeoutMs = 120_000) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
try {
const response = await fetch(url, { redirect: "manual" });
if (response.status < 500) {
return;
}
} catch {
// keep polling until ready
function captureProcessOutput(stream, lines) {
const reader = readline.createInterface({ input: stream });
reader.on("line", (line) => {
lines.push(line);
if (lines.length > serverOutputLimit) {
lines.splice(0, lines.length - serverOutputLimit);
}
});
return reader;
}
await new Promise((resolve) => setTimeout(resolve, 1000));
function formatProcessOutputSection(label, lines) {
if (lines.length === 0) {
return "";
}
throw new Error(`Timed out waiting for the local Playwright dev server at ${url}.`);
return `\n${label}:\n${lines.join("\n")}`;
}
function formatServerExitError(url, exitInfo, stdoutLines, stderrLines) {
const exitLabel = exitInfo.signal ? `signal ${exitInfo.signal}` : `code ${exitInfo.code ?? 0}`;
return new Error(
`The local Playwright dev server exited before ${url} became ready (${exitLabel}).${formatProcessOutputSection(
"stdout",
stdoutLines
)}${formatProcessOutputSection("stderr", stderrLines)}`
);
}
async function waitForServerReady(url, serverProcess, stdoutLines, stderrLines, timeoutMs = 120_000) {
const deadline = Date.now() + timeoutMs;
let exitInfo = null;
const handleExit = (code, signal) => {
exitInfo = {
code,
signal,
};
};
serverProcess.on("exit", handleExit);
try {
while (Date.now() < deadline) {
if (exitInfo || serverProcess.exitCode !== null || serverProcess.signalCode !== null) {
throw formatServerExitError(
url,
exitInfo || {
code: serverProcess.exitCode,
signal: serverProcess.signalCode,
},
stdoutLines,
stderrLines
);
}
try {
const response = await fetch(url, { redirect: "manual" });
if (response.status < 500) {
return;
}
} catch {
// keep polling until ready
}
await new Promise((resolve) => setTimeout(resolve, 1000));
}
throw new Error(
`Timed out waiting for the local Playwright dev server at ${url}.${formatProcessOutputSection(
"stdout",
stdoutLines
)}${formatProcessOutputSection("stderr", stderrLines)}`
);
} finally {
serverProcess.off("exit", handleExit);
}
}
async function buildWarmupTargets() {
@@ -180,35 +318,40 @@ export default async function globalSetup() {
const serverProcess =
process.platform === "win32"
? spawn("cmd.exe", ["/d", "/s", "/c", `npm.cmd run dev -- --host localhost --port ${devPort} --strictPort`], {
? spawn("cmd.exe", ["/d", "/s", "/c", `npm.cmd run dev -- --host ${devHost} --port ${devPort} --strictPort`], {
cwd: process.cwd(),
detached: true,
env: {
...process.env,
PLAYWRIGHT: "1",
},
stdio: "ignore",
stdio: ["ignore", "pipe", "pipe"],
windowsHide: true,
})
: spawn("npm", ["run", "dev", "--", "--host", "localhost", "--port", String(devPort), "--strictPort"], {
: spawn("npm", ["run", "dev", "--", "--host", devHost, "--port", String(devPort), "--strictPort"], {
cwd: process.cwd(),
detached: true,
env: {
...process.env,
PLAYWRIGHT: "1",
},
stdio: "ignore",
stdio: ["ignore", "pipe", "pipe"],
});
serverProcess.unref();
const stdoutLines = [];
const stderrLines = [];
const stdoutReader = captureProcessOutput(serverProcess.stdout, stdoutLines);
const stderrReader = captureProcessOutput(serverProcess.stderr, stderrLines);
await fs.writeFile(pidFile, JSON.stringify({ pid: serverProcess.pid, port: devPort }), "utf8");
try {
await waitForServerReady(baseURL);
await waitForServerReady(baseURL, serverProcess, stdoutLines, stderrLines);
await warmUpDevServer(baseURL);
} catch (error) {
await killProcessTree(serverProcess.pid);
throw error;
} finally {
stdoutReader.close();
stderrReader.close();
}
}