# UI Testing Tiers UI client test toolchain. Project-wide testing layers (service / inter-service / system) live in [`../../docs/TESTING.md`](../../docs/TESTING.md); this doc only covers the UI-specific tiers added in Phase 2 of [`../PLAN.md`](../PLAN.md). ## Tier 1 — per-PR Triggered by `.gitea/workflows/ui-test.yaml` on every push and pull request that touches `ui/**`, `backend/**`, `gateway/**`, `game/**`, `pkg/**`, `go.work`, or `go.work.sum`. Linux runner only. The `actions/checkout@v4` step uses `submodules: recursive`, so the runner pulls every git submodule the suite depends on (today only `pkg/geoip/test-data`, the MaxMind-DB fixtures used by `pkg/geoip`). Runs: - `go test` over the monorepo Go modules, excluding two areas: - `integration/` — needs Docker + testcontainers and is the project's `make -C integration integration` gate. - `client/` — the deprecated Fyne client (see `../PLAN.md` §74) is frozen; its tests are not run in CI. The `pkg//` modules are listed one by one in the workflow because they are independent go.work modules and `./pkg/...` does not recurse into separate modules. The exact command lives in `.gitea/workflows/ui-test.yaml`. - `pnpm test` (Vitest + `@testing-library/svelte` + `@testing-library/jest-dom`) — component / unit tests under `ui/frontend/tests/**/*.test.ts`. - `pnpm exec playwright test` — end-to-end smoke against `pnpm run dev` on port 5173. Four projects: - `chromium-desktop` (Desktop Chrome) - `webkit-desktop` (Desktop Safari) - `chromium-mobile-iphone-13` (iPhone 13 viewport, Chromium engine) - `chromium-mobile-pixel-5` (Pixel 5 viewport, Chromium engine) Playwright traces and screenshots are retained on failure and uploaded as Gitea Actions artefacts (`playwright-report` and `playwright-traces`, 14-day retention). ## Tier 2 — release Triggered by `.gitea/workflows/ui-release.yaml` on tag push (`v*`). Currently mirrors the Tier 1 step set; the dedicated release-only checks land in later phases: - **Visual regression baseline check** — Phase 33. Snapshots live in `ui/frontend/tests/__snapshots__/` until the project shifts to Argos or another visual-diff service. - **iOS smoke (Capacitor + Appium)** — Phase 32. Runs on a `macos-13` runner once the Capacitor mobile wrapper exists. Both blocks are present as commented sections in `.gitea/workflows/ui-release.yaml` with the phase number that re-enables them. ## Local execution From `ui/frontend/`: ```sh pnpm test # Vitest pnpm exec playwright install # one-time pnpm exec playwright test # all projects pnpm exec playwright test --project=chromium-desktop pnpm exec playwright show-report # open last HTML report ``` From the repository root, the same scope CI uses (backend serially because most packages spawn their own Postgres testcontainer and parallel bootstraps starve each other on constrained runners): ```sh go test -count=1 -p 1 ./backend/... go test -count=1 \ ./gateway/... ./game/... \ ./pkg/calc/... ./pkg/connector/... ./pkg/cronutil/... \ ./pkg/error/... ./pkg/geoip/... ./pkg/model/... \ ./pkg/postgres/... ./pkg/redisconn/... ./pkg/schema/... \ ./pkg/storage/... ./pkg/transcoder/... ./pkg/util/... ``` ## Local development stack For UI work that needs a real authenticated stack (verifying the FlatBuffers wire end-to-end, exercising a real lobby flow, hitting real Mailpit), bring up `tools/local-dev/`: ```sh make -C tools/local-dev up # postgres + redis + mailpit + backend + gateway pnpm -C ui/frontend dev # Vite on the host, talks to the stack ``` `ui/frontend/.env.development` already targets the stack (`http://localhost:8080`) and pins the matching response-signing public key from `tools/local-dev/keys/`. Per-developer overrides go into `.env.development.local` (gitignored). The stack honours `BACKEND_AUTH_DEV_FIXED_CODE` (default `123456` in `tools/local-dev/.env`) so the login form takes that literal in addition to the real Mailpit code; see [`../../tools/local-dev/README.md`](../../tools/local-dev/README.md) for the full runbook (regenerating the dev keypair, switching the mode off, troubleshooting common boot issues). The local-dev stack is independent from the local-ci stack below; they bind different ports and can run side by side. ## Synthetic reports for visual testing (DEV) For visual checks of the map, inspectors and order-overlay against rich game states, the lobby exposes a DEV-only "Load synthetic report" affordance (`import.meta.env.DEV`). The flow is: 1. Convert a legacy text report (`tools/local-dev/reports/{dg,gplus}/`) to JSON with the Go CLI: ```sh go run ./tools/local-dev/legacy-report/cmd/legacy-report-to-json \ --in tools/local-dev/reports/dg/KNNTS039.REP \ --out /tmp/dg39.json ``` See [`../../tools/local-dev/legacy-report/README.md`](../../tools/local-dev/legacy-report/README.md) for what the parser populates today and how to extend it when a new UI phase decodes a new `Report` field. 2. Run the UI dev server (`pnpm -C ui/frontend dev`), open the lobby, and use the "Load JSON…" file picker in the **Synthetic test reports (DEV)** section. The page navigates to `/games/synthetic-/map` with the report wired into the in-game shell. In synthetic mode: - The map view, inspectors and races view render against the loaded report exactly as they would for a real game. - Composing orders works locally (overlay applies through `applyOrderOverlay` as usual), but **nothing is sent to the gateway** — `OrderDraftStore.scheduleSync` short-circuits because the synthetic id is not a UUID and the layout deliberately does not bind a `GalaxyClient` for this game. - The order draft is persisted into the platform `Cache` under the same `order-drafts` namespace as real games, keyed by the synthetic id, so navigating back into the same synthetic session restores the draft. The cache is cleared with `__galaxyDebug.clearOrderDraft(gameId)` (DEV debug surface). - A page reload loses the in-memory report registry; opening the same synthetic id afterwards redirects to /lobby. Re-load the JSON to reseed. The synthetic-report parity rule (see [`../PLAN.md`](../PLAN.md) § Assumptions and Defaults) requires every UI phase that extends `decodeReport` to also extend the legacy parser in lockstep, or to record in the parser's `README.md` that the new field cannot be derived from legacy text. This keeps the synthetic-mode coverage in step with the contract as the UI grows. ## Local CI verification `tools/local-ci/` ships a self-contained Gitea + Actions runner via docker-compose so workflow changes are exercised end-to-end on a real runner before pushing to a remote Gitea instance. On Apple Silicon the runner and every spawned workflow container are arm64-native (no QEMU). Full runbook lives in [`../../tools/local-ci/README.md`](../../tools/local-ci/README.md); the cheat sheet below covers the operations needed when working a phase that touches CI. ### Bring up / push / tear down ```sh make -C tools/local-ci up # idempotent: gitea + runner + admin user + repo make -C tools/local-ci push # add `local-gitea` remote (first call) and push HEAD make -C tools/local-ci status # docker compose ps make -C tools/local-ci logs # tail container logs make -C tools/local-ci down # stop, keep state make -C tools/local-ci clean # stop and wipe volumes for a fresh start ``` Default credentials baked in: `galaxy:galaxy-dev` (admin user, also the owner of the `galaxy/galaxy` repo). Web UI on ; runs at . ### Inspect a run from the shell The Gitea Actions API is on `http://localhost:3000/api/v1` with basic auth. Useful for verifying a workflow change without opening the browser: ```sh # Latest workflow runs — `status` is a human-readable string here: # "running" / "success" / "failure" / "cancelled". curl -s -u galaxy:galaxy-dev \ 'http://localhost:3000/api/v1/repos/galaxy/galaxy/actions/tasks?limit=5' \ | python3 -m json.tool # Tight one-liner for the latest run only: curl -s -u galaxy:galaxy-dev \ 'http://localhost:3000/api/v1/repos/galaxy/galaxy/actions/tasks?limit=1' \ | python3 -c 'import json, sys; r=json.load(sys.stdin)["workflow_runs"][0]; print(r["run_number"], r["status"], r["display_title"])' ``` Step-by-step workflow output is stored zstd-compressed under `/data/gitea/actions_log/galaxy/galaxy//.log.zst` inside the gitea container: ```sh docker compose -f tools/local-ci/docker-compose.yml exec -T gitea sh -c ' apk add --quiet zstd zstdcat /data/gitea/actions_log/galaxy/galaxy/01/1.log.zst ' | less ``` `` is the run number, zero-padded to two digits (`01`, `02`, …); `` is the 1-based index of the job inside that run (only `1` for the current single-job workflows). ### Typical phase workflow When a phase changes anything under `.gitea/workflows/` or surfaces new tests in CI: 1. Local sanity first — run the affected commands directly (`pnpm test`, `pnpm exec playwright test`, the targeted `go test ./...` slice). 2. Commit and `make -C tools/local-ci push`. 3. Poll the API for the latest run; once it leaves `running`, inspect status. On failure pull the log via the snippet above. 4. Fix and repeat. The runner is always-on; each push triggers a fresh run (test cache is cleared by `-count=1` so a green run is honest). ### Quick syntax-only dry-run with `act` For a sub-second check that the workflow YAML is well-formed and action references resolve, without pulling images and without running anything: ```sh act -W .gitea/workflows/ui-test.yaml -n push ``` `act` doesn't honour Gitea-specific behaviours (artifact storage, secrets, run triggers). Use it for syntax checks; fall back to the local Gitea above for honest end-to-end verification.