Add standing Playwright e2e suite; fix two real bugs it caught
The whole point of driving a real browser instead of curling api/frontend separately: none of this session's prior "live verification" ever exercised the same-origin routing production relies on (Caddy: /ws*+/api/* -> api, else -> frontend), so a real browser's relative fetch()/WebSocket calls were never actually proven to resolve. e2e/proxy.ts mirrors that routing (no caddy binary available locally); global-setup.ts/global-teardown.ts orchestrate throwaway infra + seeded data + the api/frontend/proxy processes end to end. Getting the suite green surfaced two genuine bugs invisible to unit tests: - index.pug loaded map.js via two <script type="module"> tags (one moved to <head> to fix load-order, the original left in place by mistake), causing Alpine's x-init="init()" to run twice and Leaflet to throw "Map container is already initialized" on the second call. - map.js's exportRegion() passed the Alpine-reactive `regionBounds` object straight into worker.postMessage(); Alpine wraps assigned state in Proxies, which the structured clone algorithm can't clone, so every export silently failed. Fixed by spreading into a plain object first. Covers the two flows flagged all session as verified only at the unit/curl level: the marker click-to-place/edit popup (including that a marker created while linked shows up in a second browser context with the same session, proving server-side sync) and the region-select drag + glTF export (including a real triggered file download).
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
// Kills every process global-setup.ts spawned and removes the throwaway containers it started.
|
||||
// Reads .e2e-state.json rather than relying on in-memory state from global-setup.ts, so teardown
|
||||
// is correct even if it's ever invoked independently — see global-setup.ts's doc comment on the
|
||||
// zombie-listener gotcha this is specifically guarding against.
|
||||
import { execSync } from "node:child_process";
|
||||
import { existsSync, readFileSync, rmSync } from "node:fs";
|
||||
import { STATE_FILE, type E2eState } from "./config";
|
||||
|
||||
function killPid(pid: number) {
|
||||
try {
|
||||
if (process.platform === "win32") {
|
||||
// shell:true spawned bun via cmd.exe — killing just the parent PID leaves the real bun
|
||||
// process running; /T kills the whole tree.
|
||||
execSync(`taskkill /F /T /PID ${pid}`, { stdio: "ignore" });
|
||||
} else {
|
||||
process.kill(-pid, "SIGKILL"); // negative pid: whole process group (spawned with shell)
|
||||
}
|
||||
} catch {
|
||||
// already dead — fine
|
||||
}
|
||||
}
|
||||
|
||||
export default async function globalTeardown() {
|
||||
if (!existsSync(STATE_FILE)) return;
|
||||
|
||||
const state = JSON.parse(readFileSync(STATE_FILE, "utf-8")) as E2eState;
|
||||
|
||||
for (const pid of state.pids) killPid(pid);
|
||||
|
||||
for (const container of state.containers) {
|
||||
try {
|
||||
execSync(`docker rm -f ${container}`, { stdio: "ignore" });
|
||||
} catch {
|
||||
// fine
|
||||
}
|
||||
}
|
||||
|
||||
rmSync(STATE_FILE, { force: true });
|
||||
console.log("[global-teardown] cleaned up");
|
||||
}
|
||||
Reference in New Issue
Block a user