# 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/<name>/` 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-<uuid>/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
<http://localhost:3000>; runs at
<http://localhost:3000/galaxy/galaxy/actions>.

### 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/<run_padded>/<job_index>.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
```

`<run_padded>` is the run number, zero-padded to two digits
(`01`, `02`, …); `<job_index>` 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.
