Add Playwright result download dashboard
This commit is contained in:
+1
-1
@@ -9,4 +9,4 @@ COPY . .
|
|||||||
|
|
||||||
RUN mkdir -p results test-results playwright-report
|
RUN mkdir -p results test-results playwright-report
|
||||||
|
|
||||||
CMD ["bash", "-lc", "node run-api-checks.js && npx playwright test --reporter=list --workers=1; echo 'Tests completed. Container kept alive.'; tail -f /dev/null"]
|
CMD ["bash", "-lc", "node run-api-checks.js && npx playwright test --reporter=list --workers=1; npm run report:json; npm run update:excel; echo 'Tests completed. Starting result server.'; node scripts/result-server.js"]
|
||||||
@@ -0,0 +1,257 @@
|
|||||||
|
const http = require("http");
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const PORT = Number(process.env.RESULT_SERVER_PORT || 3000);
|
||||||
|
const ROOT = process.env.RESULT_ROOT || "/tests";
|
||||||
|
|
||||||
|
const FILES = [
|
||||||
|
{
|
||||||
|
title: "Updated Excel Result",
|
||||||
|
path: "results/Audit_Firm_ERP_Master_UAT_VAPT_Checklist_v2_4_Results.xlsx",
|
||||||
|
type: "Excel",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Merged Results JSON",
|
||||||
|
path: "results/merged-results.json",
|
||||||
|
type: "JSON",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "API Check Results JSON",
|
||||||
|
path: "results/api-check-results.json",
|
||||||
|
type: "JSON",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Playwright Report Index",
|
||||||
|
path: "playwright-report/index.html",
|
||||||
|
type: "HTML",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function safeJoin(relativePath) {
|
||||||
|
const fullPath = path.resolve(ROOT, relativePath);
|
||||||
|
if (!fullPath.startsWith(path.resolve(ROOT))) {
|
||||||
|
throw new Error("Invalid path");
|
||||||
|
}
|
||||||
|
return fullPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fileInfo(relativePath) {
|
||||||
|
try {
|
||||||
|
const full = safeJoin(relativePath);
|
||||||
|
const stat = fs.statSync(full);
|
||||||
|
return {
|
||||||
|
exists: true,
|
||||||
|
size: stat.size,
|
||||||
|
mtime: stat.mtime,
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
exists: false,
|
||||||
|
size: 0,
|
||||||
|
mtime: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatBytes(bytes) {
|
||||||
|
if (!bytes) return "-";
|
||||||
|
const units = ["B", "KB", "MB", "GB"];
|
||||||
|
let size = bytes;
|
||||||
|
let unit = 0;
|
||||||
|
while (size >= 1024 && unit < units.length - 1) {
|
||||||
|
size = size / 1024;
|
||||||
|
unit++;
|
||||||
|
}
|
||||||
|
return `${size.toFixed(unit === 0 ? 0 : 2)} ${units[unit]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function contentType(filePath) {
|
||||||
|
if (filePath.endsWith(".xlsx")) {
|
||||||
|
return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
|
||||||
|
}
|
||||||
|
if (filePath.endsWith(".json")) return "application/json";
|
||||||
|
if (filePath.endsWith(".html")) return "text/html; charset=utf-8";
|
||||||
|
if (filePath.endsWith(".png")) return "image/png";
|
||||||
|
if (filePath.endsWith(".webm")) return "video/webm";
|
||||||
|
if (filePath.endsWith(".zip")) return "application/zip";
|
||||||
|
return "application/octet-stream";
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderHome() {
|
||||||
|
const rows = FILES.map((item) => {
|
||||||
|
const info = fileInfo(item.path);
|
||||||
|
const status = info.exists ? "Available" : "Missing";
|
||||||
|
const downloadLink = info.exists
|
||||||
|
? `<a class="btn" href="/download?file=${encodeURIComponent(item.path)}">Download</a>`
|
||||||
|
: `<span class="btn disabled">Not available</span>`;
|
||||||
|
|
||||||
|
return `
|
||||||
|
<tr>
|
||||||
|
<td>${item.title}</td>
|
||||||
|
<td>${item.type}</td>
|
||||||
|
<td class="${info.exists ? "ok" : "bad"}">${status}</td>
|
||||||
|
<td>${formatBytes(info.size)}</td>
|
||||||
|
<td>${info.mtime ? info.mtime.toLocaleString() : "-"}</td>
|
||||||
|
<td>${downloadLink}</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
}).join("");
|
||||||
|
|
||||||
|
return `<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Playwright UAT/VAPT Results</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
margin: 32px;
|
||||||
|
background: #f7f7f7;
|
||||||
|
color: #222;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,.08);
|
||||||
|
max-width: 1100px;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
margin-top: 0;
|
||||||
|
font-size: 26px;
|
||||||
|
}
|
||||||
|
.sub {
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
th, td {
|
||||||
|
padding: 12px;
|
||||||
|
border-bottom: 1px solid #e5e5e5;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
.ok {
|
||||||
|
color: #0a7a2f;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.bad {
|
||||||
|
color: #b00020;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: #1f6feb;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.disabled {
|
||||||
|
background: #999;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.note {
|
||||||
|
margin-top: 18px;
|
||||||
|
padding: 12px;
|
||||||
|
background: #fff8dc;
|
||||||
|
border: 1px solid #f0dc8c;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
code {
|
||||||
|
background: #eee;
|
||||||
|
padding: 2px 5px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="card">
|
||||||
|
<h1>Playwright UAT/VAPT Results</h1>
|
||||||
|
<div class="sub">Last refreshed: ${new Date().toLocaleString()}</div>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>File</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Size</th>
|
||||||
|
<th>Modified</th>
|
||||||
|
<th>Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>${rows}</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="note">
|
||||||
|
Keep this page private. Test reports may contain screenshots, URLs, user emails, and error details.
|
||||||
|
Recommended Coolify port: <code>3000</code>.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendFile(res, relativePath) {
|
||||||
|
let fullPath;
|
||||||
|
try {
|
||||||
|
fullPath = safeJoin(relativePath);
|
||||||
|
} catch {
|
||||||
|
res.writeHead(400);
|
||||||
|
res.end("Invalid file path");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs.existsSync(fullPath) || !fs.statSync(fullPath).isFile()) {
|
||||||
|
res.writeHead(404);
|
||||||
|
res.end("File not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.writeHead(200, {
|
||||||
|
"Content-Type": contentType(fullPath),
|
||||||
|
"Content-Disposition": `attachment; filename="${path.basename(fullPath)}"`,
|
||||||
|
});
|
||||||
|
|
||||||
|
fs.createReadStream(fullPath).pipe(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = http.createServer((req, res) => {
|
||||||
|
const url = new URL(req.url, `http://${req.headers.host}`);
|
||||||
|
|
||||||
|
if (url.pathname === "/") {
|
||||||
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
||||||
|
res.end(renderHome());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.pathname === "/download") {
|
||||||
|
const file = url.searchParams.get("file");
|
||||||
|
if (!file) {
|
||||||
|
res.writeHead(400);
|
||||||
|
res.end("Missing file parameter");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sendFile(res, file);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.writeHead(404);
|
||||||
|
res.end("Not found");
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen(PORT, "0.0.0.0", () => {
|
||||||
|
console.log(`Playwright result server running on http://0.0.0.0:${PORT}`);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user