UNPKG

playwright-archive

Version:

A lightweight CLI tool to archive and serve your Playwright test run history with a web interface. Useful for CI environments and local development.

219 lines (176 loc) 7.04 kB
const express = require("express"); const path = require("path"); const fs = require("fs-extra"); const loadUserConfig = require("../utils/userConfig"); const JsonReportParser = require("../utils/jsonReportParser"); const http = require("http"); const { spawn } = require("child_process"); const WebSocket = require("ws"); const os = require("os"); async function serve() { const app = express(); // Middleware for handling JSON in request body - must be before routes app.use(express.json()); const userConfig = loadUserConfig(); const port = userConfig?.server?.port || 3000; const host = userConfig?.server?.host || "localhost"; const packagePath = path.resolve(__dirname, ".."); const runHistoryDir = path.resolve("./run-history"); const playwrightReportDir = path.resolve("./playwright-report"); const frontendDir = path.join(packagePath, "frontend", "dist"); app.use("/run-history", express.static(runHistoryDir)); app.use("/playwright-report", express.static(playwrightReportDir)); app.use("/", express.static(frontendDir)); app.get("/api/runs", async (req, res) => { try { const runs = fs .readdirSync(runHistoryDir) .filter((d) => fs.statSync(path.join(runHistoryDir, d)).isDirectory()); const data = await Promise.all(runs.map(async (run) => { const runPath = path.join(runHistoryDir, run); const jsonParser = new JsonReportParser(runPath); const reportIndex = `/run-history/${run}/report/index.html`; const metadata = await jsonParser.getData(userConfig) || {}; return { run, report: reportIndex, metadata, }; })); res.json(data); } catch (error) { res.status(500).json({ error: error.message }); } }); // Endpoint for the latest run from playwright-report folder app.get("/api/last-run", async (req, res) => { try { const playwrightReportDir = path.resolve("./playwright-report"); if (!fs.existsSync(playwrightReportDir)) { return res.status(404).json({ error: "No playwright report found" }); } const jsonParser = new JsonReportParser(playwrightReportDir); const metadata = await jsonParser.getData(userConfig) || {}; const reportIndex = `/playwright-report/index.html`; res.json({ run: new Date().toISOString(), // current time as this is the latest run report: reportIndex, metadata, }); } catch (error) { res.status(500).json({ error: error.message }); } }); app.get("/api/disk-space", async (req, res) => { try { const isWin = os.platform() === "win32"; const command = isWin ? 'powershell -command "(Get-PSDrive C).Free, (Get-PSDrive C).Used + (Get-PSDrive C).Free"' : 'df -k / --output=size,avail'; const { stdout, stderr } = await new Promise((resolve, reject) => { const process = spawn(isWin ? 'cmd.exe' : 'sh', [isWin ? '/c' : '-c', command], { stdio: ['pipe', 'pipe', 'pipe'] }); let stdout = ''; let stderr = ''; process.stdout.on('data', data => stdout += data.toString()); process.stderr.on('data', data => stderr += data.toString()); process.on('close', code => { if (code !== 0) { reject(new Error(`Command failed with code ${code}: ${stderr}`)); } else { resolve({ stdout, stderr }); } }); }); if (isWin) { const [free, total] = stdout.trim().split('\n').map(v => parseInt(v)); res.json({ free, total, freeGb: Math.round(free / 1024 / 1024 / 1024 * 100) / 100, totalGb: Math.round(total / 1024 / 1024 / 1024 * 100) / 100 }); } else { const [, size, avail] = stdout.trim().match(/(\d+)\s+(\d+)/); const total = parseInt(size) * 1024; const free = parseInt(avail) * 1024; res.json({ free, total, freeGb: Math.round(free / 1024 / 1024 / 1024 * 100) / 100, totalGb: Math.round(total / 1024 / 1024 / 1024 * 100) / 100 }); } } catch (error) { res.status(500).json({ error: error.message }); } }); app.post("/api/run-tests", (req, res) => { const testProcess = spawn("npx", ["playwright", "test"], { stdio: "pipe", shell: true }); let output = ""; testProcess.stdout.on("data", (data) => { output += data.toString(); }); testProcess.stderr.on("data", (data) => { output += data.toString(); }); testProcess.on("close", async (code) => { console.log(`Test process finished with code ${code}`); try { // Copy metadata to run directory if tests were successful if (code === 0) { const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const runDir = path.join(runHistoryDir, timestamp); // Create run directory if it doesn't exist await fs.ensureDir(runDir); // Copy test results to run directory const testResultsPath = path.resolve("test-results", "pw-archive.json"); if (await fs.pathExists(testResultsPath)) { await fs.copy(testResultsPath, path.join(runDir, "metadata.json")); } } } catch (error) { console.error("Failed to save run metadata:", error); } res.json({ success: code === 0, output, code }); }); }); app.delete("/api/runs/:runId", async (req, res) => { const { runId } = req.params; const runDir = path.join(runHistoryDir, runId); try { if (await fs.pathExists(runDir)) { await fs.remove(runDir); res.json({ success: true }); } else { res.status(404).json({ error: "Run not found" }); } } catch (error) { res.status(500).json({ error: error.message }); } }); const server = http.createServer(app); const wss = new WebSocket.Server({ server, path: "/ws-terminal" }); wss.on("connection", (ws) => { const isWin = process.platform === "win32"; const shellCmd = isWin ? "cmd.exe" : "sh"; const shell = spawn(shellCmd, [], { stdio: "pipe" }); shell.stdout.on("data", (data) => ws.send(data.toString())); shell.stderr.on("data", (data) => ws.send(data.toString())); ws.on("message", (msg) => shell.stdin.write(msg)); ws.on("close", () => shell.kill()); }); server.listen(port, host, () => { console.log(`✅ Archive is running on http://${host}:${port}`); console.log(`🖥️ Terminal available at ws://${host}:${port}/ws-terminal`); }); } module.exports = serve;