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:
2026-08-09 12:34:12 +02:00
parent c78661efb5
commit 7b85f4dff1
14 changed files with 704 additions and 21 deletions
+27
View File
@@ -101,6 +101,33 @@ bun test
Each test file creates and tears down its own server row (random token per run) so runs never
collide with each other or with real dev data — see `api/src/test-helpers.ts`.
## Running the e2e suite
`bun test` above exercises `api` and `frontend` independently — it never proves the browser can
actually reach both through the same origin the way production's Caddy routing does (`/ws*` and
`/api/*` -> `api`, everything else -> `frontend`, see `Caddyfile`). `e2e/` is a standing
Playwright suite that closes that gap: it drives a real Chromium browser against the full stack
behind a small routing-equivalent proxy (`e2e/proxy.ts` — no `caddy` binary is available in this
dev environment, so it isn't real Caddy, just the same three routing rules).
```
cd e2e
bun install
bunx playwright install chromium # one-time, downloads the browser binary
bunx playwright test
```
`global-setup.ts` does everything by itself — no manual container/migration steps needed first
(unlike the `bun test` section above): throwaway Postgres/Redis/MinIO containers
(`mcmapper-e2e-*`, distinct names/ports from the `bun test` ones so both can run at once),
migrations, a seeded server + linked account/session + a 5x5-chunk terrain footprint around the
world origin, then the `api`/`frontend`/proxy processes. `global-teardown.ts` kills every spawned
process and removes the containers afterward. Covers the two UI flows most worth a real
click-through: the marker click-to-place/edit popup (`tests/markers.spec.ts`, including that a
marker created while linked shows up in a second browser context with the same session — the
cross-device sync claim) and the region-select drag + glTF export (`tests/region-export.spec.ts`,
including a real triggered file download).
## Attribution
See `THIRD_PARTY_NOTICES.md`.