- Introduced `PosDesktopOrderBookingSelectorModal.vue` to streamline booking selection for desktop POS. - Added `orderBookingDisplay.js` utility for handling booking display and reference value normalization. - Created unit test `select-vehicle-form-pos.spec.js` to validate booking selection flow and registration matching. - Implemented `run-playwright-ci-parallel.mjs` script to optimize Playwright CI with parallel testing. - Updated styles and responsiveness for enhanced booking modal UX.
233 lines
6.7 KiB
JavaScript
233 lines
6.7 KiB
JavaScript
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 `
|
|
<tr>
|
|
<td>${result.name}</td>
|
|
<td>${result.projects.join(", ")}</td>
|
|
<td>${result.port}</td>
|
|
<td style="color: ${statusColor}; font-weight: 600;">${status}</td>
|
|
<td><a href="${reportHref}">Open report</a></td>
|
|
</tr>`;
|
|
})
|
|
.join("\n");
|
|
|
|
const html = `<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<title>Playwright Parallel Reports</title>
|
|
<style>
|
|
body { font-family: Segoe UI, Arial, sans-serif; margin: 32px; color: #111827; }
|
|
h1 { margin-bottom: 8px; }
|
|
p { color: #4b5563; }
|
|
table { border-collapse: collapse; width: 100%; margin-top: 24px; }
|
|
th, td { border: 1px solid #d1d5db; padding: 10px 12px; text-align: left; }
|
|
th { background: #f3f4f6; }
|
|
a { color: #1d4ed8; text-decoration: none; }
|
|
a:hover { text-decoration: underline; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>Playwright Parallel Reports</h1>
|
|
<p>Each child run used its own Vite port, dev-server pid file, and artifact directory.</p>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Group</th>
|
|
<th>Projects</th>
|
|
<th>Port</th>
|
|
<th>Status</th>
|
|
<th>Report</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${rows}
|
|
</tbody>
|
|
</table>
|
|
</body>
|
|
</html>`;
|
|
|
|
await fs.writeFile(path.join(reportIndexDirectory, "index.html"), html, "utf8");
|
|
}
|
|
|
|
async function shutdown(signal) {
|
|
if (isShuttingDown) {
|
|
return;
|
|
}
|
|
|
|
isShuttingDown = true;
|
|
process.stderr.write(`Received ${signal}. Stopping parallel Playwright children...\n`);
|
|
|
|
await Promise.all([...activeChildren].map((child) => killProcessTree(child.pid)));
|
|
await Promise.all(groups.map((group) => cleanupDevServerArtifacts(getArtifactNamespace(group))));
|
|
}
|
|
|
|
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
process.on(signal, () => {
|
|
void shutdown(signal).finally(() => {
|
|
process.exit(130);
|
|
});
|
|
});
|
|
}
|
|
|
|
async function main() {
|
|
console.log(
|
|
`Starting Playwright CI in parallel with ${groups.length} processes and ${perProcessWorkers} worker(s) per process.`
|
|
);
|
|
|
|
await Promise.all(groups.map((group) => cleanupDevServerArtifacts(getArtifactNamespace(group))));
|
|
const results = await Promise.all(groups.map((group, index) => spawnGroup(group, index)));
|
|
await writeCombinedReportIndex(results);
|
|
await Promise.all(groups.map((group) => cleanupDevServerArtifacts(getArtifactNamespace(group))));
|
|
|
|
const failedRuns = results.filter((result) => result.code !== 0);
|
|
if (failedRuns.length > 0) {
|
|
console.error(
|
|
`Parallel Playwright CI failed for: ${failedRuns.map((result) => `${result.name} (${result.code})`).join(", ")}`
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log(
|
|
`Parallel Playwright CI passed. Combined report index: ${path.relative(workingDirectory, path.join(reportIndexDirectory, "index.html"))}`
|
|
);
|
|
}
|
|
|
|
await main();
|