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 forwardedArgs = process.argv.slice(2); const workingDirectory = process.cwd(); const playwrightCliPath = path.join(workingDirectory, "node_modules", "@playwright", "test", "cli.js"); const basePort = normalizePositiveInt(process.env.PLAYWRIGHT_PARALLEL_BASE_PORT, 5191); const perProcessWorkers = normalizePositiveInt(process.env.PLAYWRIGHT_PARALLEL_WORKERS, 1); const reportIndexDirectory = path.join(workingDirectory, "output", "playwright", "ci-parallel-report"); const execFileAsync = promisify(execFile); const activeChildren = new Set(); let isShuttingDown = false; const groups = [ { name: "chromium", projects: ["chromium-desktop", "chromium-mobile"], }, { name: "firefox", projects: ["firefox-desktop"], }, { name: "webkit", projects: ["webkit-desktop", "webkit-mobile"], }, ]; function normalizePositiveInt(value, fallback) { const parsed = Number(value); return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback; } function getArtifactNamespace(group) { return `ci-parallel-${group.name}`; } function getDevServerPidFile(artifactNamespace) { return path.join(workingDirectory, "output", "playwright", `dev-server-${artifactNamespace}.json`); } async function killProcessTree(pid) { if (!pid) { return; } if (process.platform === "win32") { await execFileAsync("taskkill", ["/PID", String(pid), "/T", "/F"]).catch(() => {}); return; } try { process.kill(-pid, "SIGTERM"); } catch { try { process.kill(pid, "SIGTERM"); } catch { // ignore } } } async function cleanupDevServerArtifacts(artifactNamespace) { const pidFile = getDevServerPidFile(artifactNamespace); try { const file = await fs.readFile(pidFile, "utf8"); const { pid } = JSON.parse(file); await killProcessTree(pid); } catch { // ignore missing pid files or already-exited processes } await fs.rm(pidFile, { force: true }).catch(() => {}); } function prefixStream(stream, prefix) { const lineReader = readline.createInterface({ input: stream }); lineReader.on("line", (line) => { process.stdout.write(`[${prefix}] ${line}\n`); }); } function spawnGroup(group, index) { const devPort = basePort + index; const artifactNamespace = getArtifactNamespace(group); const args = [ "test", ...group.projects.flatMap((project) => ["--project", project]), ...forwardedArgs, ]; const child = spawn(process.execPath, [playwrightCliPath, ...args], { cwd: workingDirectory, env: { ...process.env, PLAYWRIGHT_BASE_URL: "", PLAYWRIGHT_DEV_PORT: String(devPort), PLAYWRIGHT_WORKERS: String(perProcessWorkers), PLAYWRIGHT_ARTIFACT_NAMESPACE: artifactNamespace, PLAYWRIGHT_REPORTER_MODE: "line-html", PLAYWRIGHT: "1", }, stdio: ["ignore", "pipe", "pipe"], windowsHide: true, }); activeChildren.add(child); prefixStream(child.stdout, group.name); prefixStream(child.stderr, `${group.name}:err`); return new Promise((resolve) => { child.on("close", (code) => { activeChildren.delete(child); resolve({ name: group.name, code: code ?? 1, artifactNamespace, projects: group.projects, port: devPort, }); }); }); } async function writeCombinedReportIndex(results) { await fs.rm(reportIndexDirectory, { recursive: true, force: true }); await fs.mkdir(reportIndexDirectory, { recursive: true }); const rows = results .map((result) => { const status = result.code === 0 ? "passed" : "failed"; const statusColor = result.code === 0 ? "#166534" : "#991b1b"; const reportHref = `../${result.artifactNamespace}/report/index.html`; return `
Each child run used its own Vite port, dev-server pid file, and artifact directory.
| Group | Projects | Port | Status | Report |
|---|